許多通用業務流程都包含人類參與者。人類活動,從簡單場景(如人工批准) 到復雜場景(涉及復雜的數據輸入),在流程實現中引入了新的方面,如人類交 互模式。人類交互模式的一個典型集合包括:
四眼原則(The 4-eyes principle),通常又被稱為“職責分離”,它是決策 由多人彼此獨立作出時的一個常見場景。在很多情況下,很容易就能得到另一個 觀點/簽名。
任命(Nomination)是指上級根據團隊成員的任務安排、工作負荷或經驗人工 地將任務分配給他的情形。
任務通常被建模來表達一種預期:它們將在確定時間段內完成。如果任務沒有 按預期地進展,就需要一種上報(escalation)機制。兩種典型的上報實現是: 重新分配任務,並常常附帶一個上報已經發生的通知;或任務未按時完成的通知 (通常發給經理)。
鏈狀執行(Chained execution)是一個流程(片斷),其中的一系列步驟是 由同一個人來完成。
jBPM中的任務管理
jBPM的一個核心功能是為人類管理任務和任務列表。jBPM允許將任務和任務節 點作為整個流程設計的一部分使用。
任務一般在jBPM中定義成任務節點。單個任務節點可以包含一個或多個任務。 包含任務節點的jBPM流程的一個公共行為就是等待任務節點中的全部任務完成, 然後繼續執行。某個任務可被分配 給個人、用戶組或泳道:
假如任務被分配給某個特定用戶,那麼就只有這個使用者可以執行它。
假如任務被分配給某個用戶組,那麼這個組內的任何參與者都能執行這個任務 。jBPM使用的是參與者池(pooled actors)符號(它可以包含組名、組名列表和 參與者個人列表等),而不是組ID。如果用戶開始執行在他們組任務列表中的任 務,最終可能會引起沖突——可能有多人開始執行相同的任務。為了避免這種情 況,在開始執行任務之前,用戶應該將任務從組任務列表移動到他們自己的任務 列表中。
泳道代表一個流程角色,它通常被分配給一個用戶組。它是一種指定流程中的 多個任務要由同一參與者完成的機制。因此,在第一個任務被分配給某個泳道之 後,流程就會記住所有在相同泳道內的後續任務都將由同一參與者完成。
jBPM提供了兩種定義任務分配的基本方法:作為流程定義的一部分或通過編程 實現。如果是作為流程定義的一部分,分配可以通過指定具體用戶、用戶組或泳 道 完成。此外,可以使用表達式根據流程變量動態確定某個具體用戶。完整的編 程實現是基於分配處理器(assignment handler)的,它允許任務根據任意的計 算規則去查找用戶ID。
流程定義描述流程實例的方式類似任務描述任務實例的方式。當流程執行時, 一個流程實例——流程的運行時表示——就會被創建。類似,一個任務實例—— 任務的運行時表示——就會被創建。根據任務定義,任務實例被分配給一個參與 者/參與者組。
任務實例的一個作用就是支持用戶交互——把數據顯示給用戶並從用戶那裡收 集數據。一個jBPM任務實例擁有訪問流程(令牌)變量的全部權限,而且還可以 有自己的變量。任務能夠擁有自己的變量對於以下場景非常有用:
在任務實例中創建流程變量的副本,這樣對任務實例變量的即時更新只有在該 任務完成且這些副本被提交給流程變量時才會影響流程變量。
創建更好支持用戶活動的“派生(計算)”變量。
任務自己的變量在jBPM中是通過任務控制器處理器(task controller handler)支持的,它可以在任務實例創建時生成任務實例數據(從流程數據), 並在任務實例完成時將任務實例數據提交給流程變量。
實現四眼原則
我們上面已經說過,實現四眼原則意味著要允許多人同時干一個活。它的實現 有以下幾種可能方法:
在任務外解決:需要大量時間的任務並行循環(parallel looping) 。
使用動作處理器(Action handler):附加到任務節點的進入事件(enter event),基於流程實例變量創建多個節點實例。
在任務內解決:引入“任務接受量(task take)”(類似jPDL 4)並允許某 個任務實例可被接受多次。
根據jBPM最佳實踐 ——“擴展jBPM API而不是去搞復雜的流程建模” ,我決 定采用任務內解決的方法。這就要求修改jBPM提供的任務和任務實例類。
擴展Task類
jBPM任務的定義被包含在org.jbpm.taskmgmt.def.Task類中。為了支持四眼原 則,我們需要給類增加以下的字段/方法(清單1):
protected int numSignatures = 1;
public int getNumSignatures(){
return numSignatures;
}
public void setNumSignatures(int numSignatures){
this.numSignatures = numSignatures;
}
清單1 給Task類增加字段和方法
這個新的參數允許指定任務完成所需的任務處理人數量。缺省值為1,這意味 著,只有1個用戶應該/可以處理這個任務。
jBPM使用Hibernate來向數據庫保存和讀取數據。為了讓我們新加的變量持久 化,我們需要更新Task類的Hibernate配置文件(Task.hbm.xml),它在 org.jbpm.taskmgmt.def文件夾中,增加代碼如下(清單2)
<property name="numSignatures" column="NUMSIGNATURES_" />
清單2 在Task映射文件中指定新增域
為了讓我們新加的屬性能被流程定義和數據庫正確讀取,我們需要修改 org.jbpm.jpdl.xml.JpdlXmlReader類以正確地讀取我們的新屬性(清單3)
String numSignatureText = taskElement.attributeValue ("numSignatures");
if (numSignatureText != null) {
try{
task.setNumSignatures(Integer.parseInt(numSignatureText));
}
catch(Exception e){}
}
清單3 讀取numSignature屬性
最後,因為JpdlXmlReader根據模式來驗證XML,因此我們需要在jpdl-3.2.xsd 中增加一個屬性定義(清單4):
<xs:element name="task">
………………….
<xs:attribute name="numSignatures" type="xs:string" />
清單4 在jpdl-3.2.xsd中增加numSignatures屬性
當完成這些工作,任務定義就被擴展可以使用numSignatures屬性(清單5):
<task name="task2" numSignatures = "2">
<assignment pooled-actors="Peter, John"></assignment>
</task>
清單5 給任務定義增加numSignatures屬性
擴展TaskInstance類
在擴展完任務類後,我們還需要創建一個自定義的任務實例類來跟蹤分配給該 任務實例的參與者,並確保所有被分配的參與者完成類執行(清單6)。
package com.navteq.jbpm.extensions;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import org.jbpm.JbpmException;
import org.jbpm.taskmgmt.exe.TaskInstance;
public class AssignableTaskInstance extends TaskInstance {
private static final long serialVersionUID = 1L;
private List<Assignee> assignees = new LinkedList<Assignee>();
private String getAssigneeIDs(){
StringBuffer sb = new StringBuffer();
boolean first = true;
for(Assignee a : assignees){
if(!first)
sb.append(" ");
else
first = false;
sb.append(a.getUserID());
}
return sb.toString();
}
public List<Assignee> getAssignees() {
return assignees;
}
public void reserve(String userID) throws JbpmException {
if(task == null)
throw new JbpmException("can't reserve instance with no task");
// Duplicate assignment is ok
for(Assignee a : assignees){
if(userID.equals(a.getUserID()))
return;
}
// Can we add one more guy?
if(task.getNumSignatures() > assignees.size()){
assignees.add(new Assignee(userID));
return;
}
throw new JbpmException("task is already reserved by " +
getAssigneeIDs());
}
public void unreserve(String userID){
for(Assignee a : assignees){
if(userID.equals(a.getUserID())){
assignees.remove(a);
return;
}
}
}
private void completeTask(Assignee assignee, String transition){
assignee.setEndDate(new Date());
// Calculate completed assignments
int completed = 0;
for(Assignee a : assignees){
if(a.getEndDate() != null)
completed ++;
}
if(completed < task.getNumSignatures())
return;
if(transition == null)
end();
else
end(transition);
}
public void complete(String userID, String transition) throws JbpmException{
if(task == null)
throw new JbpmException("can't complete instance with no task");
// make sure it was reserved
for(Assignee a : assignees){
if(userID.equals(a.getUserID())){
completeTask(a, transition);
return;
}
}
throw new JbpmException("task was not reserved by " + userID);
}
public boolean isCompleted(){
return (end != null);
}
}
清單6 擴展TaskInstance類
這個實現擴展了jBPM提供的TaskInstance類,並跟蹤完成該實例所需的參與者 個數。它引入了幾個新方法,允許參與者預留(reserve)/退還(unreserve)任 務實例,以及讓指定參與者完成任務執行。
清單6的實現依賴一個支持類Assignee(清單7)
package com.navteq.jbpm.extensions;
import java.io.Serializable;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Assignee implements Serializable{
private static final long serialVersionUID = 1L;
private static final DateFormat dateFormat = new
SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
long id = 0;
protected String startDate = null;
protected String userID = null;
protected String endDate = null;
public Assignee(){}
public Assignee(String uID){
userID = uID;
startDate = dateFormat.format(new Date());
}
////////////Setters and Getters ///////////////////
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getStartDate() {
return startDate;
}
public void setStartDate(String startDate) {
this.startDate = startDate;
}
public String getUserID() {
return userID;
}
public void setUserID(String id) {
userID = id;
}
public String getEndDate() {
return endDate;
}
public void setEndDate(String endDate) {
this.endDate = endDate;
}
public void setEndDate(Date endDate) {
this.endDate = dateFormat.format(endDate);
}
public void setEndDate() {
this.endDate = dateFormat.format(new Date());
}
public String toString(){
StringBuffer bf = new StringBuffer();
bf.append(" Assigned to ");
bf.append(userID);
bf.append(" at ");
bf.append(startDate);
bf.append(" completed at ");
bf.append(endDate);
return bf.toString();
}
}
清單7 Assignee類
自定義的TaskInstance類和Assignee類都必須保存到數據庫中。這意味著需要 給這兩個類實現Hibernate映射 (清單8,9):
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping auto-import="false" default- access="field">
<subclass namename="com.navteq.jbpm.extensions.AssignableTaskInstance"
extends="org.jbpm.taskmgmt.exe.TaskInstance"
discriminator-value="A">
<list name="assignees" cascade="all" >
<key column="TASKINSTANCE_" />
<index column="TASKINSTANCEINDEX_"/>
<one-to-many class="com.navteq.jbpm.extensions.Assignee" />
</list>
</subclass>
</hibernate-mapping>
清單8 自定義任務實例的Hibernate映射文件
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping auto-import="false" default- access="field">
<class name="com.navteq.jbpm.extensions.Assignee"
table="JBPM_ASSIGNEE">
<cache usage="nonstrict-read-write"/>
<id name="id" column="ID_"><generator class="native" /></id>
<!-- Content -->
<property name="startDate" column="STARTDATE_" />
<property name="userID" column="USERID_" />
<property name="endDate" column="ENDDATE_" />
</class>
</hibernate-mapping>
清單9 Assignee類的Hibernate映射文件
要讓jBPM能夠使用我們的自定義任務實例實現,我們還需要提供一個自定義的 任務實例工廠(清單10)。
package com.navteq.jbpm.extensions;
import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.taskmgmt.TaskInstanceFactory;
import org.jbpm.taskmgmt.exe.TaskInstance;
public class AssignableTaskInstanceFactory implements TaskInstanceFactory {
private static final long serialVersionUID = 1L;
@Override
public TaskInstance createTaskInstance(ExecutionContext executionContext) {
return new AssignableTaskInstance();
}
}
清單10 自定義的任務實例工廠
最後,為了讓jBPM運行時使用正確的任務實例工廠(清單10),還必須創建一 個新的jBPM配置(清單11)。
<jbpm-configuration>
<bean name="jbpm.task.instance.factory"
class="com.navteq.jbpm.extensions.AssignableTaskInstanceFactory" singleton="true"
/>
</jbpm-configuration>
清單11 jBPM配置
完成所有這些變更之後(清單1-11),一個典型的任務處理顯示如下:
List<String> actorIds = new LinkedList<String>();
actorIds.add("Peter");
List<TaskInstance> cTasks = jbpmContext.getGroupTaskList (actorIds)
TaskInstance cTask = cTasks.get(0);
AssignableTaskInstance aTask = (AssignableTaskInstance)cTask;
try{
aTask.reserve("Peter");
// Save
jbpmContext.close();
}
catch(Exception e){
System.out.println("Task " + cTask.getName() + " is already reserved");
e.printStackTrace();
}
清單12 處理可分配任務實例
這裡,在得到某個用戶的任務實例並將其轉變成可分配任務實例之後,我們將 試著預留它。一旦預留成功,我們將關閉jBPM運行時以提交事務。
實現任命
JBoss jBPM可以非常輕易的實現手動將任務分配給特定用戶。根據jBPM提供的 簡單API,可以完成將任務實例從一個任務列表移動到另一個任務列表,因此給某 個用戶分配任務相當直接(清單13)
List<String> actorIds = new LinkedList<String>();
actorIds.add("admins");
String actorID = "admin";
List<TaskInstance> cTasks = jbpmContext.getGroupTaskList (actorIds);
TaskInstance cTask = cTasks.get(0);
cTask.setPooledActors((Set)null);
cTask.setActorId(actorID);
清單13 將任務重新分配給指定用戶
jBPM提供了2類不同的API來設置參與者池:一類接收字符串id數組,另一類則 接收id集合。如果要清空一個池,就要使用那個接收集合的API(傳入一個null集 合)。
實現上報
前面已經說過,上報一般被實現為任務的重新分配,並常常附帶一個上報已發 生的通知;或是實現成一個任務未及時完成的通知。
實現為重新分配的上報
盡管jBPM不直接支持上報,但它提供了2個基本的機制:超時和重新分配(參 見上節)。粗一看,實現上報只需將這二者結合即可,但是仔細一想還是存在一 些困難:
jBPM實現中的關系並不總是雙向的。如,從一個任務節點我們可以找到所有這 個節點定義的任務,但是從一個任務,並沒有API可以完成找到包含它的任務節點 的工作;由某個任務實例,你可以得到一個任務,但是沒有由某個任務得到所有 實例的API,諸如此類。
超時不是發生在任務自身,而是發生在任務節點上。由於某個節點可以關聯多 個任務,並且jBPM關系實現並不是雙向的(見上),因此要跟蹤當前任務實例就 需要其他的支持手段。
以重新分配實現的上報的整個實現涉及3個處理器:
負責給任務分配參與者的分配處理器。這個處理器跟蹤它是一個首次任務調用 還是一個上報任務調用。清單14給出了一個分配處理器的例子。
package com.sample.action;
import org.jbpm.graph.def.Node;
import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.taskmgmt.def.AssignmentHandler;
import org.jbpm.taskmgmt.exe.Assignable;
public class EscalationAssignmentHandler implements AssignmentHandler {
private static final long serialVersionUID = 1L;
@Override
public void assign(Assignable assignable, ExecutionContext context)
throws Exception {
Node task = context.getToken().getNode();
if(task != null){
String tName = task.getName();
String vName = tName + "escLevel";
Long escLevel = (Long)context.getVariable(vName);
if(escLevel == null){
// First time through
assignable.setActorId("admin");
}
else{
// Escalate
assignable.setActorId("bob");
}
}
}
}
清單14 分配處理器示例
這裡我們嘗試得到一個包含了給定任務上報次數的流程變量。如果變量未定義 ,則就分配“admin”為任務擁有者,否則任務就被分配給“bob”。在這個處理 器中可以使用任何其他的分配策略。
任務實例創建動作處理器(清單15),它保存流程實例上下文的任務實例 id
package com.sample.action;
import org.jbpm.graph.def.ActionHandler;
import org.jbpm.graph.def.Node;
import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.taskmgmt.exe.TaskInstance;
public class TaskCreationActionHandler implements ActionHandler {
private static final long serialVersionUID = 1L;
@Override
public void execute(ExecutionContext context) throws Exception {
Node task = context.getToken().getNode();
TaskInstance current = context.getTaskInstance();
if((task == null) || (current == null))
return;
String tName = task.getName();
String iName = tName + "instance";
context.setVariable(iName, new Long(current.getId()));
}
}
清單15 任務實例創建處理器
任務節點計時器觸發調用的超時處理器(清單16)。
package com.sample.action;
import org.jbpm.graph.def.ActionHandler;
import org.jbpm.graph.def.GraphElement;
import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.taskmgmt.exe.TaskInstance;
public class EscalationActionHandler implements ActionHandler {
private static final long serialVersionUID = 1L;
private String escalation;
@Override
public void execute(ExecutionContext context) throws Exception {
GraphElement task = context.getTimer().getGraphElement ();
if(task == null)
return;
String tName = task.getName();
String vName = tName + "escLevel";
long escLevel = (long)context.getVariable(vName);
if(escLevel == null)
escLevel = new long(1);
else
escLevel += 1;
context.setVariable(vName, escLevel);
String iName = tName + "instance";
long taskInstanceId = (long)context.getVariable (iName);
TaskInstance current =
context.getJbpmContext().getTaskInstance(taskInstanceId);
if(current != null){
current.end(escalation);
}
}
}
清單16 超時處理器
這個處理器首先記錄上報計數器,接著完成此節點關聯的任務實例。任務實例 的完成伴隨有一個變遷(一般是回到任務節點)。
使用以上描述的處理器實現上報的簡單流程例子顯示在清單17中。
<?xml version="1.0" encoding="UTF-8"?>
<process-definition
xmlns="urn:jbpm.org:jpdl-3.2"
name="escalationHumanTaskTest">
<start-state name="start">
<transition to="customTask"></transition>
</start-state>
<task-node name="customTask">
<task name="task2">
<assignment class="com.sample.action.EscalationAssignmentHandler"><
/assignment>
</task>
<event type="task-create">
<action name="Instance Tracking" class="com.sample.action.TaskCreationActionHandler"></action>< BR> </event>
<timer duedate="10 second" name="Escalation timeout">
<action class="com.sample.action.EscalationActionHandler">
<escalation>
escalation
</escalation>
</action>
</timer>
<transition to="end" name="to end"></transition>
<transition to="customTask" name="escalation"></transition>
</task-node>
<end-state name="end"></end-state>
</process-definition>
清單17 簡單流程的上報
實現成通知的上報
jBPM為郵件傳遞提供了強大支持,這使得實現成通知的上報變得極其簡單。郵 件傳遞可由給節點附加定時器,然後觸發,它使用已經寫好的郵件動作來完成通 知傳遞。
實現鏈狀執行
鏈狀執行直接由jBPM泳道支持,並不需要額外的開發。
總結
不管我們在自動化方面投入多少努力,面對復雜的業務流程,總免不了要有人 工介入的可能。在這篇文章中,我給出了一系列已建立的高級人工交互模式,並 展示了用jBPM完成它是多麼輕而易舉。