一、流程

    在JPDL中process元素是每个流程定义的顶级元素,即任何流程定义都必须以如下形式开始和结束

<process>
...
</process>

process元素拥有的属性:


属性类型默认值是否必须描述
name文本必须展示给用户
key
如省略,则根据name生成
标识不同流程
version整型从1开始
同一流程的不同版本

它下的子元素有:description、activities



二、流转控制活动

  • start——开始活动

  • state——状态活动

  • decision——判断活动

  • fork--join——分支/聚合活动

  • end——结束活动

  • task——人工任务活动

  • sup-process——子流程活动

  • custom——自定义活动

1.start

    即流程的入口,一个流程中必须拥有一个start,必须有一个流出转移(transition),这个转移会在流程中通过start活动的时候执行。


2.state(状态活动)

当业务流程受到某些特定的外部干预后再继续运行,而在这之前流程处于一个中断等待的状态,这个时候就是state活动。

示例:

wKiom1kmw-uyj9D-AAARU0HDYG4339.jpg

对应的JPDL如下:

<process name="stateSequence" xmlns="http://jbpm.org/4.4/jpdl">
   <!-- 流程开始后转移到a state -->
   <start name="start">
      <transition to="a"/>
   </start>
   <!-- a state转移到b state -->
   <state name="a">
      <transition to="b"/>
   </state>
   <!-- b state转移到c state -->
   <state name="b">
      <transition to="c"/>
   </state>
   <!-- 最终停留在c state -->
   <state name="c"/>
</process>

根据此流程定义发起实例:

ProcessInstance processInstance = 
		executionService.startProcessInstanceByKey("stateSequence");
//在没有任何外部触发的情况下,此流程会在a state一直等待,调用singalXx会触发流程进入下一步
Execution executionInA = processInstance.findActiveExecutionIn("a");
//断言流程实例在a state等待
assertNotNull(executionInA);
//发出触发执行的信号
processInstance = executionService.signalExecutionById(executionInA.getId());
Execution executionInB = processInstance.findActiveExecutionIn("b");
//断言流程实例走向下一步b state
assertNotNull(executionInB);
//继续触发
processInstance = executionService.signalExecutionById(executionInB.getId());
//...c

在state活动里可以定义多个transition元素,通过触发指定转移路径的名称,可以选择其中的一个transition通过。

示例:

wKioL1kmx9GSHK4GAAAfnUSL9lg530.jpg

对应的JPDL如下:

<process name="stateChice" xmlns="http://jbpm.org/4.4/jpdl">
   <!-- 流程开始后转移到wait for response state -->
   <start>
   		<transition to="wait for response"/>
   </start>
   <!-- 这里有两个transaction供选择,二者选一 -->
   <state name="wait for response">
   		<transition name="accept" to="submit doc"/>
   		<transition name="reject" to="try again"/>
   </state>
   <!-- 此活动为accept的transition -->
   <state name="submit doc"/>
   <!-- 此活动为reject的transition -->
   <state name="submit doc"/>
</process>

要想该流程运行起来,则需要:

ProcessInstance processInstance = 
		executionService.startProcessInstanceByKey("stateChoice");
//假设该流程到达了wait for response,则该实例会一直等待外部触发的出现
//获得流程实例的ID
String executionId = processInstance.findActiveExecutionIn("wait for response").getId();
//触发accept信号
processInstance = executionService.signalExecutionById(executionId,"accept");
//断言流程实例流向了预期的活动
assertTrue(processInstance.isActive("submit doc"));
		
//...同理适用于reject transition


3.decision(判断活动)

    根据条件在多个流转路径中选择其一通过,也就是做一个决定性的判断,这时候使用decision。

    decision可以拥有多个流出转移,当流程实例到达decision活动时,会根据最先匹配成功的一个条件自动地通过响应的流出转移。

    decision流向哪个转移,有3种方式。

1>使用decision活动的condition元素

当一个transition的condition值为true或者一个没有设置condition的transition,那么该流程就立刻流向这个transition。

wKiom1km8LigWqWyAAAqS-BrYrk359.jpg

对应的JPDL:

<process name="decisionConditions" xmlns="http://jbpm.org/4.4/jpdl">
   <start>
   		<transition to="evaluate document"/>
   </start>
   <decision name="evaluate document">
   	<!-- 以下是两个流转条件表达式 -->
   	<transition to="submit doc">
   		<!-- 变量content等于good -->
   		<condition expr="#{content=='good'}"/>
   	</transition>
   	<transition to="try again">
   		<!-- 变量content等于bad -->
   		<condition expr="#{content=='bad'}"/>
   	</transition>
   	<!-- 无条件转移 -->
   	<transition to="give up"/>
   </decision>
   
   <state name="submit doc"/>
   <state name="try again"/>
   <state name="give up"/>
