第3章 指南
這個指南將向你展示如何用jpdl創建基本的流程以及如何使用API管理運行期的執行。
這個指南的形式是解釋一組示例,每個示例集中於一個特殊的主題,並且包含大量的註釋,這些例子也可以在jBPM下載包的目錄src/java.examples中找到。
最好的學習方法就是建立一個工程,並且通過在給定例子上做不同的變化進行實驗。
對eclipse用戶來說可以如下方式開始:下載jbpm-3.0-[version].zip並且解壓到自己的系統,然後執行菜單“File”-->“Import…”-->“Existing Project into Workspace”,然後點擊“Next”,瀏覽找到jBPM根目錄,點擊“Finish”。現在,在你的工作區中就有了一個jbpm.3工程,你可以在src/java.examples/…下找到本指南中的例子,當你打開這些例子時,你可以使用菜單“Run”-->“Run As…”-->“JUnit Test”運行它們。
jBPM包含一個用來創作例子中展示的XML的圖形化設計器工具,你可以在“2.1 下載概述”中找到這個工具的下載說明,但是完成本指南不需要圖形化設計器工具。
3.1 Hello World 示例
一個流程定義就是一個有向圖,它由節點和轉換組成。Hello world流程有三個節點,下面來看一下它們是怎樣組裝在一起的,我們以一個簡單的流程作爲開始,不用使用設計器工具,下圖展示了hello world流程的圖形化表示:
圖 3.1 hello world流程圖
public void testHelloWorldProcess() {
// 這個方法展示了一個流程定義以及流程定義的執行。
// 這個流程定義有3個節點:一個沒有命名的開始狀態,
// 一個狀態“s”,和一個名稱爲“end”的結束狀態。
// 下面這行是解析一段xml文本到ProcessDefinition對象(流程定義)。
// ProcessDefinition把一個流程的規格化描述表現爲java對象。
ProcessDefinition processDefinition = ProcessDefinition.parseXmlString(
"<process-definition>" +
" <start-state>" +
" <transition to='s' />" +
" </start-state>" +
" <state name='s'>" +
" <transition to='end' />" +
" </state>" +
" <end-state name='end' />" +
"</process-definition>"
);
// 下面這行是創建一個流程定義的執行。創建後,流程執行有一個
// 主執行路徑(=根令牌),它定位在開始狀態。
ProcessInstance processInstance =
new ProcessInstance(processDefinition);
// 創建後,流程執行有一個主執行路徑(=根令牌)。
Token token = processInstance.getRootToken();
// 創建後,主執行路徑被定位在流程定義的開始狀態。
assertSame(processDefinition.getStartState(), token.getNode());
// 讓我們開始流程執行,通過它的默認轉換離開開始狀態。
token.signal();
// signal方法將會把流程阻塞在一個等待狀態。
// 流程執行進入第一個等待狀態“s”,因此主執行路徑現在定位
// 在狀態“s”。
assertSame(processDefinition.getNode("s"), token.getNode());
// 讓我們發送另外一個信號,這將通過使用狀態“s”的默認轉換
// 離開狀態“s”,恢復流程執行。
token.signal();
// 現在signal方法將返回,因爲流程示例已經到達結束狀態。
assertSame(processDefinition.getNode("end"), token.getNode());
}
3.2 數據庫示例
jBPM的特性之一就是在流程等待狀態時,擁有把流程的執行持久化到數據庫中的能力。下面的例子將向你展示怎樣存儲一個流程實例到數據庫,例子中還會出現上下文。分開的方法被用來創建不同的用戶代碼,例如,一段代碼在web應用中啓動一個流程並且持久化執行到數據庫,稍後,由一個消息驅動bean從數據庫中加載流程實例並且恢復它的執行。
有關jBPM持久化的更多信息可以在“第7章 持久化”找到。
public class HelloWorldDbTest extends TestCase {
static JbpmConfiguration jbpmConfiguration = null;
static {
// 在“src/config.files”可以找到象下面這樣的一個示例配置文件。
// 典型情況下,配置信息在資源文件“jbpm.cfg.xml”中,但是在這裏
// 我們通過XML字符串傳入配置信息。
// 首先我們創建一個靜態的JbpmConfiguration。一個JbpmConfiguration
// 可以被系統中所有線程所使用,這也是爲什麼我們可以把它安全的設置
// 爲靜態的原因。
jbpmConfiguration = JbpmConfiguration.parseXmlString(
"<jbpm-configuration>" +
//jbpm-context機制分離了jbpm核心引擎和來自於外部環境的服務。
" <jbpm-context>" +
" <service name='persistence' " +
" factory='org.jbpm.persistence.db.DbPersistenceServiceFactory' />" +
" </jbpm-context>" +
// 同樣,jbpm使用的所有資源文件在jbpm.cfg.xml中被提供。
" <string name='resource.hibernate.cfg.xml' " +
" value='hibernate.cfg.xml' />" +
" <string name='resource.business.calendar' " +
" value='org/jbpm/calendar/jbpm.business.calendar.properties' />" +
" <string name='resource.default.modules' " +
" value='org/jbpm/graph/def/jbpm.default.modules.properties' />" +
" <string name='resource.converter' " +
" value='org/jbpm/db/hibernate/jbpm.converter.properties' />" +
" <string name='resource.action.types' " +
" value='org/jbpm/graph/action/action.types.xml' />" +
" <string name='resource.node.types' " +
" value='org/jbpm/graph/node/node.types.xml' />" +
" <string name='resource.varmapping' " +
" value='org/jbpm/context/exe/jbpm.varmapping.xml' />" +
"</jbpm-configuration>"
);
}
public void setUp() {
jbpmConfiguration.createSchema();
}
public void tearDown() {
jbpmConfiguration.dropSchema();
}
public void testSimplePersistence() {
// 在下面調用的3個方法之間,所有的數據通過數據庫被傳遞。
// 在這個測試中,這3個方法被依次執行,因爲我們想要測試一個
// 完整的流程情景。但是實際上,這些方法表示了對服務器的不同
// 請求。
// 因爲我們以一個乾淨的空數據庫開始,所以我們首先必須部署流程。
// 事實上,這只需要由流程開發者做一次。
deployProcessDefinition();
// 假設在一個web應用中當用戶提交一個表單時我們起動一個流程
// 實例(=流程執行)…
processInstanceIsCreatedWhenUserSubmitsWebappForm();
// 然後,一個異步消息到達時繼續執行。
theProcessInstanceContinuesWhenAnAsyncMessageIsReceived();
}
public void deployProcessDefinition() {
// 這個測試展示了一個流程定義以及流程定義的執行。
// 這個流程定義有3個節點:一個沒有命名的開始狀態,
// 一個狀態“s”,和一個名稱爲“end”的結束狀態。
ProcessDefinition processDefinition = ProcessDefinition.parseXmlString(
"<process-definition name='hello world'>" +
" <start-state name='start'>" +
" <transition to='s' />" +
" </start-state>" +
" <state name='s'>" +
" <transition to='end' />" +
" </state>" +
" <end-state name='end' />" +
"</process-definition>"
);
// 查找在上面所配置的pojo持久化上下文創建器。
JbpmContext jbpmContext = jbpmConfiguration.createJbpmContext();
try {
// 部署流程定義到數據庫中。
jbpmContext.deployProcessDefinition(processDefinition);
} finally {
// 關閉pojo持久化上下文。這包含激發(flush)SQL語句把流程
// 定義插入到數據庫。
jbpmContext.close();
}
}
public void processInstanceIsCreatedWhenUserSubmitsWebappForm() {
// 本方法中的代碼可以被放在struts的actiong中,或JSF管理
//的bean中。
//查找在上面所配置的pojo持久化上下文創建器。
JbpmContext jbpmContext = jbpmConfiguration.createJbpmContext();
try {
GraphSession graphSession = jbpmContext.getGraphSession();
ProcessDefinition processDefinition =
graphSession.findLatestProcessDefinition("hello world");
// 使用從數據庫中獲取的流程定義可以創建一個流程定義的執行
// 就象在hello world例子中那樣(該例沒有持久化)。
ProcessInstance processInstance =
new ProcessInstance(processDefinition);
Token token = processInstance.getRootToken();
assertEquals("start", token.getNode().getName());
// 讓我們起動流程執行
token.signal();
// 現在流程在狀態 's'。
assertEquals("s", token.getNode().getName());
// 現在流程實例processInstance被存儲到數據庫,
// 因此流程執行的當前狀態也被存儲到數據庫。
jbpmContext.save(processInstance);
// 以後我們可以從數據庫再取回流程實例,並且通過提供另外一個
// 信號來恢復流程執行。
} finally {
// 關閉pojo持久化上下文。
jbpmContext.close();
}
}
public void theProcessInstanceContinuesWhenAnAsyncMessageIsReceived() {
// 本方法中的代碼可以作爲消息驅動bean的內容。
// 查找在上面所配置的pojo持久化上下文創建器。
JbpmContext jbpmContext = jbpmConfiguration.createJbpmContext();
try {
GraphSession graphSession = jbpmContext.getGraphSession();
// 首先,我們需要從數據庫中取回流程實例。
// 有幾個可選方法來分辨出我們在這裏所要處理的流程實例。
// 在這個簡單的測試中,最容易的方式是查找整個流程實例列表,
// 這裏它應該只會給我們一個結果。
// 首先,讓我們查找流程定義。
ProcessDefinition processDefinition =
graphSession.findLatestProcessDefinition("hello world");
// 現在我們搜索這個流程定義的所有流程實例。
List processInstances =
graphSession.findProcessInstances(processDefinition.getId());
// 因爲我們知道在這個單元測試中只有一個執行。
// 在實際情況中, 可以從所到達的信息內容中提取processInstanceId
// 或者由用戶來做選擇。
ProcessInstance processInstance =
(ProcessInstance) processInstances.get(0);
// 現在我們可以繼續執行。注意:processInstance 將委託信號
// 到主執行路徑(=根令牌)。
processInstance.signal();
// 在這個信號之後,我們知道流程執行應該到達了結束狀態。
assertTrue(processInstance.hasEnded());
// 現在我們可以更新數據庫中的執行狀態。
jbpmContext.save(processInstance);
} finally {
// 關閉pojo持久化上下文。
jbpmContext.close();
}
}
}
3.3 上下文示例:流程變量
流程變量包含了流程執行期間的上下文信息,流程變量與一個java.util.Map相似,它影射變量名稱和值,值是java對象,流程變量作爲流程實例的一部分被持久化。爲了讓事情簡單,在這裏的例子中我們只是展示使用變量的API,而沒有持久化。
有關變量的更多信息可以在“第10章 上下文”中找到。
// 這個例子仍然從hello world流程開始,甚至沒有修改。
ProcessDefinition processDefinition = ProcessDefinition.parseXmlString(
"<process-definition>" +
" <start-state>" +
" <transition to='s' />" +
" </start-state>" +
" <state name='s'>" +
" <transition to='end' />" +
" </state>" +
" <end-state name='end' />" +
"</process-definition>"
);
ProcessInstance processInstance =
new ProcessInstance(processDefinition);
// 從流程實例獲取上下文實例,用來使用流程變量。
ContextInstance contextInstance =
processInstance.getContextInstance();
// 在流程離開開始狀態之前,我們要在流程實例的上下文中
// 設置一些流程變量。
contextInstance.setVariable("amount", new Integer(500));
contextInstance.setVariable("reason", "i met my deadline");
// 從現在開始,這些流程變量與流程實例相關聯。現在展示由用戶代碼通過
// API訪問流程變量,另外,這些代碼也可以存在於動作或節點的實現中。
// 流程變量被作爲流程實例的一部分也被存儲到數據庫中。
processInstance.signal();
// 通過contextInstance訪問流程變量。
assertEquals(new Integer(500),
contextInstance.getVariable("amount"));
assertEquals("i met my deadline",
contextInstance.getVariable("reason"));
3.4 任務分配示例
下一個例子我們將向你展示怎樣分配一個任務到用戶。因爲jBPM工作流引擎與組織模型是獨立的,所以任何一種用來計算參與者的表達式語言都是有限制的,因此,你不得不指定一個AssignmentHandler實現,用來包含任務參與者的計算。
public void testTaskAssignment() {
// 下面的流程基於hello world 流程。狀態節點被一個task-node節點
// 所替換。task-node JPDL中的一類節點,它表示一個等待狀態並且產生
// 將要完成的任務,這些任務在流程繼續之前被執行。
ProcessDefinition processDefinition = ProcessDefinition.parseXmlString(
"<process-definition name='the baby process'>" +
" <start-state>" +
" <transition name='baby cries' to='t' />" +
" </start-state>" +
" <task-node name='t'>" +
" <task name='change nappy'>" +
" <assignment class='org.jbpm.tutorial.taskmgmt.NappyAssignmentHandler' />" +
" </task>" +
" <transition to='end' />" +
" </task-node>" +
" <end-state name='end' />" +
"</process-definition>"
);
// 創建一個流程定義的執行。
ProcessInstance processInstance =
new ProcessInstance(processDefinition);
Token token = processInstance.getRootToken();
// 讓我們起動流程執行,通過默認轉換離開開始狀態
token.signal();
// signal方法將會把流程阻塞在一個等待狀態,
// 在這裏,就是阻塞在task-node。
assertSame(processDefinition.getNode("t"), token.getNode());
// 當執行到達task-node,一個'change nappy'任務被創建,並且
// NappyAssignmentHandler 被調用,來決定任務將要分配給誰。
// NappyAssignmentHandler 返回'papa'。
// 在一個實際環境中,將使用org.jbpm.db.TaskMgmtSession中的方法
// 從數據庫獲取任務。因爲我們不想在這個例子中包含複雜的持久化,
// 所以我們僅使用這個流程實例的第一個任務實例(我們知道,在這個
// 測試情景,只有一個任務)。
TaskInstance taskInstance = (TaskInstance)
processInstance
.getTaskMgmtInstance()
.getTaskInstances()
.iterator().next();
// 現在我們檢查taskInstance 是否真正的分配給了'papa'。
assertEquals("papa", taskInstance.getActorId() );
// 現在我們假設'papa'已經完成了他的職責,並且標示任務爲已完成。
taskInstance.end();
// 因爲這是要做的最後一個任務(也是唯一一個),所以任務的完成
// 會觸發流程實例的繼續執行。
assertSame(processDefinition.getNode("end"), token.getNode());
}
3.5 定製動作示例
動作是一種綁定你自己的定製代碼到jBPM流程的機制。動作可以與它自己的節點(如果它們與流程的圖形化表示是相關的)相關聯。動作也可以被放置在事件上,如執行轉換、離開節點或者進入節點;如果那樣的話,動作則不是圖形化表示的一部分,但是在流程執行運行時,當執行觸發事件時,它們會被執行。
我們先看一下將要在我們的例子中使用的動作實現:MyActionHandler,這個動作處理實現實際上沒有做任何事…僅僅是設置布爾變量isExecuted爲true。變量isExecuted是一個靜態變量,因此它可以在動作處理的內部訪問(即內部方法中),也可以從動作(即在動作類上)驗證它的值。
有關動作的更多信息可以在“9.5 動作”中找到。
// MyActionHandler 是一個在jBPM流程執行期間可以執行用戶代碼的類。
public class MyActionHandler implements ActionHandler {
// 在每個測試之前(在 setUp方法中), isExecuted 成員被設置爲false。
public static boolean isExecuted = false;
// 動作將設置isExecuted爲true,當動作被執行之後,單元測試會
// 展示。
public void execute(ExecutionContext executionContext) {
isExecuted = true;
}
}
就象前面所提示那樣,在每個測試之前,我們將設置靜態域MyActionHandler.isExecuted爲false。
// 每個測試都將以設置MyActionHandler的靜態成員isExecuted
// 爲false開始。
public void setUp() {
MyActionHandler.isExecuted = false;
}
我們將會在轉換上開始一個動作。
public void testTransitionAction() {
// 下面的流程與hello world 流程不同。我們在從狀態“s”到
// 結束狀態的轉換上增加了一個動作。這個測試的目的就是展示
// 集成java代碼到一個jBPM流程是多麼的容易。
ProcessDefinition processDefinition = ProcessDefinition.parseXmlString(
"<process-definition>" +
" <start-state>" +
" <transition to='s' />" +
" </start-state>" +
" <state name='s'>" +
" <transition to='end'>" +
" <action class='org.jbpm.tutorial.action.MyActionHandler' />" +
" </transition>" +
" </state>" +
" <end-state name='end' />" +
"</process-definition>"
);
// 讓我們爲流程定義起動一個新的執行。
ProcessInstance processInstance =
new ProcessInstance(processDefinition);
// 下面的信號會導致執行離開開始狀態,進入狀態“s”。
processInstance.signal();
// 這裏我們展示MyActionHandler還沒有被執行。
assertFalse(MyActionHandler.isExecuted);
// ... 並且執行的主路徑被定位在狀態“s”。
assertSame(processDefinition.getNode("s"),
processInstance.getRootToken().getNode());
// 下面的信號將觸發根令牌的執行,令牌將會執行帶有動作的轉換,
// 並且在調用signal方法期間動作經會被執行token。
processInstance.signal();
// 我們可以看到MyActionHandler在調用signal方法期間被執行了。
assertTrue(MyActionHandler.isExecuted);
}
下一個例子展示了相同的動作,但是現在動作分別被放置在了enter-node和leave-node事件上。注意,節點與轉換相比有更多的事件類型,而轉換隻有一個,因此動作要放置在節點上應該放入一個event元素中。
ProcessDefinition processDefinition = ProcessDefinition.parseXmlString(
"<process-definition>" +
" <start-state>" +
" <transition to='s' />" +
" </start-state>" +
" <state name='s'>" +
" <event type='node-enter'>" +
" <action class='org.jbpm.tutorial.action.MyActionHandler' />" +
" </event>" +
" <event type='node-leave'>" +
" <action class='org.jbpm.tutorial.action.MyActionHandler' />" +
" </event>" +
" <transition to='end'/>" +
" </state>" +
" <end-state name='end' />" +
"</process-definition>"
);
ProcessInstance processInstance =
new ProcessInstance(processDefinition);
assertFalse(MyActionHandler.isExecuted);
//下面的信號會導致執行離開開始狀態,進入狀態“s”,
// 因此狀態 's' 被進入,動作被執行。
processInstance.signal();
assertTrue(MyActionHandler.isExecuted);
// 我們重新設置MyActionHandler.isExecuted。
MyActionHandler.isExecuted = false;
// 下一個信號將會觸發執行離開狀態's',因此動作將被執行。
processInstance.signal();
// 請看
assertTrue(MyActionHandler.isExecuted);