许多通用业务流程都包含人类参与者。人类活动,从简单场景(如人工批准)到复杂场景(涉及复杂的数据输入),在流程实现中引入了新的方面,如人类交互模式。人类交互模式的一个典型集合1 包括:
在本文中,我将讨论如何使用JBoss jBPM来实现这些高级的交互模式。
jBPM的一个核心功能2是为人类管理任务和任务列表。jBPM允许将任务和任务节点作为整个流程设计的一部分使用。
任务一般在jBPM中定义成任务节点。单个任务节点可以包含一个或多个任务。包含任务节点的jBPM流程的一个公共行为就是等待任务节点中的全部任务完成,然后继续执行。某个任务可被分配3 给个人、用户组或泳道:
jBPM提供了两种定义任务分配的基本方法:作为流程定义的一部分或通过编程实现。如果是作为流程定义的一部分,分配可以通过指定具体用户、用户组或泳道 完成。此外,可以使用表达式根据流程变量动态确定某个具体用户。完整的编程实现是基于分配处理器(assignment handler)的6,它允许任务根据任意的计算规则去查找用户ID。
流程定义描述流程实例的方式类似任务描述任务实例的方式。当流程执行时,一个流程实例——流程的运行时表示——就会被创建。类似,一个任务实例——任务的运行时表示——就会被创建。根据任务定义,任务实例被分配给一个参与者/参与者组。
任务实例的一个作用就是支持用户交互——把数据显示给用户并从用户那里收集数据。一个jBPM任务实例拥有访问流程(令牌)变量7的全部权限,而且还可以有自己的变量。任务能够拥有自己的变量对于以下场景非常有用:
任务自己的变量在jBPM中是通过任务控制器处理器(task controller handler)支持的8,它可以在任务实例创建时生成任务实例数据(从流程数据),并在任务实例完成时将任务实例数据提交给流程变量。
我们上面已经说过,实现四眼原则意味着要允许多人同时干一个活。它的实现有以下几种可能方法:
根据jBPM最佳实践11 ——“扩展jBPM API而不是去搞复杂的流程建模”12 ,我决定采用任务内解决的方法。这就要求修改jBPM提供的任务和任务实例类。
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属性
在扩展完任务类后,我们还需要创建一个自定义的任务实例类来跟踪分配给该任务实例13的参与者,并确保所有被分配的参与者完成类执行(清单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映射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 自定义任务实例的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 处理可分配任务实例
这里,在得到某个用户的任务实例并将其转变成可分配任务实例之后,我们将试着预留它15。一旦预留成功,我们将关闭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个基本的机制:超时和重新分配(参见上节)。粗一看,实现上报只需将这二者结合即可,但是仔细一想还是存在一些困难:
以重新分配实现的上报的整个实现17涉及3个处理器:
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”。在这个处理器中可以使用任何其他的分配策略。
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 任务实例创建处理器
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>
</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为邮件传递提供了强大支持18,这使得实现成通知的上报变得极其简单。邮件传递可由给节点附加定时器,然后触发,它使用已经写好的邮件动作来完成通知传递。
链状执行直接由jBPM泳道支持,并不需要额外的开发。
不管我们在自动化方面投入多少努力,面对复杂的业务流程,总免不了要有人工介入的可能。在这篇文章中,我给出了一系列已建立的高级人工交互模式,并展示了用jBPM完成它是多么轻而易举。
查看英文原文:Supporting Advanced User Interaction Patterns in jBPM。