工作流引擎:Spring Boot + Activiti 7 进阶
前言
在上一篇文章中说了Spring Boot
+ Activiti 7
的基本使用,本文章将更深入的带你了解Activiti 7
。
个人任务
在上一篇文章其实有介绍到关于任务负责人的这个概念(Assignee)。指的时当任务执行到某个节点时,若该节点的负责人为:张三,那么就可以由Assignee
查询到张三的代办任务,真实场景,就可以根据负责人来查询并执行任务。
固定分配负责人
固定分配负责人是指:在绘制流程图的时候为每个节点设置固定的任务负责人,如下图:
使用UEL表达式
Activiti
使用 UEL
表达式,UEL
是 java EE6
规范的一部分,UEL
(Unified Expression Language)即统一表达式语言,activiti
支持两个 UEL
表达式:UEL-value
和 UEL-method
。
UEL-value
设置Assignee
的值为:${userId}
,这样在任务执行时,只需要把Assignee
的值设置到流程变量中,注意:key需要和你设置的表达式中的值一致,比如此处的userId
哦。
UEL-value还支持对象导航,比如你可以设置为:${user.userId}
,这样在设置流程变量的时候,只需要把查询到的user
存入流程变量,当执行到该节点时,就可以自动从流程变量的user
对象中取到userId
的值。
UEL-method
设置Assignee
的值为:${userService.getUserId()}
,其中userService
为Spring
管理的Bean
,这样在执行该节点时,就会调用userService
的getUserId
方法。
UEL-value + UEL-method结合使用
设置Assignee
的值为:${userService.getUserId(username)}
,其中userService
为Spring
管理的Bean
,username
为流程变量,执行到该节点的时候,会自动将username
作为参数传入getUserId
方法并获取调用结果的值。
注意:使用表达式时,必须保证流程变量或方法存在,否则会导致流程出现异常。
流程变量
流程变量在activiti
中是一个非常重要的角色,流程运转有时需要靠流程变量,业务系统和activiti
结合时少不了流程变量,流程变量就是activiti
在管理工作流时根据管理需要而设置的变量。
比如在请假流程流转时如果请假天数大于3 天则由总经理审核,否则由人事直接审核,请假天数就可以设置为流程变量,在流程流转时使用。
注意:虽然流程变量中可以存储业务数据可以通过activiti
的api
查询流程变量从而实现查询业务数据,但是不建议这样使用,因为业务数据查询由业务系统负责,activiti
设置流程变量是为了流程执行需要而创建,否则会造成业务和activiti
耦合过重。
使用流程变量控制流程
绘制流程图
在部门经理审批的连线上设置UEL表达式,控制流程的走向。
这里为了我后面测试方便,我分别在填写请假单
,部门经理审批
,总经理审批
添加了任务负责,这样在就可以使用任务负责人获取对应节点的任务了,对应如下:
- 填写请假单:员工
- 部门经理审批:部门经理
- 总经理审批:总经理
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| package com.imxushuai;
import lombok.extern.slf4j.Slf4j; import org.activiti.engine.*; import org.activiti.engine.runtime.ProcessInstance; import org.activiti.engine.task.Task; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner;
import java.util.Date; import java.util.HashMap; import java.util.Map;
@Slf4j @RunWith(SpringRunner.class) @SpringBootTest public class ActivitiDemo2Test {
@Autowired private ProcessEngine processEngine;
private RuntimeService runtimeService; private TaskService taskService; private RepositoryService repositoryService; private HistoryService historyService;
@Before public void init() { runtimeService = processEngine.getRuntimeService(); taskService = processEngine.getTaskService(); repositoryService = processEngine.getRepositoryService(); historyService = processEngine.getHistoryService(); }
@Test public void start() { ProcessInstance qingjia = runtimeService.startProcessInstanceByKey("qingjia"); log.info("请假流程已开启:流程ID:[{}]", qingjia.getId());
Task task = taskService.createTaskQuery().processDefinitionKey("qingjia").taskAssignee("员工").singleResult(); if (task != null) { log.info("请填写请假单:任务ID:[{}]", task.getId()); Map<String, Object> params1 = new HashMap<>(); params1.put("name", "xushuai"); params1.put("num", 2); params1.put("date", new Date()); params1.put("remark", "回家有事");
taskService.complete(task.getId(), params1); log.info("请假单填写完毕,请假单内容:{}", params1.toString()); }
task = taskService.createTaskQuery().processDefinitionKey("qingjia").taskAssignee("部门经理").singleResult(); if (task != null) { Map<String, Object> variables = taskService.getVariables(task.getId()); log.info("请部门经理审批请假单,请假人:[{}], 请假天数:[{}]天", variables.get("name"), variables.get("num"));
taskService.complete(task.getId()); log.info("部门经理审核通过"); }
task = taskService.createTaskQuery().processDefinitionKey("qingjia").taskAssignee("总经理").singleResult(); if (task != null) { Map<String, Object> variables = taskService.getVariables(task.getId()); log.info("请总经理审核请假单,请假人:[{}], 请假天数:[{}]天", variables.get("name"), variables.get("num"));
taskService.complete(task.getId()); log.info("总经理审核通过"); } else { log.info("总经理暂无任务"); }
}
}
|
- 分为四部分:创建流程实例,填写请假单,部门经理审核,总经理审核
测试
填写请假单,num
变量的值为:2
请假天数 =< 3 天,所以总经理不需要审核
填写请假单,num
变量的值为:5
请假天数 > 3天,需要总经理审核。
组任务
在流程定义中在任务结点的 assignee
固定设置任务负责人,在流程定义时将参与者固定设置在.bpmn
文件中,如果临时任务负责人变更则需要修改流程定义,系统可扩展性差。针对这种情况可以给任务设置多个候选人,可以从候选人中选择参与者来完成任务。
设置任务候选人
在Candidate Users
中设置候选人,如有多个,使用英文逗号隔开。
任务办理
查询候选人的任务
1
| Task task = taskService.createTaskQuery().processDefinitionKey("qingjia").taskCandidateUser("人事部经理").singleResult();
|
将该任务转成个人任务
1 2
| taskService.claim(taskId, "人事部经理");
|
注意:该方法调用时需要自行判断权限,一旦调用该方法,任务就会被分配给指定的负责人。
后面的处理就和普通任务的处理是一致的了。
网关
排他网关
排他网关(也叫异或(XOR)网关,或叫基于数据的排他网关),用来在流程中实现决策。当流程执行到这个网关,所有分支都会判断条件是否为true,如果为true 则执行该分支,注意,排他网关只会选择一个为true 的分支执行。(即使有两个分支条件都为true,排他网关也会只选择一条分支去执行)
不使用排他网关也可以实现流程的控制。
在连线上使用流程变量判断控制。
缺点:如果连线上的条件都不满足,流程将直接结束,在下面的效果就是,直接填写完请假单,就请假成功了。这显然不符合实际的情况。
使用排他网关流程图
使用排他网关,必须有一个条件为true
,否则系统会抛出异常,我们可以通过捕获异常获知晓情况。
建议使用排他网关。
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
|
@Test public void testExclusiveGateway() { ProcessInstance qingjia = runtimeService.startProcessInstanceByKey("exclusiveGateway_qingjia"); log.info("请假流程已开启:流程ID:[{}]", qingjia.getId());
Task task = taskService.createTaskQuery().processDefinitionKey("exclusiveGateway_qingjia").taskAssignee("员工").singleResult(); if (task != null) { log.info("请填写请假单:任务ID:[{}]", task.getId()); Map<String, Object> params1 = new HashMap<>(); params1.put("name", "xushuai"); params1.put("num", 2);
params1.put("date", new Date()); params1.put("remark", "回家吃饭");
taskService.complete(task.getId(), params1); log.info("请假单填写完毕,请假单内容:{}", params1.toString()); }
task = taskService.createTaskQuery().processDefinitionKey("exclusiveGateway_qingjia").taskAssignee("部门经理").singleResult(); if (task != null) { Map<String, Object> variables = taskService.getVariables(task.getId()); log.info("请部门经理审批请假单,请假人:[{}], 请假天数:[{}]天", variables.get("name"), variables.get("num"));
taskService.complete(task.getId()); log.info("部门经理审核通过"); }
task = taskService.createTaskQuery().processDefinitionKey("exclusiveGateway_qingjia").taskAssignee("总经理").singleResult(); if (task != null) { Map<String, Object> variables = taskService.getVariables(task.getId()); log.info("请总经理审核请假单,请假人:[{}], 请假天数:[{}]天", variables.get("name"), variables.get("num"));
taskService.complete(task.getId()); log.info("总经理审核通过"); } else { log.info("总经理暂无任务"); }
}
|
测试结果
并行网关
并行网关允许将流程分成多条分支,也可以把多条分支汇聚到一起,并行网关的功能是基于进入和外出顺序流的:
- fork 分支:
并行后的所有外出顺序流,为每个顺序流都创建一个并发分支。
- join 汇聚:
所有到达并行网关,在此等待的进入分支,直到所有进入顺序流的分支都到达以后,流程就会通过汇聚网关。
注意:如果同一个并行网关有多个进入和多个外出顺序流,它就同时具有分支和汇聚功能。这时,网关会先汇聚所有进入的顺序流,然后再切分成多个并行分支。
与其他网关的主要区别是,并行网关不会解析条件。即使顺序流中定义了条件,也会被忽略。
并行网关的分支必须全部执行完毕才会走下一流程节点,如上图的“人事存档”和“行政考勤”,必须两个都完成,流程才会结束。
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
|
@Test public void testParallelGateway() { ProcessInstance qingjia = runtimeService.startProcessInstanceByKey("parallelGateway_qingjia"); log.info("请假流程已开启:流程ID:[{}]", qingjia.getId());
Task task = taskService.createTaskQuery().processDefinitionKey("parallelGateway_qingjia").taskAssignee("员工").singleResult(); if (task != null) { log.info("请填写请假单:任务ID:[{}]", task.getId()); Map<String, Object> params1 = new HashMap<>(); params1.put("name", "xushuai");
params1.put("num", 5); params1.put("date", new Date()); params1.put("remark", "回家吃饭");
taskService.complete(task.getId(), params1); log.info("请假单填写完毕,请假单内容:{}", params1.toString()); }
task = taskService.createTaskQuery().processDefinitionKey("parallelGateway_qingjia").taskAssignee("部门经理").singleResult(); if (task != null) { Map<String, Object> variables = taskService.getVariables(task.getId()); log.info("请部门经理审批请假单,请假人:[{}], 请假天数:[{}]天", variables.get("name"), variables.get("num"));
taskService.complete(task.getId()); log.info("部门经理审核通过"); }
task = taskService.createTaskQuery().processDefinitionKey("parallelGateway_qingjia").taskAssignee("总经理").singleResult(); if (task != null) { Map<String, Object> variables = taskService.getVariables(task.getId()); log.info("请总经理审核请假单,请假人:[{}], 请假天数:[{}]天", variables.get("name"), variables.get("num"));
taskService.complete(task.getId()); log.info("总经理审核通过"); } else { log.info("总经理暂无任务"); }
task = taskService.createTaskQuery().processDefinitionKey("parallelGateway_qingjia").taskAssignee("人事").singleResult(); if (task != null) { Map<String, Object> variables = taskService.getVariables(task.getId()); log.info("人事审核,请假人:[{}], 请假天数:[{}]天", variables.get("name"), variables.get("num"));
taskService.complete(task.getId()); log.info("人事审核通过"); }
task = taskService.createTaskQuery().processDefinitionKey("parallelGateway_qingjia").taskAssignee("人事").singleResult(); if (task != null) { Map<String, Object> variables = taskService.getVariables(task.getId()); log.info("人事存档,请假人:[{}], 请假天数:[{}]天", variables.get("name"), variables.get("num"));
taskService.complete(task.getId()); log.info("人事存档完毕"); }
task = taskService.createTaskQuery().processDefinitionKey("parallelGateway_qingjia").taskAssignee("行政").singleResult(); if (task != null) { Map<String, Object> variables = taskService.getVariables(task.getId()); log.info("行政考勤,请假人:[{}], 请假天数:[{}]天", variables.get("name"), variables.get("num"));
taskService.complete(task.getId()); log.info("行政考勤记录完毕"); }
}
|
测试结果
请假天数为:5,总经理需要审批。人事存档和行政考勤也正确获取到任务。
包含网关
包含网关可以看做是排他网关和并行网关的结合体。和排他网关一样,你可以在外出顺序流上定义条件,包含网关会解析它们。但是主要的区别是包含网关可以选择多于一条顺序流,这和并行网关一样。
包含网关的功能是基于进入和外出顺序流的:
- 分支:
所有外出顺序流的条件都会被解析,结果为true 的顺序流会以并行方式继续执行,会为每个顺序流创建一个分支。
- 汇聚:
所有并行分支到达包含网关,会进入等待状态,直到每个包含流程token 的进入顺序流的分支都到达。这是与并行网关的最大不同。换句话说,包含网关只会等待被选中执行了的进入顺序流。在汇聚之后,流程会穿过包含网关继续执行。、
若不设置条件,默认为true
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
|
@Test public void testInclusiveGateway() { ProcessInstance qingjia = runtimeService.startProcessInstanceByKey("inclusiveGateway_qingjia"); log.info("请假流程已开启:流程ID:[{}]", qingjia.getId());
Task task = taskService.createTaskQuery().processDefinitionKey("inclusiveGateway_qingjia").taskAssignee("员工").singleResult(); if (task != null) { log.info("请填写请假单:任务ID:[{}]", task.getId()); Map<String, Object> params1 = new HashMap<>(); params1.put("name", "xushuai"); params1.put("num", 2);
params1.put("date", new Date()); params1.put("remark", "回家吃饭");
taskService.complete(task.getId(), params1); log.info("请假单填写完毕,请假单内容:{}", params1.toString()); }
task = taskService.createTaskQuery().processDefinitionKey("inclusiveGateway_qingjia").taskAssignee("部门经理").singleResult(); if (task != null) { Map<String, Object> variables = taskService.getVariables(task.getId()); log.info("请部门经理审批请假单,请假人:[{}], 请假天数:[{}]天", variables.get("name"), variables.get("num"));
taskService.complete(task.getId()); log.info("部门经理审核通过"); }
task = taskService.createTaskQuery().processDefinitionKey("inclusiveGateway_qingjia").taskAssignee("总经理").singleResult(); if (task != null) { Map<String, Object> variables = taskService.getVariables(task.getId()); log.info("请总经理审核请假单,请假人:[{}], 请假天数:[{}]天", variables.get("name"), variables.get("num"));
taskService.complete(task.getId()); log.info("总经理审核通过"); } else { log.info("总经理暂无任务"); }
task = taskService.createTaskQuery().processDefinitionKey("inclusiveGateway_qingjia").taskAssignee("人事").singleResult(); if (task != null) { Map<String, Object> variables = taskService.getVariables(task.getId()); log.info("人事审核,请假人:[{}], 请假天数:[{}]天", variables.get("name"), variables.get("num"));
taskService.complete(task.getId()); log.info("人事审核通过"); }
task = taskService.createTaskQuery().processDefinitionKey("inclusiveGateway_qingjia").taskAssignee("行政").singleResult(); if (task != null) { Map<String, Object> variables = taskService.getVariables(task.getId()); log.info("行政考勤,请假人:[{}], 请假天数:[{}]天", variables.get("name"), variables.get("num"));
taskService.complete(task.getId()); log.info("行政考勤记录完毕"); }
}
|
测试结果
因为请假天数为:2,所以不需要总经理审批。
代码获取
Github: