jbpm3\jbpm4
许多通用业务流程包括人类参与者。 从简单的场景(例如人工批准)到涉及复杂的数据输入的复杂场景,人类活动都将新的方面(例如人类交互模式)引入到流程实现中。 人类交互模式的典型集合1包括以下内容:
- 四眼原则 (通常称为“职责分离”)是常见的情况,即两个或两个以上的人彼此独立地做出决定。 在许多情况下,获得第二意见/签名就足够了。
- 提名是指主管根据他们的日程安排或工作量限制或专业知识将任务手动分配给其团队成员。
- 通常对任务进行建模以表达期望它们将在特定时间范围内完成的期望。 如果任务未按预期进行,则需要升级机制。 两种典型的升级实现是-任务的重新分配(通常带有通知),说明升级已经发生,通知(通常是给管理者)任务没有及时完成。
- 链式执行是同一人执行一系列步骤的过程(片段)。
在本文中,我将描述如何使用JBoss jBPM来实现这些高级交互模式。
jBPM中的任务管理
jBPM 2的核心功能之一是人员的任务和任务列表的管理。 jBPM允许将任务和任务节点用作整个流程设计的一部分。
通常,任务是在任务节点的jBPM中定义的。 单个任务节点可以包含一个或多个任务。 包含任务节点的jBPM流程的常见行为是等待,直到任务节点中的所有任务都完成,然后再继续。 可以将给定任务分配3到单个用户或用户组或泳道:
- 如果将任务分配给特定用户,则只有该用户才能执行它。
- 如果将任务分配给一组用户,则该组的任何参与者都可以执行任务。 jBPM代替了集合ID,而是使用池化参与者的概念(它可以包含一个群组名称,群组名称列表,各个参与者的列表等)。 如果用户开始处理其组任务列表中的任务,则可能会导致冲突4-许多人将开始处理同一任务。 为避免这种情况,用户在开始执行任务之前,应将任务实例从组任务列表移至其个人任务列表。
- 泳道是一个过程角色,通常分配给一组用户。 它是一种机制,指定流程中的多个任务应由同一角色5完成 。 因此,在将第一个任务分配给给定泳道后,该过程将记住该泳者在同一泳道中的所有后续任务的参与者。
jBPM提供了两种定义任务分配的基本方法-作为流程定义的一部分或以编程方式。 在分配作为流程定义的一部分的情况下,可以通过指定特定用户,用户组或泳道来定义受让人。 另外,表达式可用于基于过程变量动态确定特定用户。 完全程序化的实现基于分配处理程序6 ,该分配处理程序6允许任务根据任意计算来找到用户ID。
流程定义以类似于任务描述任务实例的方式描述流程实例。 执行流程时,将创建流程实例(流程的运行时表示)。 同样,将创建任务实例–任务的运行时表示。 根据任务定义,将任务实例分配给一个参与者/一组参与者。
任务实例的作用是支持用户交互-向用户显示数据并从用户收集数据。 jBPM任务实例可以完全访问流程(令牌)变量7 ,也可以拥有自己的变量。 具有任务自己的变量的功能对于以下方面很有用:
- 在任务实例中创建过程变量的副本,以使对任务实例变量的中间更新不会影响过程变量,直到任务完成并将副本提交回过程变量中为止。
- 创建“派生的”(计算得出的)变量可以更好地支持用户的活动。
通过任务控制器处理程序8在jBPM中支持任务自己的变量,该任务控制器可以在任务实例创建时填充任务实例数据(来自过程数据),并在任务实例完成后将任务的实例数据提交到过程变量中。
实施四眼原则
正如我们上面所定义的,实现四眼原理意味着允许多个人同时完成任务。 有几种可能的方法可以实现这种实现:
- 任务外部–任务的并行循环9所需的时间。
- 使用附加到任务节点的动作处理程序,输入事件基于流程实例变量来创建多个节点实例10 。
- 在任务内部,引入了“任务执行”(类似于jPDL 4)支持,并允许多次执行给定的任务实例
基于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,表示只有一个用户可以/可以处理任务。
jBPM正在使用Hibernate模式将数据存储到数据库或从数据库检索数据。 为了使新变量具有持久性,我们需要通过添加以下行(清单2)来更新Task类的Hibernate配置文件-Task.hbm.xml –位于org.jbpm.taskmgmt.def
文件夹中。
<propertyname =" numSignatures " column =" NUMSIGNATURES_ " />
清单2在任务映射中指定其他字段
为了从流程定义和数据库中正确读取我们的新属性,我们需要修改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将numSignatures属性添加到jpdl-3.2.xsd
完成所有这些操作后,可以使用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 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. Extended TaskInstance类
此实现扩展了jBPM提供的TaskInstance类,并跟踪完成一个实例所需的actor数量。 它引入了几种新方法,允许角色保留/取消保留任务实例,并由给定角色完成任务执行。
一个实现(清单6)依赖于支持类的受让人(清单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受让人类
自定义任务实例类和受让人类都必须可存储在数据库中。 这意味着有必要为它们两者实现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 name name=" 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受让人类的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自定义taskInstance工厂
最后,为了使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)后,典型的任务处理如下所示(清单12):
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提供了两种不同的API来设置池角色,一个采用ID的字符串数组,另一个采用ID的集合。 要清除一个池,应该使用一个带一组(带有空Set)的池。
实施升级
正如我们上面定义的那样,升级通常是通过重新分配任务(通常带有已发生升级的通知)或任务实例未及时完成的通知来实现的。
通过重新分配进行升级
尽管jBPM不直接支持升级,但它提供了两种基本机制-超时和重新分配(请参见上文)。 尽管将两者结合起来以实现升级似乎很简单,但仔细观察会发现一些复杂情况:
- jBPM实现中的关系并不总是双向的。 例如,基于一个任务节点,我们可以找到该节点定义的所有任务,但是给定一个任务,就找不到包含任务节点16的 API。 给定任务实例,您可以获取任务,但是没有API可以获取给定任务的所有实例; 等等
- 超时不是在任务本身上,而是在任务节点上。 由于给定节点可以具有多个任务,因此与其关联以及jBPM关系的实现不是双向的(请参见上文),因此需要附加支持来跟踪当前任务实例。
通过重新分配进行升级的总体实施17涉及三个处理程序:
- 负责将角色分配给任务的分配处理程序。 该处理程序跟踪它是第一个还是升级的任务调用。 清单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>
</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实施这些模式有多么容易。
2本文基于jBPM3。jBPM 4引入了对任务支持的一些扩展。
3任务定义指定分配定义。 实际分配是在创建任务实例时发生的。 请参阅下面的任务实例。
5请参见上面的链接执行模式。
8与过程变量不同,jBPM任务控制器处理程序变量保留在内存中。 因此,如果有必要允许对支持保存中间执行结果的任务实例进行多次访问,则使用任务控制器处理程序可能是错误的解决方案。
10这需要向任务节点添加create-tasks =“ false”属性。
12从技术上讲,任务处理程序方法的使用将满足我的需要,所需的代码更少,但它与升级实现不兼容(如下)。 我还想展示如何修改默认任务和任务实例实现。
13与jPDL 4实施类似。
14为自定义任务实例创建Hibernate映射时,将映射实现为默认类接口的子类要容易得多。 现有的jBPMHibernate映射严重依赖于任务实例映射,因此,子类化标准实现将最大限度地减少引入新任务实例所需的更改。
15仍有可能出现赛车状况,但可能性很小。
16这可以使用Hibernate查询来完成,但是没有直接的API。
17此实现假定给定任务节点包含一个任务,该任务创建一个任务实例。
jbpm3\jbpm4