一、流程
在JPDL中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活动。
示例:
对应的JPDL如下:
根据此流程定义发起实例:
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 stateassertNotNull(executionInB);//继续触发processInstance = executionService.signalExecutionById(executionInB.getId());//...c
在state活动里可以定义多个transition元素,通过触发指定转移路径的名称,可以选择其中的一个transition通过。
示例:
对应的JPDL如下:
要想该流程运行起来,则需要:
ProcessInstance processInstance = executionService.startProcessInstanceByKey("stateChoice");//假设该流程到达了wait for response,则该实例会一直等待外部触发的出现//获得流程实例的IDString 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。
对应的JPDL:
测试代码如下:
Mapvariables = new HashMap<>(); variables.put("content", "good");//发起流程实例并传入流程变量ProcessInstance processInstance = executionService .startProcessInstanceById("decisionCondition", variables);//断言assertTrue(processInstance.isActive("submit doc"));
2>使用decision活动的expr属性
expr属性来判断流程的转向,需要指定流转的路径
对应的JPDL如下:
单元测试如下(同上):
Mapvariables = 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活动的子元素进行配置。
示例:
对应的jPDL如下:
对应的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"; }}
对应的单元测试如下:
Mapvariables = 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则可以使流程的并发分支聚合成一条主干。
对应的jPDL:
单元测试执行上述流程定义:
//发起流程实例ProcessInstance processInstance = executionService .startProcessInstanceByKey("concurrency");String pid = processInstance.getId();//构造一个活动集合以验证分支Setactivitys = 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 truckactivitys.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活动的流程实例中仍然活跃的流程活动将会被保留继续执行。
简单流程定义:
对应的jPDL:
单元测试如下:
//发起流程实例ProcessInstance processInstance = executionService .startProcessInstanceByKey("concurrency");//创建流程实例后,直接断言其结束assertTrue(processInstance.isEnded());
复杂一些的end活动
一个流程定义可以有多个end活动,以便通过事件机制触发不同的结束方式。
测试:
//发起流程实例ProcessInstance processInstance = executionService .startProcessInstanceByKey("concurrency");String executionId = processInstance.getId();//指定转移名称:400processInstance = executionService.signalExecutionById(executionId,"400");//断言流程实例结束assertTrue(processInstance.isEnded());
在实际应用中,为了表明流程实例的结束状态,可以利用end活动的state属性标识 或者利用jBPM4提供的特殊end活动:end-cancel活动和end-error活动。
例如:
对应的jPDL:
单元测试:测200的转移实例
//发起流程实例ProcessInstance processInstance = executionService .startProcessInstanceByKey("concurrency");String executionId = processInstance.getId();//指定转移名称:400processInstance = executionService.signalExecutionById(executionId,"200");//断言流程实例的状态与预期符合,state属性值为completedassertEquals("completed", processInstance.getState());assertTrue(processInstance.isEnded());
6.task(人工任务活动)
用来处理涉及人机交互的活动。
1>关于任务的分配者
assignee属性将一个任务分配给指定的用户。
对应的jPDL:
注意: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; }}
基于此定义进行测试:
Mapvars = new HashMap<>();vars.put("order", new Order("alex"));//发起流程实例ProcessInstance processInstance = executionService .startProcessInstanceByKey("concurrency",vars);//用户alex可通过findPersonalTasks获得List taskList = taskService.findPersonalTasks("alex");
2.关于任务的候选者
jBPM支持将任务分配给一组候选用户,组内的一个用户可以接受这个任务并完成。
对应的jPDL如下:
流程实例发起后,任务review会被创建,但是不会显示在任何人的个人任务列表中,因为还没有创建sales-dept组。可以通过taskService.findGroupTasks来获取。
//首先创建sales-dept组identityService.createGroup("sales-dept");//创建用户alexidentityService.createUser("alex","alex", "Alex", "Miller");//将alex加入sales-dept组identityService.createMembership("alex", "sales-dept");//创建用户joesidentityService.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接口。
对应的jPDL如下:
对应的任务分配处理器:
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是通过定义注入的任务分配者ListtaskList = 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>.关于任务泳道
在实际业务中,流程定义中的多个任务需要被分配或候选给同一个群用户,这个时候可以将"同一群用户"定义为"一个泳道"。
例如:
在此图中,有三个泳道——申请人、主管、财务部。
对应的jPDL文件如下:
在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。
验证代码如下:
ListtaskList = 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(子流程活动)