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

0%

Camunda工作流引擎二

本篇将在上一篇应用的基础上开发一个最基本的 ”流程Demo“ !

提要

简单总结一下《Camunda工作流引擎一》中关于表的知识,大体上分为 4 类:

  1. act_RE_* :RE 表示 repository,这个前缀的表包含了流程定义和流程静态资源(图片,规则,等等)。
  2. *act_RU_\ **:RU 表示 runtime,这些运行时的表,包含流程实例、任务、变量、异步任务等运行中的数据。只在流程实例执行过程中保存这些数据,在流程结束时就会删除这些记录,这样来尽量使运行时表可以一直很小速度很快。
  3. act_HI_* :HI 表示 history,这些表包含历史数据,比如历史流程实例、遍历、任务等等。
  4. act_GE_* :GE 表示 general,通用数据,用于不同场景下。

这些表存储了流程相关的各种信息,那么我们如何去对其进行CRUD呢?按照传统的思路,那自然是写 domain、dao、service 去进行对上述表的操作。

但是,实际上 Camunda 已经将上述表的操作给我们封装好了:

UlRP9f.png

可以通过流程引擎去获取到各种 Service,从而进行一些流程操作:RepositoryService,资源管理类;RuntimeService,流程运行管理类;TaskService,任务管理类;HistoryService,历史管理类;ManagerService,引擎管理类等等。。。(在 Spring 项目中更多的是将这些 Service 也交由 Spring 容器进行管理控制)

本篇将按照下述的开发第一个 Camunda 流程Demo 的基本步骤来进行:

  1. 整合 Camunda(添加依赖)。

  2. 完善必要的配置。

  3. 实现业务流程建模(绘制 BPMN )。

    工具下载地址: https://camunda.com/download/modeler/

  4. 部署流程定义。

  5. 启动流程实例。

  6. 查询代办任务。

  7. 处理代办任务。

  8. 结束流程。

1、2两个步骤,在上一篇中已经完成了,这两部也是启动应用、创建相关表的先行条件。以公司请假审批流程为例,直接从第 3 步开始!由简到繁!

绘制业务流程

先看一下绘制好的样子:

U5Pwk9.png

首先创建一个开始事件U5SZVA.png)和一个结束事件U5SeUI.png,可以修改两个事件的 id 和 name,或者不作任何修改。

接下来就是创建 Task,然后点击右侧的”小扳手”设置为用户任务,可以根据需要修改用户任务的 Id、Name 和 Version Tag,或者不做修改使用默认,重点是 assignee 这个就是任务的处理人,可以有三种方式进行设置:

  1. 直接字符串写死

  2. UEL-value,是 Java EE6 规范的一部分,写法:${变量名} 或者 ${对象.属性名},本篇也将采用这种形式进行设置,以”员工输入”任务(input)为例:

    U5iUjf.png

    如何传这个变量呢?后面运行流程实例的时候会再次提到!

  3. UEL-Method,写法:${bean.getId()}、${xxxService.findIdByName(empName)},bean 是 Spring 容器中的一个 bean,调用这个 bean 的 getId() 方法,xxxService 也是 Spring 容器中的一个 bean,其中 empName 是流程变量,empName 作为参数传到 findIdByName() 方法中。

用同样的方法创建另外两个用户任务:approve1(部门审批) 和 approve2(人事审批)。

最后使用最后将事件和任务按照顺序连接起来(顺序流)。

将文件另存为 holiday.bpmn。

PS:bpmn 文件本质上是 xml 配置文件,只是我们的工具将其解析为了这样的图形样式,去方便使用者去进行业务流程模型的创建。

部署流程定义

定义好了 bpmn 文件,就可以将其进行部署了!首先在《Camunda工作流引擎一》中那个 SpringBoot 项目的资源路径下建一个 BPMN 文件夹,将我们的 bpmn 文件 copy 进去:

UbJpl9.png

然后就是使用我们开头说的各种工作流已经帮我们定义好的 Service 去进行流程部署操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Autowired
RepositoryService repositoryService;

/**
* 流程定义部署
*/
@Test
void deploy() {
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("BPMN/holiday.bpmn")
.deploy();
//部署Id
System.out.println(deploy.getName());//84c0a64c-cc1c-11ea-85a7-98fa9b4e8fcb
}

除了这种最简单的部署方式之外,还可以同时部署 .bpmn 文件对应的 .png 图片、压缩包的方式进行部署等等。。。可以根据业务的需求选择合适的方式。

部署完成后可以去观察数据库表的变化:act_re_deployment 中会新增本次的部署信息、act_re_procdef流程定义的一些信息、act_ge_bytearray 部署的资源文件。。

PS:流程定义和流程实例,很像是 Java 中类定义和 对象(实例) 的关系。

启动流程实例

有了流程定义,我们就可以去启动对应的流程实例!同时会演示“绘制业务流程”时留下的问题:如何给 assignee 中的 UEL-value 表达式进行传参!

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 开启一条流程,并给用户任务的 assignee 赋值
*/
@Test
public void startProcessInstanceWithAssignee(){
Map<String,Object> map = new HashMap<>();
map.put("employee","zhangsan");
map.put("deptment","lisi");
map.put("personal","wangwu");
ProcessInstance holiday = runtimeService.startProcessInstanceByKey("holiday", map);
//流程实例Id
System.out.println(holiday.getProcessInstanceId());//c3156a0d-cc28-11ea-adb8-98fa9b4e8fcb
}

一个流程实例启动后,可以去观察 act_hi_actinst 表中记录了每个任务的相关信息(审批人、开始时间、结束时间等等)、act_hi_identitylink 表中记录了流程实例的参与者、act_hi_procinst 表中是流程实例的信息、act_hi_tastinst 记录了流程实例中每个任务实例的信息、act_ru_identitylink 表中记录了运行时的流程实例当前的参与者、act_ru_tast 表记录了当前运行中的任务、act_ru_variable 表中记录了设置的参数、等等。。。

流程变量

在业务流程推进的过程中,可能会涉及到很多流程变量。

Camunda 支持的变量类型不限于:String、int、short、long、double、date、binary、serializable(Java 对象需要去实现序列化接口,并生成 serialVersionUID) 等等。。

流程变量的作用域默认是一个流程实例(从流程启动到流程结束),一个流程实例是一个流程变量最大范围的作用域,所以被称为 global 变量;作用域也可以是一个任务(Task)或者一个执行实例(execution),所以被称为 local 变量。

PS:global 变量中变量名不允许重复,设置相同的变量名,后设置的值会覆盖先设置的值。local 变量和 global 变量可以变量名相同,没有影响。

设置流程变量的方法也有多种:

  1. 启动流程时设置变量:

    1
    2
    3
    4
    5
    6
    //键值对形式的变量,值也可以是实现了 serializable 接口的对象
    Map<String,Object> params = new HashMap<>();
    params.put("xxx","xxx");
    params.put("xxx","xxx");
    ...
    ProcessInstance holiday = runtimeService.startProcessInstanceByKey("holiday",params);
  2. 完成任务时设置变量:

    1
    2
    //依然是键值对形式的 params
    taskService.complete(task.getId(),params);
  3. 设置global/local变量:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Map params = new HashMap();
    params.put("days",3);
    params.put("type","休假");
    ...
    //设置global变量
    runtimeService.setVariable("excutionId","key","value");
    runtimeService.setVariables("excutionId",params);
    //设置local变量
    runtimeService.setVariableLocal("excutionId","key","value");
    runtimeService.setVariablesLocal("excutionId",params);

任务监听器

除此之外 Camunda 还可以给每个任务节点设置监听器

UbIoUx.png

就以 ”用户输入“ 这个用户任务为例,为其设置一个任务监听器:

am6OxO.png

既然设置了 Java Class 类型的监听器,那么就需要实现一个监听器:

1
2
3
4
5
6
public class UserListener implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
System.out.println("给用户发一个消息!");
}
}

监听器需要去实现 TaskListener 接口,并重写其 notify() 方法,之后当这个任务节点被创建的时候,就会去执行这个任务监听器中的 notify() 方法。

PS:执行监听器使用方法也类似,需要实现 org.camunda.bpm.engine.delegate.ExecutionListener 接口。

一个示例

aR9vb8.png

依然是请假流程,除了开始事件和结束事件,还有 input(用户任务—员工输入)、approve1(用户任务—部门审批)、approve2(用户任务—人事审批),以及一个系统任务—数据同步方法(模拟保存请假信息)。

给两个审批人都添加了监听器,当其任务创建时,会调用监听器的 notify() 方法给予 assignee 一个模拟的通知:

aR9jDf.png

1
2
3
4
5
6
public class ApproverNotice implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
System.out.println("给"+ delegateTask.getAssignee() +"发一个消息!");
}
}

如果员工请假天数少于等于 3 天,则只需要部门审批(approve1)之后即可审批结束保存数据,如果请假天数大于 3 天,则还需要人事审批(approve2)之后才能完成审批保存数据:

aR9XKP.png

上图是其中的一条连线(序列流)。

当审批结束,则进入到系统任务(autoSync)保存请假信息:

aR9zVS.png