</process>

测试代码如下:

Map<String, Object> variables = new HashMap<>();
	variables.put("content", "good");
//发起流程实例并传入流程变量
ProcessInstance processInstance = executionService
		.startProcessInstanceById("decisionCondition", variables);
//断言
assertTrue(processInstance.isActive("submit doc"));

2>使用decision活动的expr属性

expr属性来判断流程的转向,需要指定流转的路径

wKioL1km-KmTHOFRAAAgyPKFxqU290.jpg

对应的JPDL如下:

<process name="decisionExpression" xmlns="http://jbpm.org/4.4/jpdl">
   <start>
   	<transition to="evaluate document"/>
   </start>
   <!-- #{content}即为判断表达式 -->
   <decision expr="#{content}" name="evaluate document">
       <!-- content值为good时的转移 -->
       <transition name="good" to="submit doc"/>
       <!-- content值为bad时的转移 -->
       <transition name="bad" to="try again"/>
       <!-- content值为ugly时的转移 -->
       <transition name="ugly" to="give up"/>
   </decision>
   
   <state name="submit doc"/>
   <state name="try again"/>
   <state name="give up"/>
</process>

单元测试如下(同上):

Map<String, Object> variables = new HashMap<>();
variables.put("content", "good");
//发起流程实例并传入流程变量
ProcessInstance processInstance = executionService
		.startProcessInstanceById("decisionCondition", variables);
//断言
assertTrue(processInstance.isActive("submit doc"));

3>使用decision活动的handler元素

当判断流转时计算大量、复杂的业务逻辑时,可以实现DecisionHandler接口

DecisionHandler接口

public interface DecisionHandler extends Serializable {

  //提供流程实例的执行上下文(execution)作为参数,返回字符串类型的转移名称
  String decide(OpenExecution execution);
}

此时handler需要作为decision活动的子元素进行配置。

示例:

wKiom1km-4mC-I9aAAAgyPKFxqU824.jpg

对应的jPDL如下:

<process name="decisionHandler" xmlns="http://jbpm.org/4.4/jpdl">
   <start name="start">
      <transition to="evaluate document"/>
   </start>
   <decision name="evaluate document">
      <!-- 所有的流转处理逻辑都委派给ContentEvaluation -->
      <handler class="test.TestHandler"/>
      <transition name="good" to="submit doc"/>
      <transition name="bad" to="try again"/>
      <transition name="ugly" to="give up"/>
   </decision>
   <state name="submit doc"/>
   <state name="try again"/>
   <state name="give up"/>
</process>

对应的TestHandler如下:

public class TestHandler implements DecisionHandler {

	@Override
	public String decide(OpenExecution execution) {
		//获得流程变量content
		String content = (String)execution.getVariable("content");
		if ("great".equals(content)) {
			return "good";
		}
		if ("improve".equals(content)) {
			return "bad";
		}
		return "ugly";
	}

}

对应的单元测试如下:

Map<String, Object> variables = new HashMap<>();
variables.put("content", "great");
//发起流程实例并传入流程变量
ProcessInstance processInstance = executionService
		.startProcessInstanceById("decisionHandler", variables);
//断言
assertTrue(processInstance.isActive("submit doc"));

注意:decision与state的区别!

decision活动定义的流转条件没有任何一个得到满足,那么流畅实例将无法进行下去会抛出异常。

state活动有多个流出转移,且同样没有任何一个得到满足,那么会从定义的第一条流转出去。

结论:decision具有更加严格的判断,如不定义默认路径,当无法满足条件时则报错


4.fork-join(分支/聚合活动)

当我们需要流程并发执行的时候,就需要使用到fork-join活动的组合,fork可以使流程在一条主干上出现并行的分支,join则可以使流程的并发分支聚合成一条主干。

wKiom1k-lVHC2HGrAAA57WCnWeQ728.jpg

对应的jPDL:

