OSWorkFlow入門指南

悠悠隱於市發表於2011-01-27
OSWorkFlow入門指南
目的 

這篇指導資料的目的是介紹OSWorkflow的所有概念,指導你如何使用它,並且保證你逐步理解OSWorkflow的關鍵內容。 

本指導資料假定你已經部署OSWorkflow的範例應用在你的container上。範例應用部署是使用基於記憶體的資料儲存,這樣你不需要擔心如何配置其他持久化的例子。範例應用的目的是為了說明如何應用OSWorkflow,一旦你精通了OSWorkflow的流程定義描述符概念和要素,應該能通過閱讀這些流程定義檔案而瞭解實際的流程。 

本指導資料目前有3部分: 
1. 你的第一個工作流 
2. 測試你的工作流 
3. 更多的流程定義描述符概念 

1. Your first workflow 

建立描述符 

首先,讓我們來定義工作流。你可以使用任何名字來命名工作流。一個工作流對應一個XML格式的定義檔案。讓我們來開始新建一個“myworkflow.xml”的檔案,這是樣板檔案: 

<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE workflow PUBLIC 
  "-//OpenSymphony Group//DTD OSWorkflow 2.7//EN" 
  "http://www.opensymphony.com/osworkflow/workflow_2_7.dtd"> 
<workflow> 
  <initial-actions> 
    ... 
  </initial-actions> 
  <steps> 
    ... 
  </steps> 
</workflow> 
首先是標準的XML頭部,要注意的是OSWorkflow將會通過這些指定的DTD來驗證XML內容的合法性。你可以使用絕大多數的XML編輯工具來編輯它,並且可以highlight相應的錯誤。 

步驟和動作 

接下來我們來定義初始化動作和步驟。首先需要理解的OSWorkflow重要概念是steps (步驟) 和 actions (動作)。一個步驟是工作流所處的位置,比如一個簡單的工作流過程,它可能從一個步驟流轉到另外一個步驟(或者有時候還是停留在一樣的步驟)。舉例來說,一個文件管理系統的流程,它的步驟名稱可能有“First Draft - 草案初稿”,“Edit Stage -編輯階段”,“At publisher - 出版商”等。 

動作指定了可能發生在步驟內的轉變,通常會導致步驟的變更。在我們的檔案管理系統中,在“草案初稿”這個步驟可能有“start first draft - 開始草案初稿”和“complete first draft - 完成草案初稿”這樣2個動作。 

簡單的說,步驟是“在哪裡”,動作是“可以去哪裡”。 

初始化步驟是一種特殊型別的步驟,它用來啟動工作流。在一個工作流程開始前,它是沒有狀態,不處在任何一個步驟,使用者必須採取某些動作才能開始這個流程。這些特殊步驟被定義在 <initial-actions>。 

在我們的例子裡面,假定只有一個簡單的初始化步驟:“Start Workflow”,它的定義在裡面<initial-actions>: 

<action id="1" name="Start Workflow"> 
  <results> 
    <unconditional-result old-status="Finished" status="Queued" step="1"/> 
  </results> 
</action> 
這個動作是最簡單的型別,只是簡單地指明瞭下一個我們要去的步驟和狀態。 

工作流狀態 

工作流狀態是一個用來描述工作流程中具體步驟狀態的字串。在我們的文件管理系統中,在“草案初稿”這個步驟可能有2個不同的狀態:“Underway - 進行中”和“Queued - 等候處理中” 

我們使用“Queued”指明這個條目已經被排入“First Draft”步驟的佇列。比如說某人請求編寫某篇文件,但是還沒有指定作者,那麼這個文件在“First Draft”步驟的狀態就是“Queued”。“Underway”狀態被用來指明一個作者已經挑選了一篇文件開始撰寫,而且可能正在鎖定這篇文件。 

第一個步驟 

讓我們來看第一個步驟是怎樣被定義在<steps>元素中的。我們有2個動作:第一個動作是保持當前步驟不變,只是改變了狀態到“Underway”,第二個動作是移動到工作流的下一步驟。我們來新增如下的內容到<steps>元素: 

<step id="1" name="First Draft"> 
  <actions> 
    <action id="1" name="Start First Draft"> 
      <results> 
        <unconditional-result old-status="Finished" 
        status="Underway" step="1"/> 
      </results> 
    </action> 
    <action id="2" name="Finish First Draft"> 
      <results> 
        <unconditional-result old-status="Finished" 
        status="Queued" step="2"/> 
      </results> 
    </action> 
  </actions> 
</step> 
<step id="2" name="finished" /> 
這樣我們就定義了2個動作,old-status屬性是用來指明當前步驟完成以後的狀態是什麼,在大多數的應用中,通常用"Finished"表示。 

上面定義的這2個動作是沒有任何限制的。比如,一個使用者可以呼叫action 2而不用先呼叫action 1。很明顯的,我們如果沒有開始撰寫草稿,是不可能去完成一個草稿的。同樣的,上面的定義也允許你開始撰寫草稿多次,這也是毫無意義的。我們也沒有做任何的處理去限制其他使用者完成別人的草稿。這些都應該需要想辦法避免。 

讓我們來一次解決這些問題。首先,我們需要指定只有工作流的狀態為“Queued”的時候,一個caller (呼叫者)才能開始撰寫草稿的動作。這樣就可以阻止其他使用者多次呼叫開始撰寫草稿的動作。我們需要指定動作的約束,約束是由Condition(條件)組成。 

條件 

OSWorkflow 有很多有用的內建條件可以使用。在此相關的條件是“StatusCondition - 狀態條件”。 條件也可以接受引數,引數的名稱通常被定義在javadocs裡(如果是使用Java Class實現的條件的話)。在這個例子裡面,狀態條件接受一個名為“status”的引數,指明瞭需要檢查的狀態條件。我們可以從下面的xml定義裡面清楚的理解: 

<action id="1" name="Start First Draft"> 
  <restrict-to> 
    <conditions> 
      <condition type="class"> 
        <arg name="class.name"> 
          com.opensymphony.workflow.util.StatusCondition</arg> 
        <arg name="status">Queued</arg> 
      </condition> 
    </conditions> 
  </restrict-to> 
  <results> 
    <unconditional-result old-status="Finished" status="Underway" step="1"/> 
  </results> 
</action> 
希望對於條件的理解現在已經清楚了。上面的條件定義保證了動作1只能在當前狀態為“Queued”的時候才能被呼叫,也就是說在初始化動作被呼叫以後。 

函式 

接下來,我們想在一個使用者開始撰寫草稿以後,設定他為“owner”。為了達到這樣的目的,我們需要做2件事情: 

1) 通過一個函式設定“caller”變數在當前的環境設定裡。 
2) 根據“caller”變數來設定“owner”屬性。 

函式是OSWorkflow的一個功能強大的特性。函式基本上是一個在工作流程中的工作單位,他不會影響到流程本身。舉例來說,你可能有一個“SendEmail”的函式,用來在某些特定的流程流轉發生時來傳送email提醒。 

函式也可以用來新增變數到當前的環境設定裡。變數是一個指定名稱的物件,可以用來在工作流中被以後的函式或者指令碼使用。 

OSWorkflow提供了一些內建的常用函式。其中一個稱為“Caller”,這個函式會獲得當前呼叫工作流的使用者,並把它放入一個名為“caller”的字元型變數中。 

因為我們需要追蹤是哪個使用者開始了編寫草稿,所以可以使用這個函式來修改我們的動作定義: 

<action id="1" name="Start First Draft"> 
  <pre-functions> 
    <function type="class"> 
      <arg name="class.name"> 
      com.opensymphony.workflow.util.Caller</arg> 
    </function> 
  </pre-functions> 
  <results> 
    <unconditional-result old-status="Finished"status="Underway" 
    step="1" owner="${caller}"/> 
  </results> 