1
2
3
4
5
6
public class AutoSync implements JavaDelegate {
@Override
public void execute(DelegateExecution delegateExecution) throws Exception {
System.out.println("存储请假信息!!!!");
}
}

这只是一个及其简化的示例,可以在每个用户任务后面添加排他网关,审批通过则继续,驳回时则直接到结束事件,只有当用户任务都审批通过才到 autoSync 任务。

部署上述流程后,就可以开始启动一条流程,观察表中的变化:

1
2
3
4
5
6
7
8
9
10
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());
}

启动流程后,可以在 act_hi_procinst 表中看到开启的流程实例, act_ru_task 和 act_hi_taskinst 表中看到正在运行中的用户任务实例,act_ru_variable、act_ru_varinst 和 act_hi_detail 表中找到设置的参数(其中 Leave 对象还有其对应的 bytearray_id_ 可以在 act_ge_bytearray 中找到其更具体的参数内容)

完成当前任务,向前推进:

1
2
3
4
5
6
7
8
9
10
public void taskComplete(){
//目前zhangsan只有一个任务,业务中根据场景选择其他合适的方式
Task task = taskService.createTaskQuery()
.taskAssignee("zhangsan")
.singleResult();
Map<String,Object> params = new HashMap<>();
params.put("deptment","lisi");
//zhangsan完成任务,交由部门经理lisi审批
taskService.complete(task.getId(),params);
}

执行方法,控制台上打印 给lisi发一个消息! 说明监听器在部门审批(approve1)用户任务创建时被调用了。

继续观察启动流程时介绍的表,act_ru_task 中之前 zhangsan 的用户任务已经被删除,在 act_hi_taskinst 中已经变为 completed 状态;但是由于我们设置的是 global 变量,在 act_ru_variable 中还可以找到开启流程时设置的参数;其他的表中也会有记录,细细观察会有更多收获,碍于篇幅,就不再赘述。

继续执行推进 lisi 的当前任务:

1
2
3
4
5
6
7
8
9
public void taskComplete(){
//目前lisi只有一个任务,业务中根据场景选择其他合适的方式
Task task = taskService.createTaskQuery()
.taskAssignee("lisi")
.singleResult();
Map<String,Object> params = new HashMap<>();
params.put("personal","wangwu");
taskService.complete(task.getId(),params);
}

控制台打印 存储请假信息!!!! ,并没有去执行人事审批(approve2)用户任务,而是直接去执行数据同步(autoSync)系统任务,说明连线(顺序流)设置的条件表达式起效了(开启流程时,设置的请假天数是 2)。

此时, act_ru_task 表中的已经没有当前流程实例的任务了,流程结束。act_hi_taskinsk 表中此流程实例的用户任务也都变成了 completed 状态。

总结

初学,仅作为学习记录,备忘。

本篇仅作为从0到1的入门过程,不过是 Camunda 的冰山一角。

下一篇继续学习记录 Camunda 的其他组件功能!