<process name="concurrency" xmlns="http://jbpm.org/4.4/jpdl">
   <start name="start1" g="57,199,48,48">
      <transition to="fork"/>
   </start>
   <!--流程在此产生3个并行的分支 -->
   <fork name="fork" g="179,198,48,48">
      <transition to="send invoice"/>
      <transition to="load truck" g="205,322:"/>
      <transition to="print doc" g="201,130:"/>
   </fork>
   <state name="send invoice" g="315,105,92,52">
      <transition to="final join" g="664,130:"/>
   </state>
   <state name="load truck" g="318,196,92,52">
      <transition to="shipping join" g="503,220:"/>
   </state>
   <state name="print doc" g="316,296,92,52">
      <transition to="shipping join" g="511,323:"/>
   </state>
   <!-- 分支活动load truck和print doc在此聚合 -->
   <join name="shipping join">
      <transition to="drive truck"/>
   </join>
   <state name="drive truck">
   	  <transition to="final join"/>
   </state>
   <!-- 最终聚合 -->
   <join name="final join">
   	  <transition to="end"/>
   </join>
   <end name="end" />
</process>

单元测试执行上述流程定义:

//发起流程实例
ProcessInstance processInstance = executionService
		.startProcessInstanceByKey("concurrency");
String pid = processInstance.getId();
//构造一个活动集合以验证分支
Set<String> activitys = new HashSet<>();
activitys.add("send invoice");
activitys.add("load truck");
activitys.add("print doc");
//断言当前活动即为产生的3个分支
assertEquals(activitys, processInstance.findActiveActivityNames());
//发出执行信号通过"send invoice"活动,这个时候流程会在final join等待其他分支
String sendInvoice = processInstance.findActiveExecutionIn("send invoice").getId();
processInstance = executionService
		.signalExecutionById(sendInvoice);
		
//...在活动名称集合中排除"send invoice"活动
activitys.remove("send invoice");
//此时,仍然可以断言另外2个分支还在等待
assertNotNull(processInstance.findActiveExecutionIn("load truck"));
assertNotNull(processInstance.findActiveExecutionIn("print doc"));
//发出执行信号通过剩下的第1个分支——load truck活动
String loadTruck = processInstance.findActiveExecutionIn("load truck").getId();
processInstance = executionService.signalExecutionById(loadTruck);
		
//....在活动中排除"load truck"
activitys.remove("load truck");
//发出执行信号——print doc活动
String printDoc = processInstance
	.findActiveExecutionIn("print doc").getId();
processInstance = executionService.signalExecutionById(printDoc);

//...在活动中排除"print doc"
activitys.remove("print doc");
//断言通过第一个活动shipping join,到达了drive truck
activitys.add("drive truck");
assertEquals(activitys, processInstance.findActiveActivityNames());
assertNotNull(processInstance.findActiveExecutionIn("drive truck"));
//发出执行信号通过"drive truck"
String driveTruck = processInstance.findActiveExecutionIn("drive truck").getId();
processInstance = executionService.signalExecutionById(driveTruck);
		
//最终聚合"final join"
//因此断言此流程已经不存在了
assertNull("流程:"+pid+"不存在,"+executionService.findExecutionById(pid));


5.end(结束活动)

默认情况下,当流程实例运行到end活动会结束,但是在到达end活动的流程实例中仍然活跃的流程活动将会被保留继续执行。

简单流程定义:

wKiom1k-lbChCYJqAAAKE74i16U595.jpg

对应的jPDL:

<process name="concurrency" xmlns="http://jbpm.org/4.4/jpdl">
   <start g="57,199,48,48">
      <transition to="end"/>
   </start>
   <end name="end" g="243,197,48,48"/>
</process>

单元测试如下:

//发起流程实例
ProcessInstance processInstance = executionService
		.startProcessInstanceByKey("concurrency");
//创建流程实例后,直接断言其结束
assertTrue(processInstance.isEnded());


复杂一些的end活动

一个流程定义可以有多个end活动,以便通过事件机制触发不同的结束方式。

<process name="concurrency" xmlns="http://jbpm.org/4.4/jpdl">
   <start g="57,199,48,48">
      <transition to="get code"/>
   </start>
   <!-- 此活动有3条流出转移可选择 -->
   <state name="get code" g="213,196,92,52">
      <transition name="200" to="ok" g="258,135:-33,-5"/>
      <transition name="400" to="bad request" g="-35,-17"/>
      <transition name="500" to="server error" g="261,303:-35,-22"/>
   </state>
   <end name="ok" g="423,112,48,48"/>
   <end name="bad request" g="427,197,48,48"/>
   <end name="server error" g="427,277,48,48"/>
</process>

测试:

//发起流程实例
ProcessInstance processInstance = executionService
		.startProcessInstanceByKey("concurrency");
String executionId = processInstance.getId();
//指定转移名称:400
processInstance = executionService.signalExecutionById(executionId,"400");
//断言流程实例结束
assertTrue(processInstance.isEnded());

