长大后想做什么?做回小孩!

0%

Camunda工作流引擎三

本篇继续拓宽对 Camunda 工作流的学习!

《Camunda 工作流引擎一》

《Camunda 工作流引擎二》

提要

本文仍然在前文的 Demo 基础上进行!

a70tbV.png

正文

组任务

在一个流程中一个用户任务,有时可以被多个人中的任意一个审批即可,例如:请假流程的部门审批这个用户任务中,只要部门经理或者部门副经理其中的任意一个人审批通过即可。

这时候就需要候选人机制,首先在流程模型中给该用户任务设置候选人列表,这样用户任务就变成了组任务

a70mHf.png

重新部署流程定义,启动一条流程,执行 input 用户任务:

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
/**
* 开启一个流程实例
*/
@Test
public void runProcinst(){
Map<String,Object> params = new HashMap<>();
params.put("employee","zhangsan");
params.put("leave",new Leave("NO00001","休假",new Date()));
params.put("days",2);
ProcessInstance holiday = runtimeService.startProcessInstanceByKey("holiday",params);
System.out.println(holiday.getProcessDefinitionId());
System.out.println(holiday.getId());
System.out.println(holiday.getProcessInstanceId());
}

/**
* 流程任务执行
*/
@Test
public void taskComplete(){
//目前只有一个任务,业务中根据场景选择其他合适的方式
Task task = taskService.createTaskQuery()
.taskAssignee("zhangsan")
.singleResult();
taskService.complete(task.getId());
}

此时 approve1 用户任务被创建,可以在 act_ru_task 表中看到这个任务的 assignee 是空的,同时可以观察到 act_ru_identitylink 表中有两个 type 为 candidate 的记录(user_id 是 zhangsan 和 lisi),需要根据某个候选人去查询到他的代办任务,但是并不能像之前仅有一个 assignee 的时候那样直接去 complete 自己的待办任务,因为每个候选人都可以看到这个组任务,如果某个候选人准备执行(审批)这个查到的代办组任务,则需要先拾取(claim)这个组任务,将其变成自己的个人任务后执行:

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
/**
* 候选人拾取任务
*/
@Test
public void claimTask() {
//查询候选人的代办任务
Task task = taskService.createTaskQuery()
.processDefinitionKey("holiday")
.taskCandidateUser("lisi")
.singleResult();
//打印任务实例Id和assignee
System.out.println(task.getId());
System.out.println(task.getAssignee());//null
taskService.claim(task.getId(),"lisi");
}

/**
* 流程任务执行
*/
@Test
public void taskComplete(){
//已经变成了 lis 的个人任务
Task task = taskService.createTaskQuery()
.taskAssignee("lisi")
.singleResult();
taskService.complete(task.getId());
}

如果某个候选人拾取组任务后,又不想执行了,还可以退还这个个人任务,此时个人任务会重新变成组任务,每个候选人都可以查到并去拾取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 候选人返还任务
*/
@Test
public void sendBackTask() {
//查询候选人的代办任务
Task task1 = taskService.createTaskQuery()
.processDefinitionKey("holiday")
.taskCandidateUser("lisi")
.singleResult();
System.out.println(task1);//null
//查询候选人的代办任务
Task task2 = taskService.createTaskQuery()
.processDefinitionKey("holiday")
.taskAssignee("lisi")
.singleResult();
//打印任务实例Id和assignee
System.out.println(task2.getId());
System.out.println(task2.getAssignee());//lisi
//退还任务,就是将assignee重新设置为null
taskService.setAssignee(task2.getId(),null);
}

从退还任务发现可以主动去设置 assignee ,那么也可以使用同样的方法去完成任务的转移(交接/交付):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 主动交接/转移任务
*/
@Test
public void handOverTask() {
//查询候选人的代办任务
Task task = taskService.createTaskQuery()
.processDefinitionKey("holiday")
.taskCandidateUser("lisi")
.singleResult();
System.out.println(task.getId());
System.out.println(task.getAssignee());//null
//将assignee重新设置为laowang
taskService.setAssignee(task.getId(),"laowang");
}

此时 act_ru_task 表中对应的任务实例中的 assignee 字段就变成了 laowang,执行任务推进流程直至结束后,观察 act_hi_identitylink 表:

a70uE8.png

TYPE_ 出现了两种:assignee 是任务的参与者、candidate 是任务的候选人,其中 wangwu 也是任务的候选人,但是其并不是任务的参与者;OPERATION_TYPE_ 也出现了两种:add 用户作为任务的 assignee 加入,delete 用户退还任务(其中 lisi 在任务创建时是 candidate,然后拾取任务,再退换任务)。

网关

排他网关

在之前设想过一种情况:请假时间<=3的时候只需要部门审批(approve1),当请假时间>3的时候还需要人事总监审批。在没有使用排他网关的时候,只是在两条顺序流上添加了条件表达式,当表达式结果是 true 的时候,才会走对应的分支:

dVFSN6.png

但是如果上面这种方式出现两条顺序流上的表达式都为 true ,则两条分支都会进行,从而违背我们的设计初衷。这是就需要引入我们的排他网关:

dVizAx.png

此时,由于排他网关的存在,即使两条分支条件都成立,也只会走其中的一条分支。

排他网关还可以接受进入的顺序流,只要有顺序流进入网关,就会立刻通过。

PS:引入排他网关后,如果两条分支的结果都为 false,则会抛出异常。