文末放出笔者在学习本篇内容时的测试手稿:

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
@SpringBootTest
class CamundaDemoApplicationTests {

@Autowired
RuntimeService runtimeService;

@Autowired
TaskService taskService;

@Autowired
HistoryService historyService;

@Autowired
RepositoryService repositoryService;

/**
* 流程定义部署
*/
@SneakyThrows
@Test
void deploy() {
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("bpmn/holiday.bpmn")
.deploy();
System.out.println(deploy.getId());
}

/**
* 开启一个流程实例
*/
@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 taskQuery() {
List<Task> tasks = taskService.createTaskQuery()
.processDefinitionKey("holiday")
.list();
for (Task task : tasks) {
System.out.println(task.getAssignee());
System.out.println(task.getId());
System.out.println(task.getName());
System.out.println(task.getTenantId());
}
}

/**
* 流程任务执行
*/
@Test
public void taskComplete(){
//目前lisi只有一个任务,业务中根据场景选择其他合适的方式
Task task = taskService.createTaskQuery()
.taskAssignee("lisi")
.singleResult();
Map<String,Object> params = new HashMap<>();
params.put("personal","wangwu");
taskService.complete(task.getId(),params);
}

/**
* 流程定义查询
*/
@Test
public void queryDefine(){
ProcessDefinitionQuery query = repositoryService.createProcessDefinitionQuery();
List<ProcessDefinition> definitions = query.processDefinitionKey("holiday")
.orderByProcessDefinitionVersion()
.desc()
.list();
for (ProcessDefinition definition : definitions) {
System.out.println(definition.getDeploymentId());
System.out.println(definition.getName());
System.out.println(definition.getVersion());
System.out.println(definition.getId());
System.out.println(definition.getKey());
}
}

/**
* 删除流程定义
*/
@Test
public void deleteDefine(){
ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
List<ProcessDefinition> definitions = processDefinitionQuery.processDefinitionKey("holiday")
.orderByProcessDefinitionVersion()
.asc()
.list();
ProcessDefinition processDefinition = definitions.get(0);
if (processDefinition != null){
//删除流程定义,如果存在该定义的流程实例在运行中则删除报错
//repositoryService.deleteDeployment(processDefinition.getDeploymentId());
//true 级联删除流程定义,即使该流程有流程实例启动也可以删除(没有审批完的流程也会被先删除)
repositoryService.deleteDeployment(processDefinition.getDeploymentId(),true);
}
}

/**
* 获取流程定义的资源文件
*/
@SneakyThrows
@Test
public void outputBPMNFile(){
List<ProcessDefinition> res = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey("holiday")
.list();
ProcessDefinition holiday = res.get(0);
//获取BPMN文件的输入流 获取BPMN文件名
InputStream BPMNIs = repositoryService.getResourceAsStream(holiday.getDeploymentId(), holiday.getResourceName());
//如果部署了对应的图片,也可以获取到该图片的输入流 获取对应图片的资源名
//InputStream PNGIs = repositoryService.getResourceAsStream(holiday.getDeploymentId(), holiday.getDiagramResourceName());
//输入流转换为输出流
FileOutputStream outputStream = new FileOutputStream("E:\\流程\\" + holiday.getResourceName(),true);
IOUtils.copy(BPMNIs,outputStream);
}

/**
* 查询历史信息
*/
@Test
public void queryHistory(){
List<HistoricActivityInstance> list = historyService.createHistoricActivityInstanceQuery()
.finished()
.orderByHistoricActivityInstanceEndTime()
.asc()
.list();
for (HistoricActivityInstance instance : list) {
System.out.println(instance.getActivityId());
System.out.println(instance.getProcessDefinitionKey());
System.out.println(instance.getAssignee());
System.out.println(instance.getStartTime());
System.out.println(instance.getEndTime());
System.out.println("=============================");
}
}

/**
* 启动一个流程实例,并且添加一个业务key
* 业务key 可以在 act_ru_execution 中看到
*/
@Test
public void startProcInstAddBusinessKey(){
ProcessInstance holiday = runtimeService.startProcessInstanceByKey("holiday", "0001");
System.out.println(holiday.getBusinessKey());
}

/**
* 挂起、激活流程定义的所有流程实例
*/
@Test
public void activateOrSuspendProcInsts(){
//获取流程定义
List<ProcessDefinition> holidays = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey("holiday")
.list();
ProcessDefinition holiday = holidays.get(0);
//得到流程定义的所有流程实例会否都是暂停状态
boolean suspended = holiday.isSuspended();
if (suspended) {
//激活
repositoryService.activateProcessDefinitionById(holiday.getId(),true,null);
System.out.println("定义"+holiday.getId()+"激活");
}else {
//挂起
repositoryService.suspendProcessDefinitionById(holiday.getId(),true,null);
System.out.println("定义"+holiday.getId()+"挂起");
}
}

/**
* 挂起、激活单个流程实例
* 执行挂起的流程会抛出异常
*/
@Test
public void activateOrSuspendProcInst(){
ProcessInstance holiday = runtimeService.createProcessInstanceQuery()
.processInstanceBusinessKey("0001")
.singleResult();
boolean suspended = holiday.isSuspended();
if (suspended) {
runtimeService.activateProcessInstanceById(holiday.getId());
System.out.println("实例"+holiday.getId()+"激活");
}else {
runtimeService.suspendProcessInstanceById(holiday.getId());
System.out.println("实例"+holiday.getId()+"挂起");
}
}

/**
* 开启一条流程,并给用户任务的 assignee 赋值
*/
@Test
public void startProcessInstanceWithAssignee(){
Map<String,Object> map = new HashMap<>();
map.put("employee","zhangsan");
map.put("deptment","lisi");
map.put("personal","wangwu");
ProcessInstance holiday = runtimeService.startProcessInstanceByKey("holiday", map);
System.out.println(holiday.getProcessInstanceId());
}

/**
* 设置global/loacal变量
*/
@Test
public void setVariables(){
Map params = new HashMap();
params.put("days",3);
params.put("type","休假");
//设置global变量
runtimeService.setVariable("excutionId","key","value");
runtimeService.setVariables("excutionId",params);
//设置local变量
runtimeService.setVariableLocal("excutionId","key","value");
runtimeService.setVariablesLocal("excutionId",params);
}
}

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

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