引用:http://www.duduwolf.com/cmd.asp?act=tb&id=212
目的
這篇指導資料的目的是介紹OSWorkflow的所有概念,指導你如何使用它,并且保證你逐步理解OSWorkflow的關鍵內容。
本指導資料假定你已經部署OSWorkflow的范例應用在你的container上。范例應用部署是使用基于內存的數據存儲,這樣你不需要擔心如何配置其他持久化的例子。范例應用的目的是為了說明如何應用OSWorkflow,一旦你精通了OSWorkflow的流程定義描述符概念和要素,應該能通過閱讀這些流程定義文件而了解實際的流程。
本指導資料目前有3部分:
1. 你的第一個工作流
2. 測試你的工作流
3. 更多的流程定義描述符概念
創建描述符
首先,讓我們來定義工作流。你可以使用任何名字來命名工作流。一個工作流對應一個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());
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);
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)
????? thrownew InvalidInputException("Missing working.title");
??? if(title.length() > 30)
????? thrownew 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 自動步驟等等。你可以通過閱讀手冊來獲得更進一步的理解。
Lyyb2001