BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

使用jBpm支持高级用户交互模式

| 作者 Boris Lublinsky 关注 1 他的粉丝 ,译者 胡键 关注 0 他的粉丝 发布于 2010年1月16日. 估计阅读时间: 46 分钟 | QCon上海2018 关注大数据平台技术选型、搭建、系统迁移和优化的经验。

许多通用业务流程都包含人类参与者。人类活动,从简单场景(如人工批准)到复杂场景(涉及复杂的数据输入),在流程实现中引入了新的方面,如人类交互模式。人类交互模式的一个典型集合1 包括:

  • 四眼原则(The 4-eyes principle),通常又被称为“职责分离”,它是决策由多人彼此独立作出时的一个常见场景。在很多情况下,很容易就能得到另一个观点/签名。
  • 任命(Nomination)是指上级根据团队成员的任务安排、工作负荷或经验人工地将任务分配给他的情形。
  • 任务通常被建模来表达一种预期:它们将在确定时间段内完成。如果任务没有按预期地进展,就需要一种上报(escalation)机制。两种典型的上报实现是:重新分配任务,并常常附带一个上报已经发生的通知;或任务未按时完成的通知(通常发给经理)。
  • 链状执行(Chained execution)是一个流程(片断),其中的一系列步骤是由同一个人来完成。

在本文中,我将讨论如何使用JBoss jBPM来实现这些高级的交互模式。

jBPM中的任务管理

jBPM的一个核心功能2是为人类管理任务和任务列表。jBPM允许将任务和任务节点作为整个流程设计的一部分使用。

任务一般在jBPM中定义成任务节点。单个任务节点可以包含一个或多个任务。包含任务节点的jBPM流程的一个公共行为就是等待任务节点中的全部任务完成,然后继续执行。某个任务可被分配3 给个人、用户组或泳道:

  • 假如任务被分配给某个特定用户,那么就只有这个使用者可以执行它。
  • 假如任务被分配给某个用户组,那么这个组内的任何参与者都能执行这个任务。jBPM使用的是参与者池(pooled actors)符号(它可以包含组名、组名列表和参与者个人列表等),而不是组ID。如果用户开始执行在他们组任务列表中的任务,最终可能会引起冲突4——可能有多人开始执行相同的任务。为了避免这种情况,在开始执行任务之前,用户应该将任务从组任务列表移动到他们自己的任务列表中。 
  • 泳道代表一个流程角色,它通常被分配给一个用户组。它是一种指定流程中的多个任务要由同一参与者完成的机制5。因此,在第一个任务被分配给某个泳道之后,流程就会记住所有在相同泳道内的后续任务都将由同一参与者完成。

jBPM提供了两种定义任务分配的基本方法:作为流程定义的一部分或通过编程实现。如果是作为流程定义的一部分,分配可以通过指定具体用户、用户组或泳道 完成。此外,可以使用表达式根据流程变量动态确定某个具体用户。完整的编程实现是基于分配处理器(assignment handler)的6,它允许任务根据任意的计算规则去查找用户ID。

流程定义描述流程实例的方式类似任务描述任务实例的方式。当流程执行时,一个流程实例——流程的运行时表示——就会被创建。类似,一个任务实例——任务的运行时表示——就会被创建。根据任务定义,任务实例被分配给一个参与者/参与者组。

任务实例的一个作用就是支持用户交互——把数据显示给用户并从用户那里收集数据。一个jBPM任务实例拥有访问流程(令牌)变量7的全部权限,而且还可以有自己的变量。任务能够拥有自己的变量对于以下场景非常有用:

  • 在任务实例中创建流程变量的副本,这样对任务实例变量的即时更新只有在该任务完成且这些副本被提交给流程变量时才会影响流程变量。
  • 创建更好支持用户活动的“派生(计算)”变量。

任务自己的变量在jBPM中是通过任务控制器处理器(task controller handler)支持的8,它可以在任务实例创建时生成任务实例数据(从流程数据),并在任务实例完成时将任务实例数据提交给流程变量。

实现四眼原则

我们上面已经说过,实现四眼原则意味着要允许多人同时干一个活。它的实现有以下几种可能方法:

  • 在任务外解决:需要大量时间的任务并行循环(parallel looping)9
  • 使用动作处理器(Action handler):附加到任务节点的进入事件(enter event),基于流程实例变量创建多个节点实例10
  • 在任务内解决:引入“任务接受量(task take)”(类似jPDL 4)并允许某个任务实例可被接受多次。

根据jBPM最佳实践11 ——“扩展jBPM API而不是去搞复杂的流程建模”12 ,我决定采用任务内解决的方法。这就要求修改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类

在扩展完任务类后,我们还需要创建一个自定义的任务实例类来跟踪分配给该任务实例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个基本的机制:超时和重新分配(参见上节)。粗一看,实现上报只需将这二者结合即可,但是仔细一想还是存在一些困难:

  • 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;

	@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完成它是多么轻而易举。

1 WS-BPEL Extension for People - BPEL4People

2 本文依据jBPM 3完成。jBPM 4对任务支持引入了一些扩展。

3 任务定义规定了分配定义。实际的分配发生在任务实例创建的时候。参见以下的任务实例。

4 这一问题在jBPM 4 中有所缓解,它引入了任务完成所需的任务接受API。

5 参见上面的链状执行模式。

6 jBPM任务节点开发

7 使用JBoss / JBPM编排长时间运行的活动

8不象流程变量,jBPM任务控制器处理器变量是保存在内存中的。因此,如果必须允许多次访问一个支持保存中间执行结果的任务实例,使用任务控制器处理器可能是个错误的解决方案。

9参见InfoQ文章 for implementation of parallel loop.

10这要求给任务节点增加create-tasks="false"属性。

11 JBPM最佳实践

12严格说来,使用任务处理器方法将可能实现我的需求且代码更少,但是这样它就与上报实现(见下)不兼容。此外,我还想展示如何修改缺省的任务和任务实例实现。

13类似jPDL 4实现。

14在给自定义任务实例创建Hibernate映射时,将映射实现成缺省类接口的子类要简单得多。现有jBPM Hibernate映射严重依赖任务实例映射,因此从标准实现派生将使引入新任务实例的改动最小。

15冲突条件仍然可能存在,但是已经最小了。

16这可以使用Hibernate Queries完成,但没有可供直接使用的API。

17这个实现假定某个任务节点只包含一个任务,该任务只创建一个任务实例。

18 Jbpm邮件传递

查看英文原文:Supporting Advanced User Interaction Patterns in jBPM


给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家加入到InfoQ中文站用户讨论组中与我们的编辑和其他读者朋友交流。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT