使用jBpm支持高级用户交互模式
使用jBpm支持高级用户交互模式
<!-- AddThis Button END -->
许多通用业务流程都包含人类参与者。人类活动,从简单场景(如人工批准)到复杂场景(涉及复杂的数据输入),在流程实现中引入了新的方面,如人类交互模式。人类交互模式的一个典型集合1 包括:
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类在扩展完任务类后,我们还需要创建一个自定义的任务实例类来跟踪分配给该任务实例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 okfor(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 assignmentsint 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 reservedfor(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 /></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;@Overridepublic 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");// SavejbpmContext.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个基本的机制:超时和重新分配(参见上节)。粗一看,实现上报只需将这二者结合即可,但是仔细一想还是存在一些困难:
jBPM实现中的关系并不总是双向的。如,从一个任务节点我们可以找到所有这个节点定义的任务,但是从一个任务,并没有API可以完成找到包含它的任务节点的工作16;由某个任务实例,你可以得到一个任务,但是没有由某个任务得到所有实例的API,诸如此类。?超时不是发生在任务自身,而是发生在任务节点上。由于某个节点可以关联多个任务,并且jBPM关系实现并不是双向的(见上),因此要跟踪当前任务实例就需要其他的支持手段。以重新分配实现的上报的整个实现17涉及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;@Overridepublic 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 throughassignable.setActorId("admin");}else{// Escalateassignable.setActorId("bob");}}}}清单14 分配处理器示例
这里我们尝试得到一个包含了给定任务上报次数的流程变量。如果变量未定义,则就分配“admin”为任务拥有者,否则任务就被分配给“bob”。在这个处理器中可以使用任何其他的分配策略。
任务实例创建动作处理器(清单15),它保存流程实例上下文的任务实例idpackage 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;@Overridepublic 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;@Overridepublic 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);elseescLevel += 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完成它是多么轻而易举。
1WS-BPEL Extension for People - BPEL4People
2 本文依据jBPM 3完成。jBPM 4对任务支持引入了一些扩展。
3 任务定义规定了分配定义。实际的分配发生在任务实例创建的时候。参见以下的任务实例。
4 这一问题在jBPM 4 中有所缓解,它引入了任务完成所需的任务接受API。
5 参见上面的链状执行模式。
6jBPM任务节点开发
7使用JBoss / JBPM编排长时间运行的活动
8不象流程变量,jBPM任务控制器处理器变量是保存在内存中的。因此,如果必须允许多次访问一个支持保存中间执行结果的任务实例,使用任务控制器处理器可能是个错误的解决方案。
9参见InfoQ文章 for implementation of parallel loop.
10这要求给任务节点增加create-tasks="false"属性。
11JBPM最佳实践
12严格说来,使用任务处理器方法将可能实现我的需求且代码更少,但是这样它就与上报实现(见下)不兼容。此外,我还想展示如何修改缺省的任务和任务实例实现。
13类似jPDL 4实现。
14在给自定义任务实例创建Hibernate映射时,将映射实现成缺省类接口的子类要简单得多。现有jBPM Hibernate映射严重依赖任务实例映射,因此从标准实现派生将使引入新任务实例的改动最小。
15冲突条件仍然可能存在,但是已经最小了。
16这可以使用Hibernate Queries完成,但没有可供直接使用的API。
17这个实现假定某个任务节点只包含一个任务,该任务只创建一个任务实例。
18Jbpm邮件传递