jBPM實現(xiàn)高級交互模式詳解
JBPM是目前應(yīng)用廣泛的Java工作流管理系統(tǒng),在51CTO之前報道的J2EE工作流管理系統(tǒng)JBPM詳解中,我們曾詳細介紹過JBPM的工作原理和工作流應(yīng)用方案。今天通過對JBPM中的四眼原則、任命和上報的實現(xiàn)來詳細講解如何使用JBPM實現(xiàn)高級交互模式。
#t#
許多通用業(yè)務(wù)流程都包含人類參與者。人類活動,從簡單場景(如人工批準)到復雜場景(涉及復雜的數(shù)據(jù)輸入),在流程實現(xiàn)中引入了新的方面,如人類交互模式。人類交互模式的一個典型集合包括:
1、四眼原則(The 4-eyes principle),通常又被稱為“職責分離”,它是決策由多人彼此獨立作出時的一個常見場景。在很多情況下,很容易就能得到另一個觀點/簽名。
2、任命(Nomination)是指上級根據(jù)團隊成員的任務(wù)安排、工作負荷或經(jīng)驗人工地將任務(wù)分配給他的情形。
3、任務(wù)通常被建模來表達一種預期:它們將在確定時間段內(nèi)完成。如果任務(wù)沒有按預期地進展,就需要一種上報(escalation)機制。兩種典型的上報實現(xiàn)是:重新分配任務(wù),并常常附帶一個上報已經(jīng)發(fā)生的通知;或任務(wù)未按時完成的通知(通常發(fā)給經(jīng)理)。
4、鏈狀執(zhí)行(Chained execution)是一個流程(片斷),其中的一系列步驟是由同一個人來完成。
在本文中,我將討論如何實現(xiàn)JBPM高級交互模式。
JBPM中的任務(wù)管理
JBPM的一個核心功能2是為人類管理任務(wù)和任務(wù)列表。JBPM允許將任務(wù)和任務(wù)節(jié)點作為整個流程設(shè)計的一部分使用。
任務(wù)一般在JBPM中定義成任務(wù)節(jié)點。單個任務(wù)節(jié)點可以包含一個或多個任務(wù)。包含任務(wù)節(jié)點的JBPM流程的一個公共行為就是等待任務(wù)節(jié)點中的全部任務(wù)完成,然后繼續(xù)執(zhí)行。某個任務(wù)可被分配3 給個人、用戶組或泳道:
假如任務(wù)被分配給某個特定用戶,那么就只有這個使用者可以執(zhí)行它。
假如任務(wù)被分配給某個用戶組,那么這個組內(nèi)的任何參與者都能執(zhí)行這個任務(wù)。JBPM使用的是參與者池(pooled actors)符號(它可以包含組名、組名列表和參與者個人列表等),而不是組ID。如果用戶開始執(zhí)行在他們組任務(wù)列表中的任務(wù),最終可能會引起沖突4——可能有多人開始執(zhí)行相同的任務(wù)。為了避免這種情況,在開始執(zhí)行任務(wù)之前,用戶應(yīng)該將任務(wù)從組任務(wù)列表移動到他們自己的任務(wù)列表中。
泳道代表一個流程角色,它通常被分配給一個用戶組。它是一種指定流程中的多個任務(wù)要由同一參與者完成的機制5。因此,在第一個任務(wù)被分配給某個泳道之后,流程就會記住所有在相同泳道內(nèi)的后續(xù)任務(wù)都將由同一參與者完成。
JBPM提供了兩種定義任務(wù)分配的基本方法:作為流程定義的一部分或通過編程實現(xiàn)。如果是作為流程定義的一部分,分配可以通過指定具體用戶、用戶組或泳道完成。此外,可以使用表達式根據(jù)流程變量動態(tài)確定某個具體用戶。完整的編程實現(xiàn)是基于分配處理器(assignment handler)的6,它允許任務(wù)根據(jù)任意的計算規(guī)則去查找用戶ID。那么如何才能很好的實現(xiàn)JBPM高級交互模式這一功能呢,讓我們繼續(xù)往下看。
流程定義描述流程實例的方式類似任務(wù)描述任務(wù)實例的方式。當流程執(zhí)行時,一個流程實例——流程的運行時表示——就會被創(chuàng)建。類似,一個任務(wù)實例——任務(wù)的運行時表示——就會被創(chuàng)建。根據(jù)任務(wù)定義,任務(wù)實例被分配給一個參與者/參與者組。
任務(wù)實例的一個作用就是支持用戶交互——把數(shù)據(jù)顯示給用戶并從用戶那里收集數(shù)據(jù)。一個JBPM任務(wù)實例擁有訪問流程(令牌)變量7的全部權(quán)限,而且還可以有自己的變量。任務(wù)能夠擁有自己的變量對于以下場景非常有用:
在任務(wù)實例中創(chuàng)建流程變量的副本,這樣對任務(wù)實例變量的即時更新只有在該任務(wù)完成且這些副本被提交給流程變量時才會影響流程變量。
創(chuàng)建更好支持用戶活動的“派生(計算)”變量。
任務(wù)自己的變量在JBPM中是通過任務(wù)控制器處理器(task controller handler)支持的,它可以在任務(wù)實例創(chuàng)建時生成任務(wù)實例數(shù)據(jù)(從流程數(shù)據(jù)),并在任務(wù)實例完成時將任務(wù)實例數(shù)據(jù)提交給流程變量。
#p#
擴展Task類
JBPM高級交互模式的實現(xiàn)操作中任務(wù)的定義被包含在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類增加字段和方法
這個新的參數(shù)允許指定任務(wù)完成所需的JBPM高級交互模式任務(wù)處理人數(shù)量。缺省值為1,這意味著,只有1個用戶應(yīng)該/可以處理這個任務(wù)。
JBPM使用Hibernate來向數(shù)據(jù)庫保存和讀取數(shù)據(jù)。為了讓我們新加的變量持久化,我們需要更新Task類的Hibernate配置文件(Task.hbm.xml),它在org.JBPM.taskmgmt.def文件夾中,增加代碼如下(清單2)
- < property name="numSignatures"
column="NUMSIGNATURES_" />
清單2 在Task映射文件中指定新增域
為了讓我們新加的屬性能被流程定義和數(shù)據(jù)庫正確讀取,我們需要修改org.JBPM.jpdl.xml.JpdlXmlReader類以正確地讀取我們的新屬性(清單3)
- String numSignatureText =
taskElement.attributeValue
("numSignatures");- if (numSignatureText != null) {
- try{
- task.setNumSignatures
(Integer.parseInt(num
SignatureText));- }
- catch(Exception e){}
- }
清單3 讀取numSignature屬性
最后,因為JpdlXmlReader根據(jù)模式來驗證XML,因此我們需要在jpdl-3.2.xsd中增加一個屬性定義(清單4):
- < xs:element name="task">
- ………………….
- < xs:attribute name=
"numSignatures" type=
"xs:string" />
清單4 在jpdl-3.2.xsd中增加numSignatures屬性
當完成這些工作,JBPM高級交互模式的任務(wù)定義就被擴展可以使用numSignatures屬性(清單5):
- < task name="task2"
numSignatures = "2">- < assignment pooled-actors
="Peter, John">- < /assignment>
- < /task>
清單5 給任務(wù)定義增加numSignatures屬性
#p#
擴展TaskInstance類
在JBPM高級交互模式實現(xiàn)操作到此處時,擴展完任務(wù)類后,我們還需要創(chuàng)建一個自定義的任務(wù)實例類來跟蹤分配給該任務(wù)實例的參與者,并確保所有被分配的參與者完成類執(zhí)行(清單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高級交互模式的操作實現(xiàn)擴展了JBPM提供的TaskInstance類,并跟蹤完成該實例所需的參與者個數(shù)。它引入了幾個新方法,允許參與者預留(reserve)/退還(unreserve)任務(wù)實例,以及讓指定參與者完成任務(wù)執(zhí)行。
#p#
JBPM高級交互模式的實現(xiàn)操作中,清單6的實現(xiàn)依賴一個支持類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類
#p#
JBPM高級交互模式自定義的TaskInstance類和Assignee類都必須保存到數(shù)據(jù)庫中。這意味著需要給這兩個類實現(xiàn)Hibernate映射14 (清單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 自定義任務(wù)實例的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高級交互模式的實現(xiàn)操作能夠使用我們的自定義任務(wù)實例實現(xiàn),我們還需要提供一個自定義的任務(wù)實例工廠(清單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 自定義的任務(wù)實例工廠
最后,為了讓JBPM運行時使用正確的任務(wù)實例工廠(清單10),還必須創(chuàng)建一個新的JBPM配置(清單11)。
- < JBPM-configuration>
- < bean name="JBPM.task.instance.
factory" class="com.navteq.JBPM.
extensions.AssignableTask
InstanceFactory" singleton="true" />- < /JBPM-configuration>
清單11 JBPM配置
完成所有這些變更之后(清單1-11),一個典型的任務(wù)處理顯示如下:
- 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 處理可分配任務(wù)實例
這里,在得到某個用戶的任務(wù)實例并將其轉(zhuǎn)變成可分配任務(wù)實例之后,我們將試著預留它15。一旦預留成功,我們將關(guān)閉JBPM高級交互模式運行時以提交事務(wù)。
#p#
實現(xiàn)任命
JBoss JBPM可以非常輕易的實現(xiàn)手動將任務(wù)分配給特定用戶。根據(jù)JBPM提供的簡單API,可以完成將任務(wù)實例從一個任務(wù)列表移動到另一個任務(wù)列表,因此給某個用戶分配任務(wù)相當直接(清單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 將任務(wù)重新分配給指定用戶
在JBPM高級交互模式的操作中,提供了2類不同的API來設(shè)置參與者池:一類接收字符串id數(shù)組,另一類則接收id集合。如果要清空一個池,就要使用那個接收集合的API(傳入一個null集合)。
實現(xiàn)上報
前面已經(jīng)說過,上報一般被實現(xiàn)為任務(wù)的重新分配,并常常附帶一個上報已發(fā)生的通知;或是實現(xiàn)成一個任務(wù)未及時完成的通知。
實現(xiàn)為重新分配的上報
盡管JBPM不直接支持上報,但它提供了2個基本的機制:超時和重新分配(參見上節(jié))。粗一看,實現(xiàn)上報只需將這二者結(jié)合即可,但是仔細一想還是存在一些困難:
JBPM實現(xiàn)中的關(guān)系并不總是雙向的。如,從一個任務(wù)節(jié)點我們可以找到所有這個節(jié)點定義的任務(wù),但是從一個任務(wù),并沒有API可以完成找到包含它的任務(wù)節(jié)點的工作16;由某個任務(wù)實例,你可以得到一個任務(wù),但是沒有由某個任務(wù)得到所有實例的API,諸如此類。
超時不是發(fā)生在任務(wù)自身,而是發(fā)生在任務(wù)節(jié)點上。由于某個節(jié)點可以關(guān)聯(lián)多個任務(wù),并且JBPM關(guān)系實現(xiàn)并不是雙向的(見上),因此要跟蹤當前任務(wù)實例就需要其他的支持手段。
以重新分配實現(xiàn)的上報的整個實現(xiàn)17涉及3個處理器:
負責給任務(wù)分配參與者的分配處理器。這個處理器跟蹤它是一個首次任務(wù)調(diào)用還是一個上報任務(wù)調(diào)用。清單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 EscalationAssi
gnmentHandler 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 分配處理器示例
在JBPM高級交互模式的實現(xiàn)操作中我們嘗試得到一個包含了給定任務(wù)上報次數(shù)的流程變量。如果變量未定義,則就分配“admin”為任務(wù)擁有者,否則任務(wù)就被分配給“bob”。在這個處理器中可以使用任何其他的分配策略。
任務(wù)實例創(chuàng)建動作處理器(清單15),它保存流程實例上下文的任務(wù)實例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 任務(wù)實例創(chuàng)建處理器
JBPM高級交互模式中任務(wù)節(jié)點計時器觸發(fā)調(diào)用的超時處理器(清單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 超時處理器
這個處理器首先記錄上報計數(shù)器,接著完成此節(jié)點關(guān)聯(lián)的任務(wù)實例。任務(wù)實例的完成伴隨有一個變遷(一般是回到任務(wù)節(jié)點)。
使用以上描述的處理器實現(xiàn)JBPM高級交互模式的上報的簡單流程例子顯示在清單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>
- < /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 簡單流程的上報
實現(xiàn)成通知的上報
JBPM為郵件傳遞提供了強大支持18,這使得實現(xiàn)成通知的上報變得極其簡單。郵件傳遞可由給節(jié)點附加定時器,然后觸發(fā),它使用已經(jīng)寫好的郵件動作來完成通知傳遞。
實現(xiàn)鏈狀執(zhí)行
鏈狀執(zhí)行直接由JBPM泳道支持,并不需要額外的開發(fā)。
總結(jié)
不管我們在自動化方面投入多少努力,面對復雜的業(yè)務(wù)流程,總免不了要有人工介入的可能。在這篇JBPM高級交互模式的操作介紹的文章中,我給出了一系列已建立的高級人工交互模式,并展示了用JBPM完成它是多么輕而易舉。