并行网关

并行网关主要有分支和汇聚的功能。

分支:并行所有分支出去的顺序流。PS:即使并行网关分支出去的顺序流上有条件表达式,也会被忽略掉。

汇聚:所有到达并行网关的顺序流,都要在此等待,直到所有的顺序流分支都到达并行网关之后才可以通过。

例如:

dVivH1.png

存储请假单信息后,并行去存储考勤信息和计算工资,当两个任务都完成后、都汇聚到并行网关后再执行到结束事件。

包含网关

包含网关更像是排他网关和并行网关的结合体。它也有分支和汇聚两个功能,同时它也会去执行顺序流上的表达式,只有当分支出的顺序流上的表达式为 true 的时候,才会去执行该顺序流。

同样的,包含网关在汇聚时,也只需要等待条件为 true 被执行的分支都汇聚后,就可以继续执行。

例如:

dViLjJ.png

存储请假单信息后,正式员工请假不扣工资就不需要去计算工资,只需要去计算考勤即可;实习生按天算工资,所以请假后需要去执行计算工资;如果是正式员工则执行完计算考勤后到达第二个包含网关后直接继续执行;如果是实习生则需要去执行计算考勤和计算工资,当两个任务都执行完汇聚到第二个包含网关后,才能继续执行到结束事件。

事件网关

之前的网关在控制出口顺序流的时候,通常是以顺序流定义的条件表达式的结果去进行选择的。而事件网关,则是以事件作为驱动,通过捕获中间事件来选择出口顺序流的。

当进行之事件网关时执行被暂停并创建一个事件订阅,等待某个中间事件被触发后流程会继续沿着该事件的顺序流继续执行,一个事件网关只会创建一个事件订阅,所以只会选择第一个触发的中间事件继续执行。

一个事件网关至少有两条出口顺序流,且事件网关只能连接至 intermediateCatchEvent(捕获中间事件) 类型的元素:

dViXu9.png

例如:

dVijBR.png

事件网关两条出口顺序流分别连接到 TimerIntermediateCatchEvent(定时器中间事件) 元素:

dViqc4.png

PS:Timer Definition 的值需要遵循 ISO 8086 标准中的表示方法。

和一个 SignalIntermediateCatchEvent(信号中间事件) 元素:

dVib3F.png

为了便于观察流程的走向,两个捕获中间事件元素后面又连接了一个用户任务,分别设置了执行监听器和任务监听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TimerEventListener implements TaskListener, ExecutionListener {
@Override
public void notify(DelegateExecution delegateExecution) throws Exception {
System.out.println("定时器事件的ExecutionListener触发了!");
}

@Override
public void notify(DelegateTask delegateTask) {
System.out.println("定时器事件的TaskListener触发了!");
}
}

public class SignalEventListener implements TaskListener, ExecutionListener {
@Override
public void notify(DelegateExecution delegateExecution) throws Exception {
System.out.println("信号事件的ExecutionListener触发了!");
}

@Override
public void notify(DelegateTask delegateTask) {
System.out.println("信号事件的TaskListener触发了!");
}
}

接下来就是部署流程定义,启动一个流程实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void runProcinst(){
Map<String,Object> params = new HashMap<>();
// 30秒
params.put("duration","P0Y0M0DT0H0M30S");
ProcessInstance holiday = runtimeService.startProcessInstanceByKey("eventGateway",params);
System.out.println(holiday.getProcessDefinitionId());
System.out.println(holiday.getId());
System.out.println(holiday.getProcessInstanceId());
//为了观察控制台效果
try {
Thread.sleep(10000000000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

观察 act_ru_job 表中,timer 已经被创建了,等待 30 秒后,控制台打印出:

1
2
定时器事件的ExecutionListener触发了!
定时器事件的TaskListener触发了!

并且 act_ru_task 表中,已经创建出 “ 定时器事件触发了 ”用户任务了,此时 timer 也从 act_ru_job 表中删除了。

再次启动一条流程,观察 act_ru_job 表中,timer 已经被创建了,测试触发信号中间事件

1
2
3
4
5
6
7
8
9
10
11
12
/**
*触发信号中间事件
*/
public void signalEvent(){
List<Execution> list = runtimeService.createExecutionQuery()
.processInstanceId("b2be29c3-dfab-11ea-aa93-98fa9b4e8fcb")
.signalEventSubscriptionName("nextSignal")
.list();
for (Execution execution : list) {
runtimeService.signalEventReceived("nextSignal",execution.getId());
}
}

控制台打印出:

1
2
信号事件的ExecutionListener触发了!
信号事件的TaskListener触发了!

act_ru_task 表中,已经创建出 “ 信号事件触发了 ”用户任务,此时 act_ru_job 表中的 timer 也被删除了。

SpringBoot整合

在第一篇文章《Camunda 工作流引擎一》中就已经详细记录了如何启动一个最简单的 SpringBoot + Camunda 的项目了。

在前文的所有测试方法方法中,也不难看出已经整合入了 SpringBoot ,所以就不需要采用 下面这种方式去获取各种 Service 实例:

1
2
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
RuntimeServiceImpl runtimeService = processEngine.getRuntimeService();

而是交由 Spring 容器管理,采用 @Autowired 的方法自动注入,这也是业务中更加常用的方式。


菜鸟本菜,不吝赐教,感激不尽!

更多题解源码和学习笔记:githubCSDNM1ng