在实际应用中,为了表明流程实例的结束状态,可以利用end活动的state属性标识 或者利用jBPM4提供的特殊end活动:end-cancel活动end-error活动

例如:

wKioL1k-o5PxKJePAAAf-C59LiM360.jpg

对应的jPDL:

<process name="concurrency" xmlns="http://jbpm.org/4.4/jpdl">
   <start g="57,199,48,48">
      <transition to="get code"/>
   </start>
   
   <!-- 3条流出转移指向不同状态的end活动 -->
   <state name="get code" g="213,196,92,52">
      <transition name="200" to="ok" g="258,135:-33,-5"/>
      <transition name="400" to="bad request" g="-45,-25"/>
      <transition name="500" to="server error" g="261,317:-36,-48"/>
   </state>
   <!-- 此end活动设置流程的状态为completed -->
   <end name="ok" g="423,112,48,48"/>
   <!-- 此end活动设置流程的状态为cancel -->
   <end-cancel name="bad request" g="423,199,48,48"/>
   <!-- 此end活动设置流程的状态为error -->
   <end-error name="server error" g="427,295,48,48"/>
</process>

单元测试:测200的转移实例

//发起流程实例
ProcessInstance processInstance = executionService
		.startProcessInstanceByKey("concurrency");
String executionId = processInstance.getId();
//指定转移名称:400
processInstance = executionService.signalExecutionById(executionId,"200");
//断言流程实例的状态与预期符合,state属性值为completed
assertEquals("completed", processInstance.getState());
assertTrue(processInstance.isEnded());


6.task(人工任务活动)

用来处理涉及人机交互的活动。

1>关于任务的分配者

assignee属性将一个任务分配给指定的用户。

wKioL1k-qdmhF0VCAAAQvmcObcQ183.jpg

对应的jPDL:

<process name="concurrency" xmlns="http://jbpm.org/4.4/jpdl">
   <start name="start" g="111,196,48,48">
      <transition to="review"/>
   </start>
   <!-- EL表达式"#{order.owner}"的值在这里表示分配者ID -->
   <task name="review" assignee="#{order.owner}" g="264,190,92,52">
      <transition to="wait"/>
   </task>
   <state name="wait" g="431,189,92,52"/>
</process>

注意:assignee属性引用了一个用户,即负责完成任务的人;assignee属性默认会作为EL表达式来执行,#{order.owner}意味着使用order这个名称在任务对应的流程变量中查找一个对象,然后通过order对象的getOwner方法获得用户ID。

public class Order implements Serializable{

	//owner成员域保存用户的ID
	String owner;
	public Order(String owner){
		this.owner = owner;
	}
	public String getOwner() {
		return owner;
	}
	public void setOwner(String owner) {
		this.owner = owner;
	}
}

基于此定义进行测试:

Map<String, Object> vars = new HashMap<>();
vars.put("order", new Order("alex"));
//发起流程实例
ProcessInstance processInstance = executionService
		.startProcessInstanceByKey("concurrency",vars);
//用户alex可通过findPersonalTasks获得
List<Task> taskList = taskService.findPersonalTasks("alex");


2.关于任务的候选者

jBPM支持将任务分配给一组候选用户,组内的一个用户可以接受这个任务并完成。

wKioL1k_-UbA8keAAAAQvmcObcQ740.jpg

对应的jPDL如下:

<process name="concurrency" xmlns="http://jbpm.org/4.4/jpdl">
   <start name="start" g="111,196,48,48">
      <transition to="review"/>
   </start>
   <!-- 这里用字符串引用了一个用户组:sales-dept -->
   <task name="review" candidate-groups="sales-dept" g="264,190,92,52">
      <transition to="wait"/>
   </task>
   <state name="wait" g="431,189,92,52"/>
</process>

流程实例发起后,任务review会被创建,但是不会显示在任何人的个人任务列表中,因为还没有创建sales-dept组。可以通过taskService.findGroupTasks来获取。

//首先创建sales-dept组
identityService.createGroup("sales-dept");
//创建用户alex
identityService.createUser("alex","alex", "Alex", "Miller");
//将alex加入sales-dept组
identityService.createMembership("alex", "sales-dept");
//创建用户joes
identityService.createUser("joes", "joes", "Joe","Smoe");
//将joes加入sales-dept组
identityService.createMembership("joes", "sales-dept");

此任务将会有2个后选择——alex和joes,候选者在处理任务之前,必须先接受任务,这时两个候选者将同时看到任务。