</action> 
h3 組合起來 
把這些概念都組合起來,這樣我們就有了動作1: 

<action id="1" name="Start First Draft"> 
  <restrict-to> 
    <conditions> 
      <condition type="class"> 
        <arg name="class.name"> 
          com.opensymphony.workflow.util.StatusCondition 
        </arg> 
        <arg name="status">Queued</arg> 
      </condition> 
    </conditions> 
  </restrict-to> 
  <pre-functions> 
    <function type="class"> 
      <arg name="class.name"> 
        com.opensymphony.workflow.util.Caller 
      </arg> 
    </function> 
  </pre-functions> 
  <results> 
    <unconditional-result old-status="Finished"status="Underway" 
    step="1"  owner="${caller}"/> 
  </results> 
</action> 
我們使用類似想法來設定動作2: 

<action id="2" name="Finish First Draft"> 
  <restrict-to> 
    <conditions type="AND"> 
      <condition type="class"> 
        <arg name="class.name"> 
          com.opensymphony.workflow.util.StatusCondition 
        </arg> 
        <arg name="status">Underway</arg> 
      </condition> 
      <condition type="class"> 
        <arg name="class.name"> 
          com.opensymphony.workflow.util.AllowOwnerOnlyCondition 
       </arg> 
      </condition> 
    </conditions> 
  </restrict-to> 
  <results> 
    <unconditional-result old-status="Finished" status="Queued" step="2"/> 
  </results> 
</action> 
在這裡我們指定了一個新的條件:“allow owner only”。這樣能夠保證只有開始撰寫這份草稿的使用者才能完成它。而狀態條件確保了只有在“Underway”狀態下的流程才能呼叫“finish first draft”動作。 

把他們組合在一起,我們就有了第一個流程定義: 

<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE workflow PUBLIC 
  "-//OpenSymphony Group//DTD OSWorkflow 2.7//EN" 
  "http://www.opensymphony.com/osworkflow/workflow_2_7.dtd"> 
<workflow> 
  <initial-actions> 
    <action id="1" name="Start Workflow"> 
      <results> 
        <unconditional-result old-status="Finished" 
        status="Queued" step="1"/> 
      </results> 
    </action> 
  </initial-actions> 
  <steps> 
    <step id="1" name="First Draft"> 
      <actions> 
        <action id="1" name="Start First Draft"> 
          <restrict-to> 
            <conditions> 
              <condition type="class"> 
                <arg name="class.name"> 
                   com.opensymphony.workflow.util.StatusCondition 
                </arg> 
                <arg name="status">Queued</arg> 
              </condition> 
            </conditions> 
          </restrict-to> 
          <pre-functions> 
            <function type="class"> 
              <arg name="class.name"> 
                 com.opensymphony.workflow.util.Caller 
              </arg> 
            </function> 
          </pre-functions> 
          <results> 
            <unconditional-result old-status="Finished" status="Underway" 
            step="1"  owner="${caller}"/> 
          </results> 
        </action> 
        <action id="2" name="Finish First Draft"> 
          <restrict-to> 
            <conditions type="AND"> 
              <condition type="class"> 
                <arg name="class.name"> 
                    com.opensymphony.workflow.util.StatusCondition 
                </arg> 
                <arg name="status">Underway</arg> 
              </condition> 
              <condition type="class"> 
                <arg name="class.name"> 
                  com.opensymphony.workflow.util. 
                  AllowOwnerOnlyCondition 
                </arg> 
              </condition> 
            </conditions> 
          </restrict-to> 
          <results> 
            <unconditional-result old-status="Finished" 
            status="Queued" step="2"/> 
          </results> 
        </action> 
      </actions> 
    </step> 
    <step id="2" name="finished" /> 
  </steps> 
</workflow> 
現在這個工作流的定義已經完整了,讓我們來測試和檢查它的執行。 

2. Testing your workflow 

現在我們已經完成了一個完整的工作流定義,下一步是檢驗它是否按照我們預想的方式執行。 

在一個快速開發環境中,最簡單的方法就是寫一個測試案例。通過測試案例呼叫工作流,根據校驗結果和捕捉可能發生的錯誤,來保證流程定義的正確性。 

我們假設你已經熟悉Junit和了解怎樣編寫測試案例。如果你對這些知識還不瞭解的話,可以去JUnit的網站查詢、閱讀相關文件。編寫測試案例會成為你的一個非常有用的工具。 

在開始載入流程定義、呼叫動作以前,我們需要配置OSWorkflow的資料儲存方式和定義檔案的位置等。 

配置 osworkflow.xml 

我們需要建立的第一個檔案是 osworkflow.xml。子: 

<osworkflow> 
  <persistence class="com.opensymphony.workflow. 
  spi.memory.MemoryWorkflowStore"/> 
  <factory class="com.opensymphony.workflow.loader.XMLWorkflowFactory"> 
    <property key="resource" value="workflows.xml" /> 
  </factory> 
</osworkflow> 
這個例子指明瞭我們準備使用記憶體 (MemoryWorkflowStore) 來儲存流程資料。這樣可以減少設定資料庫的相關資訊,減少出問題的可能性。用記憶體持久化對於測試來說是非常方便的。 

Workflow factories 

上面的配置檔案還指明瞭我們工作流工廠(XMLWorkflowFactory),工作流工廠的主要功能是管理流程定義檔案,包括讀取定義檔案和修改定義檔案的功能。通過'resource'這個屬性指明瞭採用通過從classpath中讀取流程定義檔案的方式,按照這個定義,接下來我們需要在classpath中建立一個名為workflows.xml的檔案。 

workflows.xml 的內容: 

<workflows> 
  <workflow name="mytest" type="resource" location="myworkflow.xml"/> 
</workflows> 
我們把 myworkflow.xml 和workflows.xml放在同一目錄,這樣它就能夠被工作流工廠讀取了。 

這樣就完成了配置,接下來是初始化一個流程並呼叫它。 

Initialising OSWorkflow 

OSWorkflow 的呼叫模式相當簡單:通過一個主要的介面來執行大部分操作。這個介面就是 Workflow interface,及其擴充套件 AbstractWorkflow 的實現,例如EJBWorkflow 和 SOAPWorkflow. 為了簡單起見,我們使用最基本的一種: BasicWorkflow。 

首先,我們來建立Workflow。在實際專案中,這個物件應該被放在一個全域性的位置上以供重用,因為每次都建立一個新的Workflow物件是需要耗費比較昂貴的系統資源。在這裡的例子,我們採用BasicWorkflow,它的構建器由一個當前呼叫者的使用者名稱構成,當然我們很少看到單使用者的工作流應用,可以參考其他的Workflow實現有不同的方式去獲得當前呼叫者。 

為了簡單起見,我們採用BasicWorkflow來建立一個單一的使用者模式,避免編寫其他獲取使用者方法的麻煩。 

這樣我們來建立一個'testuser'呼叫的workflow: 

Workflow workflow = new BasicWorkflow("testuser"); 
下一步是提供配置檔案,在大多數情況下,只是簡單的傳遞一個DefaultConfiguration例項: 

DefaultConfiguration config = new DefaultConfiguration(); 
workflow.setConfiguration(config); 
現在我們已經建立並且配置好了一個workflow,接下來就是開始呼叫它了。 

啟動和進行一個工作流程 

首先我們需要呼叫initialize 方法來啟動一個工作流程,這個方法有3個引數,workflow name (定義在workflows.xml裡,通過workflow factory處理), action ID (我們要呼叫的初始化動作的ID),和初始化變數。 因為在例子裡面不需初始化變數,所以我們只是傳遞一個null, 

