工作流引擎:Activiti 进阶
工作流引擎:Spring Boot + Activiti 7 进阶

前言

在上一篇文章中说了Spring Boot + Activiti 7的基本使用,本文章将更深入的带你了解Activiti 7

个人任务

在上一篇文章其实有介绍到关于任务负责人的这个概念(Assignee)。指的时当任务执行到某个节点时,若该节点的负责人为:张三,那么就可以由Assignee查询到张三的代办任务,真实场景,就可以根据负责人来查询并执行任务。

固定分配负责人

固定分配负责人是指:在绘制流程图的时候为每个节点设置固定的任务负责人,如下图:

使用UEL表达式

Activiti 使用 UEL 表达式,UELjava EE6 规范的一部分,UELUnified Expression Language)即统一表达式语言,activiti 支持两个 UEL 表达式:UEL-valueUEL-method

UEL-value

设置Assignee的值为:${userId},这样在任务执行时,只需要把Assignee的值设置到流程变量中,注意:key需要和你设置的表达式中的值一致,比如此处的userId哦。

UEL-value还支持对象导航,比如你可以设置为:${user.userId},这样在设置流程变量的时候,只需要把查询到的user存入流程变量,当执行到该节点时,就可以自动从流程变量的user对象中取到userId的值。

UEL-method

设置Assignee的值为:${userService.getUserId()},其中userServiceSpring管理的Bean,这样在执行该节点时,就会调用userServicegetUserId方法。

UEL-value + UEL-method结合使用

设置Assignee的值为:${userService.getUserId(username)},其中userServiceSpring管理的Beanusername为流程变量,执行到该节点的时候,会自动将username作为参数传入getUserId方法并获取调用结果的值。

注意:使用表达式时,必须保证流程变量或方法存在,否则会导致流程出现异常。

流程变量

流程变量在activiti 中是一个非常重要的角色,流程运转有时需要靠流程变量,业务系统和activiti结合时少不了流程变量,流程变量就是activiti 在管理工作流时根据管理需要而设置的变量。

比如在请假流程流转时如果请假天数大于3 天则由总经理审核,否则由人事直接审核,请假天数就可以设置为流程变量,在流程流转时使用。

注意:虽然流程变量中可以存储业务数据可以通过activitiapi 查询流程变量从而实现查询业务数据,但是不建议这样使用,因为业务数据查询由业务系统负责,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());

// 填写请假单(由于我这里确认只有一个任务,我直接使用singleResult,实际应为list)
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");
// 修改num参数的值是否小于3 可以控制流程的走向
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("总经理暂无任务");
}

}

}
  • 分为四部分:创建流程实例,填写请假单,部门经理审核,总经理审核

测试

  1. 填写请假单,num变量的值为:2

    请假天数 =< 3 天,所以总经理不需要审核

  2. 填写请假单,num变量的值为:5

    请假天数 > 3天,需要总经理审核。

组任务

在流程定义中在任务结点的 assignee 固定设置任务负责人,在流程定义时将参与者固定设置在.bpmn 文件中,如果临时任务负责人变更则需要修改流程定义,系统可扩展性差。针对这种情况可以给任务设置多个候选人,可以从候选人中选择参与者来完成任务。

设置任务候选人

Candidate Users中设置候选人,如有多个,使用英文逗号隔开。

任务办理

  1. 查询候选人的任务

    1
    Task task = taskService.createTaskQuery().processDefinitionKey("qingjia").taskCandidateUser("人事部经理").singleResult();
  2. 将该任务转成个人任务

    1
    2
    // 将该任务分配给 “人事部经理” 处理
    taskService.claim(taskId, "人事部经理");

    注意:该方法调用时需要自行判断权限,一旦调用该方法,任务就会被分配给指定的负责人。

  3. 后面的处理就和普通任务的处理是一致的了。

网关

排他网关

排他网关(也叫异或(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());

// 填写请假单(由于我这里确认只有一个任务,我直接使用singleResult,实际应为list)
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");
// 修改num参数的值是否小于3 可以控制流程的走向
params1.put("num", 2);
// 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("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());

// 填写请假单(由于我这里确认只有一个任务,我直接使用singleResult,实际应为list)
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");
// 修改num参数的值是否小于3 可以控制流程的走向
// params1.put("num", 2);
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());

// 填写请假单(由于我这里确认只有一个任务,我直接使用singleResult,实际应为list)
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");
// 修改num参数的值是否小于3 可以控制流程的走向
params1.put("num", 2);
// 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("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:

文章作者: imxushuai
文章链接: https://www.imxushuai.com/2019/07/06/16.工作流引擎:Activiti-进阶/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 imxushuai
支付宝打赏
微信打赏