//接受任务

taskService.taskTask(task.getId(),"alex");

此时alex接受了任务后,就会由任务的候选者变为任务的分配者,同时此任务会从所有候选者的任务列表中消失,它会出现在alex的已分配任务列表中。


3>.关于任务分配处理器

任务分配处理器需要实现AssignmentHandler接口

任务分配处理器作为任务活动的一个子元素,名称为assignment-handler。它指向用户代码实现的AssignmentHandler接口。

wKioL1lAAZ7TRZ8cAAAQvmcObcQ778.jpg

对应的jPDL如下:

<process name="concurrency" xmlns="http://jbpm.org/4.4/jpdl">
   <start name="start" g="111,196,48,48">
      <transition to="review"/>
   </start>
   
   <task name="review" g="264,190,92,52">
   	  <!-- AssignTask作为任务分配处理器的实现 -->
   	  <assignment-handler class="test.AssignTask">
   	  	<!-- assignee注入值 -->
   	  	<field name="assignee">
   	  		<string value="alex"/>
   	  	</field>
   	  </assignment-handler>
      <transition to="wait"/>
   </task>
   <state name="wait" g="431,189,92,52"/>
</process>

对应的任务分配处理器:

public class AssignTask implements AssignmentHandler {

	String assignee;
	@Override
	public void assign(Assignable assignable, OpenExecution execution)
			throws Exception {
		//设置任务的分配者
		assignable.setAssignee(assignee);
	}

}

测试代码如下:

//发起流程实例
ProcessInstance processInstance = executionService
		.startProcessInstanceByKey("concurrency");
//alex是通过定义注入的任务分配者
List<Task> taskList = taskService.findPersonalTasks("alex");
//断言alex有一个任务
assertEquals(1, taskList.size());
Task task = taskList.get(0);
//断言任务名称
assertEquals("review", task.getName());
//断言任务的分配者
assertEquals("alex", task.getAssignee());

此流程运行后到任务活动review,当review任务被创建时,AssignTask任务分配处理器被调用,此时已设置alex用户为此任务的分配者,所以alex将在他的个人任务列表中找到这个任务。


4>.关于任务泳道

在实际业务中,流程定义中的多个任务需要被分配或候选给同一个群用户,这个时候可以将"同一群用户"定义为"一个泳道"。

例如:

wKioL1lBI-ySF_4NAAC9hRx12WI419.jpg

在此图中,有三个泳道——申请人主管财务部

wKioL1lBJoPj2uyRAAAQvmcObcQ586.jpg

对应的jPDL文件如下:

<process name="concurrency" xmlns="http://jbpm.org/4.4/jpdl">
   <!-- 定义泳道 -->
   <swimlane name="sales" candidate-groups="sales-dept"/>
   <start g="111,196,48,48" name="start1">
      <transition to="order data"/>
   </start>
   <!-- 分配工作 -->
   <task g="264,190,92,52" name="order data" swimlane="sales">
      <transition to="quote"/>
   </task>
   <task name="quote" swimlane="sales"/>
</process>

在xml中的泳道"sales"引用了一个用户组sales-dept。在流程运行前这个用户组就得需要被创建出来

identityService.createGroup("sales-dept");
//创建用户alex并加入sales-dept组
identityService.createUser("alex", "alex", "Alex","Miller");
identityService.createMembership("alex","sales-dept");

在发起流程后,alex将成为order data的唯一候选者(因为组里只有他一个用户)

//接受任务
taskService.takeTask(taskId, "alex");

接受任务后alex将成为任务的分配者,alex可以通过completeTask API完成任务

//完成任务
taskService.completeTask(taskId);

完成任务后,流程实例会流转到下一个任务"quote",这个任务也引用了泳道sales,因此,任务会直接分配给alex。

验证代码如下:

List<Task> taskList = taskService.findPersonalTasks("alex");
//断言alex直接拿到了任务
assertEquals(1, taskList.size());
Task task = taskList.get(0);
//断言是否为预期的任务和分配者
assertEquals("quote", task.getName());
assertEquals("alex", task.getAssignee());


5>.关于任务变量

任务可以读取、更新流程变量还可以定义任务自由的变量,主要作用是作为任务表单的数据容器——任务表单负责展现来自任务和流程的变量数据

6>.关于任务提醒邮件

jBPM4支持使用电子邮件进行任务提醒,邮件里的内容是根据一个模板生成出来的,默认使用jBPM内置的,可以在process-engine-context指定自定义的模板。


7.sub-process(子流程活动)