long workflowId = workflow.initialize("mytest", 1, null); 
我們現在已經有了一個工作流例項,返回的workflowId可以在後續的操作中來代表這個例項。這個引數會在Workflow interface的絕大部分方法中用到。 

檢驗工作流 

現在讓我們來檢驗啟動的工作流例項是否按照我們所預期的那樣執行。根據流程定義,我們期望的當前步驟是第一步,而且應該可以執行第一個動作(開始編寫草稿)。 

Collection currentSteps = workflow.getCurrentSteps 
(workflowId); 
//校驗只有一個當前步驟 
assertEquals("Unexpected number of current steps", 
1, currentSteps.size()); 
//校驗這個步驟是1 
Step currentStep = (Step)currentSteps.iterator().next(); 
assertEquals("Unexpected current step", 1, 
currentStep.getStepId()); 

int[] availableActions = 
workflow.getAvailableActions(workflowId); 
//校驗只有一個可執行的動作 
assertEquals("Unexpected number of available actions", 1, 
availableActions.length); 
//校驗這個步驟是1 
assertEquals("Unexpected available action", 1, availableActions[0]); 
執行動作 

現在已經校驗完了工作流例項正如我們所期望的到了第一個步驟,讓我們來開始執行第一個動作: 

workflow.doAction(workflowId, 1, null); 
這是簡單的呼叫第一個動作,工作流引擎根據指定的條件,改變狀態到‘Underway’,並且保持在當前步驟。 

現在我們可以類似地呼叫第2個動作,它所需要的條件已經都滿足了。 

在呼叫完第2個動作以後,根據流程定義就沒有可用的動作了,getAvailableActions將會返回一個空陣列。 

Congratulations, 你已經完成了一個工作流定義並且成功地呼叫了它。下一節我們將會講解osworkflow一些更深入的概念。 

3. Further descriptor concepts 

定義條件和函式 

你也許已經注意到,到目前為止,我們定義的條件和函式型別都是“class”。這種型別的條件和函式接受一個引數:“class.name”,以此來指明一個實現FunctionProvider或Condition介面的完整類名。 

在osworkflow裡面也有一些其他內建的型別,包括beanshell,無狀態的session bean,JNDI樹上的函式等。我們在下面的例子裡使用beanshell型別。 

Property sets 

我們可能需要在工作流的任意步驟持久化一些少量資料。在osworkflow裡,這是通過OpenSymphony的PropertySet library來實現。一個PropertySet基本上是一個可以持久化的型別安全map,你可以新增任意的資料到propertyset(一個工作流例項對應一個propertyset),並在以後的流程中再讀取這些資料。除非你特別指定操作,否則propertyset中的資料不會被清空或者被刪除。任意的函式和條件都可以和propertyset互動,以beanshell script來說,可以在指令碼上下文中用“propertyset”這個名字來獲取。下面來看具體寫法是怎麼樣的,讓我們增加如下的程式碼在“Start First Draft”動作的pre-functions裡面: 

<function type="beanshell"> 
  <arg name="script">propertySet.setString("foo", "bar")</arg> 
</function> 
這樣我們就新增了一個持久化的屬性“foo”,它的值是“bar”。這樣在以後的流程中,我們就可以獲得這個值了。 

Transient Map 臨時變數 

另外一個和propertyset變數相對的概念是臨時變數:“transientVars”。臨時變數是一個簡單的map,只是在當前的工作流呼叫的上下文內有效。它包括當前的工作流例項,工作流定義等對應值的引用。你可以通過FunctionProvider的javadoc來檢視這個map有那些可用的key。 

還記得我們在教程的第2部分傳入的那個null嗎?如果我們不傳入null的話,那麼這些輸入資料將會被新增到臨時變數的map裡。 

inputs 輸入 

每次呼叫workflow的動作時可以輸入一個可選的map,可以在這個map裡面包含供函式和條件使用的任何資料,它不會被持久化,只是一個簡單的資料傳遞。 

Validators 校驗器 

為了讓工作流能夠校驗輸入的資料,引入了校驗器的概念。一個校驗器和函式,條件的實現方式非常類似(比如,它可以是一個class,指令碼,或者EJB)。在這個教程裡面,我們將會定義一個校驗器,在“finish first draft”這個步驟,校驗使用者輸入的資料“working.title”不能超過30個字元。這個校驗器看起來是這樣的: 

package com.mycompany.validators; 

public class TitleValidator implements Validator 
{ 
  public void validate(Map transientVars, Map args, 
  PropertySet ps) 
        throws InvalidInputException, WorkflowException 
  { 
    String title = 
    (String)transientVars.get("working.title"); 
    if(title == null) 
      throw new InvalidInputException("Missing working.title"); 
    if(title.length() > 30) 
      throw new InvalidInputException("Working title too long"); 
  } 
} 
然後通過在流程定義檔案新增validators元素,就可以登記這個校驗器了: 

<validators> 
  <validator type="class"> 
    <arg name="class.name"> 
      com.mycompany.validators.TitleValidator 
    </arg> 
  </validator> 
</validators> 
這樣,當我們執行動作2的時候,這個校驗器將會被呼叫,並且檢驗我們的輸入。這樣在測試程式碼裡面,如果加上: 

Map inputs = new HashMap(); 
inputs.put("working.title", 
  "the quick brown fox jumped over the lazy dog," + 
  " thus making this a very long title"); 
workflow.doAction(workflowId, 2, inputs); 
我們將會得到一個InvalidInputException,這個動作將不會被執行。減少輸入的title字元,將會讓這個動作成功執行。 

我們已經介紹了輸入和校驗,下面來看看暫存器。 

Registers 暫存器 

暫存器是一個工作流的全域性變數。和propertyset類似,它可以在工作流例項的任意地方被獲取。和propertyset不同的是,它不是一個持久化的資料,而是每次呼叫時都需要重新計算的資料。 

它可以被用在什麼地方呢?在我們的文件管理系統裡面,如果定義了一個“document”的暫存器,那麼對於函式、條件、指令碼來說就是非常有用的:可以用它來獲得正在被編輯的文件。 

暫存器地值會被放在臨時變數(transientVars map)裡,這樣能夠在任意地方獲得它。 

定義一個暫存器和函式、條件的一個重要區別是,它並不是依靠特定的呼叫(不用關心當前的步驟,或者是輸入資料,它只是簡單地暴露一些資料而已),所以它不用臨時變數裡的值。 

暫存器必須實現Register介面,並且被定義在流程定義檔案的頭部,在初始化動作之前。 

舉例來說,我們將會使用一個osworkflow內建的暫存器:LogRegister。這個暫存器簡單的新增一個“log”變數,能夠讓你使用Jakarta的commons-logging輸出日誌資訊。它的好處是會在每條資訊前新增工作流例項的ID。 

<registers> 
  <register type="class" variable-name="log"> 
    <arg name="class.name"> 
      com.opensymphony.workflow.util.LogRegister 
    </arg> 
    <arg name="addInstanceId">true</arg> 
    <arg name="Category">workflow</arg> 
  </register> 
</registers> 
這樣我們定義了一個可用的“log”變數,可以通過其他的pre-function的指令碼里面使用它: 

<function type="beanshell"> 
  <arg name="script">transientVars.get("log").info("executing action 2") 
  </arg> 
</function> 
日誌輸出將會在前面新增工作流例項的ID 

結論 

這個教程的目的是希望可以闡明一些主要的osworkflow概念。你還可以通過API和流程定義格式去獲取更多的資訊。有一些更高階的特性沒有在此提到,比如splits 分支、joins 連線, nested conditions 複合條件、auto stpes 自動步驟等等。你可以通過閱讀手冊來獲得更進一步的理解。