Camunda 开源流程引擎学习笔记(三)——实战
本文通过一个模拟的复杂流程, 全面演示camunda和bpmn的一些流程及操作
语雀原文链接: Camunda 开源流程引擎学习笔记(三)——实战
实战思路
本文通过一个模拟的复杂流程, 全面演示
- 审批
- 驳回
- 重新打开
- 流程自动化
- 异常处理
- 事件
- 消息
- 网关
- 信号
- 子流程
- DMN
- 条件
脚本(外部任务模式不可执行脚本)- 流程修改
- 中间事件
- 边界事件
- 可选操作(待定)
拟订流程
- 提货单分解、审批(二级)、驳回、重新编辑
- 提货单审批完成后,流程接受其它系统消息, 执行进场流程。(流程自动化)
- 提供两种进场流程(子流程)
- 使用
DMN
决定提货单执行哪个流程 - 某一指令可以将流程修改为任一指定的节点.(流程修改)
- 子流程, 任务节点, 均可
消息事件
信号事件
BPMN规范与Camunda
实现
实施步骤
- 画bpmn流程图
- 部署流程图
- 对于人工节点, 手动提交审核
- 对于事件捕获节点, 通过编程的方式, 编写代码, 驱动流程前进
- 写模拟接口, 通过postman调用接口
定时器事件, 定义具体日期,循环时间, 时间周期的格式, 参考: https://docs.camunda.org/manual/latest/reference/bpmn20/events/timer-events/
程序结构
Camunda run
作为工作流引擎的服务器.app-demo
作为应用服务, 它包含两个部分
- 控制台程序, 用于用户手动完成用户任务, 以及查询流程/任务状态. 此程序也可以用
Camunda web app
替代. - web 服务, 用于模拟外部事件, 触发应用提交工作流事件
使用rest-client
连接camunda
服务器, 参考: https://github.com/camunda-community-hub/camunda-engine-rest-client-java/
<dependency>
<groupId>org.camunda.community</groupId>
<artifactId>camunda-engine-rest-client-openapi-springboot</artifactId>
<version>7.16.0-alpha1</version>
</dependency>
camunda.bpm.client.base-url: http://localhost:8080/engine-rest
测试API
测试代码结构:
package com.hchw.camundademoapp;
import com.alibaba.fastjson.JSON;
import org.camunda.community.rest.client.api.DeploymentApi;
import org.camunda.community.rest.client.api.MessageApi;
import org.camunda.community.rest.client.api.ProcessDefinitionApi;
import org.camunda.community.rest.client.api.ProcessInstanceApi;
import org.camunda.community.rest.client.dto.*;
import org.camunda.community.rest.client.invoker.ApiClient;
import org.camunda.community.rest.client.invoker.ApiException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Date;
import static com.hchw.camundademoapp.config.Constant.TRANS_TASK_ID;
public class CamundaTest {
ProcessDefinitionApi processDefinitionApi;
DeploymentApi deploymentApi;
ProcessInstanceApi processInstanceApi;
MessageApi messageApi;
@BeforeEach
public void before(){
ApiClient apiClient = new ApiClient();
processDefinitionApi = new ProcessDefinitionApi();
deploymentApi = new DeploymentApi();
processDefinitionApi = new ProcessDefinitionApi();
messageApi = new MessageApi();
}
@Test
public void testCreateDeployment() throws FileNotFoundException, ApiException {
DeploymentWithDefinitionsDto deployment = deploymentApi.createDeployment(null, null, true, true, "demo-process", new Date(), new File("C:\\code\\camunda\\camunda-demo-app\\src\\main\\resources\\demo-process.bpmn"));
prettyPrint(deployment);
}
@Test
public void testDeleteDeployment() throws FileNotFoundException, ApiException {
String deployId = "09449dfe-d440-11ec-aa04-e89eb412cf47";
deploymentApi.deleteDeployment(deployId, true, true, true);
}
private void prettyPrint(Object obj){
System.out.println(JSON.toJSONString(obj, true));
}
}
部署流程
@Test
public void testCreateDeployment() throws FileNotFoundException, ApiException {
DeploymentWithDefinitionsDto deployment = deploymentApi.createDeployment(null, null, true, true, "demo-process", new Date(), new File("C:\\code\\camunda\\camunda-demo-app\\src\\main\\resources\\demo-process.bpmn"));
prettyPrint(deployment);
}
首次部署方法返回:
{
"deployedProcessDefinitions":{
"Process_1ih13rv:1:0ad500a9-d456-11ec-aa04-e89eb412cf47":{
"category":"http://bpmn.io/schema/bpmn",
"deploymentId":"0ac15197-d456-11ec-aa04-e89eb412cf47",
"id":"Process_1ih13rv:1:0ad500a9-d456-11ec-aa04-e89eb412cf47",
"key":"Process_1ih13rv",
"resource":"demo-process.bpmn",
"startableInTasklist":true,
"suspended":false,
"version":1
}
},
"deploymentTime":1652622655433,
"id":"0ac15197-d456-11ec-aa04-e89eb412cf47",
"links":[
{
"href":"http://localhost:8080/engine-rest/deployment/0ac15197-d456-11ec-aa04-e89eb412cf47",
"method":"GET",
"rel":"self"
}
],
"name":"demo-process"
}
部署前:
部署后:
再次部署相同流程, 因为引擎中已有流程, 则直接返回已有流程的数据:
方法返回:
{
"deploymentTime":1652614825541,
"id":"cfc675ce-d443-11ec-aa04-e89eb412cf47",
"links":[
{
"href":"http://localhost:8080/engine-rest/deployment/cfc675ce-d443-11ec-aa04-e89eb412cf47",
"method":"GET",
"rel":"self"
}
],
"name":"demo-process"
}
创建流程
@Test
public void testStartProcessInstance() throws ApiException {
String transTaskId = "T001";
String definitionId = "Process_1ih13rv:1:cfcfebb0-d443-11ec-aa04-e89eb412cf47";
StartProcessInstanceDto dto = new StartProcessInstanceDto();
dto.putVariablesItem(TRANS_TASK_ID, new VariableValueDto().value(transTaskId).type("string"));
ProcessInstanceWithVariablesDto instance = processDefinitionApi.startProcessInstance(definitionId, dto);
prettyPrint(instance);
}
返回结果:
{
"definitionId":"Process_1ih13rv:1:0ad500a9-d456-11ec-aa04-e89eb412cf47",
"ended":false,
"id":"8437fc0a-d456-11ec-aa04-e89eb412cf47",
"links":[
{
"href":"http://localhost:8080/engine-rest/process-instance/8437fc0a-d456-11ec-aa04-e89eb412cf47",
"method":"GET",
"rel":"self"
}
],
"suspended":false
}
查询流程中的任务
使用ProcessInstanceApi.
该方法可以返回
@Test
public void testGetTasks() throws ApiException {
String instanceId = "7fd73ea8-d444-11ec-aa04-e89eb412cf47";
// taskApi.getTasks(null, null, instanceId,)
ActivityInstanceDto activityInstanceTree = processInstanceApi.getActivityInstanceTree(instanceId);
prettyPrint(activityInstanceTree);
}
{
"activityId":"Process_1ih13rv:1:cfcfebb0-d443-11ec-aa04-e89eb412cf47",
"activityType":"processDefinition",
"childActivityInstances":[
{
"activityId":"Activity_0i00a8u",
"activityName":"提交提货单",
"activityType":"userTask",
"childActivityInstances":[],
"childTransitionInstances":[],
"executionIds":[
"7fd73ea8-d444-11ec-aa04-e89eb412cf47"
],
"id":"Activity_0i00a8u:7fd78ccc-d444-11ec-aa04-e89eb412cf47",
"incidentIds":[],
"incidents":[],
"parentActivityInstanceId":"7fd73ea8-d444-11ec-aa04-e89eb412cf47",
"processDefinitionId":"Process_1ih13rv:1:cfcfebb0-d443-11ec-aa04-e89eb412cf47",
"processInstanceId":"7fd73ea8-d444-11ec-aa04-e89eb412cf47"
}
],
"childTransitionInstances":[],
"executionIds":[
"7fd73ea8-d444-11ec-aa04-e89eb412cf47"
],
"id":"7fd73ea8-d444-11ec-aa04-e89eb412cf47",
"incidentIds":[],
"incidents":[],
"processDefinitionId":"Process_1ih13rv:1:cfcfebb0-d443-11ec-aa04-e89eb412cf47",
"processInstanceId":"7fd73ea8-d444-11ec-aa04-e89eb412cf47"
}
使用TaskApi
这个api的参数也不封装一下…
@Test
public void testGetTasks3() throws ApiException {
String instanceId = "8437fc0a-d456-11ec-aa04-e89eb412cf47";
String taskDefinitionKey = TransProcessUserTasks.USER_1_SUBMIT_FORM.getDefinitionKey();
List<TaskDto> tasks = taskApi.queryTasks(0, 1, new TaskQueryDto().processInstanceId(instanceId).taskDefinitionKey(taskDefinitionKey));
prettyPrint(tasks, TransProcessUserTasks.USER_1_SUBMIT_FORM.getName());
}
响应:
-------- 提交提货单 --------
[
{
"assignee":"user1",
"created":1652622859277,
"executionId":"8437fc0a-d456-11ec-aa04-e89eb412cf47",
"id":"8440fcbf-d456-11ec-aa04-e89eb412cf47",
"name":"提交提货单",
"priority":50,
"processDefinitionId":"Process_1ih13rv:1:0ad500a9-d456-11ec-aa04-e89eb412cf47",
"processInstanceId":"8437fc0a-d456-11ec-aa04-e89eb412cf47",
"suspended":false,
"taskDefinitionKey":"u-task-submit-form"
}
]
-------- 提交提货单 --------
完成用户任务
完成第一个用户任务
@Test
public void testUserTasks() throws ApiException {
String userName = "user1";
String expectTaskName = "提交提货单";
String instanceId = "8437fc0a-d456-11ec-aa04-e89eb412cf47";
String taskDefinitionKey = TransProcessUserTasks.USER_1_SUBMIT_FORM.getDefinitionKey();
List<TaskDto> tasks = taskApi.queryTasks(0, 1, new TaskQueryDto().processInstanceId(instanceId).taskDefinitionKey(taskDefinitionKey));
TaskDto taskDto = null;
if (tasks.size() == 1){
taskDto = tasks.get(0);
}
Map<String, VariableValueDto> taskVariables = taskVariableApi.getTaskVariables(taskDto.getId(), true);
prettyPrint(taskVariables, taskDto.getName() + " variables");
Map<String, VariableValueDto> submit = taskApi.complete(taskDto.getId(), new CompleteTaskDto());
prettyPrint(submit, taskDto.getName() + "submit response");
}
提交之后, 用user1
登录camunda web, 显示当前任务已完成, 下一个任务分配给user2了.
用user2登录, user2可以操作这个任务:
再次查询第一个任务
显示, 指定的查询条件, 已经查询不到任务了, 说明这个方法查询的是等待中的任务.
使用第二个用户任务的definitionKey查询, 则可以查询出结果:
提交第二个用户任务
第二个任务是分配给了user2
, 我们用user1提交任务:
此处报错, 是因为第二个任务后的排他网关, 需要一个firstApproved
的参数, 用于判断下一步执行. 因此, 提交任务时, 应同时提交这个变量.
增加变量后, 成功提交该任务, 说明使用api完成任务, 引擎是不检查用户的, 用户和其它变量一样, 只是一个特殊的变量.
此时, 使用user2访问camunda web, 出现错误提示. 但流程已经流转到第三节点了.
驳回第三用户任务
查看web,已经查不到流程实例了, camunda默认不展示流程历史.流程审计是商业版的功能, 想要相关功能只能定制开发了.
查看历史实例
提交第三任务
重新创建一个实例, 提交所有用户任务.此时查看流程状态:
通过代码查询流程活动实例树:
@Test
public void testGetTasks() throws ApiException {
String instanceId = "7b2b9935-d45b-11ec-aa04-e89eb412cf47";
ActivityInstanceDto activityInstanceTree = processInstanceApi.getActivityInstanceTree(instanceId);
prettyPrint(activityInstanceTree);
}
查询结果:
{
"activityId":"Process_1ih13rv:1:0ad500a9-d456-11ec-aa04-e89eb412cf47",
"activityType":"processDefinition",
"childActivityInstances":[
{
"activityId":"Event_tidan_date",
"activityName":"到达提单日期",
"activityType":"intermediateMessageCatch",
"childActivityInstances":[],
"childTransitionInstances":[],
"executionIds":[
"78f059db-d45c-11ec-aa04-e89eb412cf47"
],
"id":"Event_tidan_date:78f0a7fd-d45c-11ec-aa04-e89eb412cf47",
"incidentIds":[],
"incidents":[],
"parentActivityInstanceId":"7b2b9935-d45b-11ec-aa04-e89eb412cf47",
"processDefinitionId":"Process_1ih13rv:1:0ad500a9-d456-11ec-aa04-e89eb412cf47",
"processInstanceId":"7b2b9935-d45b-11ec-aa04-e89eb412cf47"
}
],
"childTransitionInstances":[],
"executionIds":[
"7b2b9935-d45b-11ec-aa04-e89eb412cf47"
],
"id":"7b2b9935-d45b-11ec-aa04-e89eb412cf47",
"incidentIds":[],
"incidents":[],
"processDefinitionId":"Process_1ih13rv:1:0ad500a9-d456-11ec-aa04-e89eb412cf47",
"processInstanceId":"7b2b9935-d45b-11ec-aa04-e89eb412cf47"
}
消息事件
发送消息事件:到达提货日期
@Test
public void testSendMsg() throws ApiException {
String instanceId = "7b2b9935-d45b-11ec-aa04-e89eb412cf47";
String transTaskId = "T001";
CorrelationMessageDto dto = new CorrelationMessageDto();
dto.processInstanceId(instanceId)
.messageName(TransProcessMessages.TO_TRANS_DATE.getMsgName(transTaskId));
List<MessageCorrelationResultWithVariableDto> messageCorrelationResultWithVariableDtos = messageApi.deliverMessage(dto);
prettyPrint(messageCorrelationResultWithVariableDtos, "消息返回");
}
发送消息后, 流程执行到下一个事件处:
使用代码查询流程状态:
{
"activityId":"Process_1ih13rv:1:0ad500a9-d456-11ec-aa04-e89eb412cf47",
"activityType":"processDefinition",
"childActivityInstances":[
{
"activityId":"Event_17g9vf1",
"activityName":"车辆进入工厂",
"activityType":"intermediateMessageCatch",
"childActivityInstances":[],
"childTransitionInstances":[],
"executionIds":[
"a6e97f1a-d45d-11ec-aa04-e89eb412cf47"
],
"id":"Event_17g9vf1:a6e9cd3c-d45d-11ec-aa04-e89eb412cf47",
"incidentIds":[],
"incidents":[],
"parentActivityInstanceId":"7b2b9935-d45b-11ec-aa04-e89eb412cf47",
"processDefinitionId":"Process_1ih13rv:1:0ad500a9-d456-11ec-aa04-e89eb412cf47",
"processInstanceId":"7b2b9935-d45b-11ec-aa04-e89eb412cf47"
}
],
"childTransitionInstances":[],
"executionIds":[
"7b2b9935-d45b-11ec-aa04-e89eb412cf47"
],
"id":"7b2b9935-d45b-11ec-aa04-e89eb412cf47",
"incidentIds":[],
"incidents":[],
"processDefinitionId":"Process_1ih13rv:1:0ad500a9-d456-11ec-aa04-e89eb412cf47",
"processInstanceId":"7b2b9935-d45b-11ec-aa04-e89eb412cf47"
}
发送消息: 车辆进入工厂
@Test
public void testSendMsg2() throws ApiException {
String instanceId = "7b2b9935-d45b-11ec-aa04-e89eb412cf47";
String transTaskId = "T001";
CorrelationMessageDto dto = new CorrelationMessageDto();
dto.processInstanceId(instanceId)
.messageName(TransProcessMessages.TO_FACTORY.getMsgName(transTaskId));
List<MessageCorrelationResultWithVariableDto> messageCorrelationResultWithVariableDtos = messageApi.deliverMessage(dto);
prettyPrint(messageCorrelationResultWithVariableDtos, "消息返回");
}
发送成功后再次查询流程状态, 可以看到, 流程活动实例有3个, 都是中间捕获事件:
{
"activityId":"Process_1ih13rv:1:0ad500a9-d456-11ec-aa04-e89eb412cf47",
"activityType":"processDefinition",
"childActivityInstances":[
{
"activityId":"Event_0n7ke7r",
"activityName":"接收安检结果",
"activityType":"intermediateMessageCatch",
"childActivityInstances":[],
"childTransitionInstances":[],
"executionIds":[
"53c42334-d45e-11ec-aa04-e89eb412cf47"
],
"id":"Event_0n7ke7r:53c42336-d45e-11ec-aa04-e89eb412cf47",
"incidentIds":[],
"incidents":[],
"parentActivityInstanceId":"7b2b9935-d45b-11ec-aa04-e89eb412cf47",
"processDefinitionId":"Process_1ih13rv:1:0ad500a9-d456-11ec-aa04-e89eb412cf47",
"processInstanceId":"7b2b9935-d45b-11ec-aa04-e89eb412cf47"
},
{
"activityId":"Event_0r5kmd0",
"activityName":"接收空车过磅数据",
"activityType":"intermediateMessageCatch",
"childActivityInstances":[],
"childTransitionInstances":[],
"executionIds":[
"53c3fc21-d45e-11ec-aa04-e89eb412cf47"
],
"id":"Event_0r5kmd0:53c42333-d45e-11ec-aa04-e89eb412cf47",
"incidentIds":[],
"incidents":[],
"parentActivityInstanceId":"7b2b9935-d45b-11ec-aa04-e89eb412cf47",
"processDefinitionId":"Process_1ih13rv:1:0ad500a9-d456-11ec-aa04-e89eb412cf47",
"processInstanceId":"7b2b9935-d45b-11ec-aa04-e89eb412cf47"
},
{
"activityId":"Event_1ssfe9w",
"activityName":"司机报道",
"activityType":"intermediateMessageCatch",
"childActivityInstances":[],
"childTransitionInstances":[],
"executionIds":[
"53c44a47-d45e-11ec-aa04-e89eb412cf47"
],
"id":"Event_1ssfe9w:53c47159-d45e-11ec-aa04-e89eb412cf47",
"incidentIds":[],
"incidents":[],
"parentActivityInstanceId":"7b2b9935-d45b-11ec-aa04-e89eb412cf47",
"processDefinitionId":"Process_1ih13rv:1:0ad500a9-d456-11ec-aa04-e89eb412cf47",
"processInstanceId":"7b2b9935-d45b-11ec-aa04-e89eb412cf47"
}
],
"childTransitionInstances":[],
"executionIds":[
"7b2b9935-d45b-11ec-aa04-e89eb412cf47",
"53c3adfe-d45e-11ec-aa04-e89eb412cf47",
"53c3fc1f-d45e-11ec-aa04-e89eb412cf47",
"53c3fc20-d45e-11ec-aa04-e89eb412cf47"
],
"id":"7b2b9935-d45b-11ec-aa04-e89eb412cf47",
"incidentIds":[],
"incidents":[],
"processDefinitionId":"Process_1ih13rv:1:0ad500a9-d456-11ec-aa04-e89eb412cf47",
"processInstanceId":"7b2b9935-d45b-11ec-aa04-e89eb412cf47"
}
查询web:
发送消息: 接收空车过磅数据
发送后, 查询流程状态, 发现开始排队
的外部任务居然处于激活状态:
查询web:
尝试完成开始排队
任务:
@Test
public void testExtService() throws ApiException {
String definitionKey = "Process_1ih13rv";
String workId = "the-only-worker";
List<LockedExternalTaskDto> lockedExternalTaskDtos = externalTaskApi.fetchAndLock(new FetchExternalTasksDto()
.maxTasks(1)
.workerId(workId)
.topics(Collections.singletonList(new FetchExternalTaskTopicDto()
.topicName(TransProcessExtTasks.EXT_1_START_QUEUE.getTopic())
.processDefinitionKey(definitionKey)
.lockDuration(1000 * 1L)
))
);
prettyPrint(lockedExternalTaskDtos);
for (LockedExternalTaskDto dto: lockedExternalTaskDtos){
externalTaskApi.completeExternalTaskResource(dto.getId(), new CompleteExternalTaskDto().workerId(workId));
}
}
完成外部任务后:
修改并行网关
并行网关分开和汇聚都要有网关, 分开是发token, 汇聚时收token.
插入元素是一个比较常见的场景, 如下, 可以使用建模工具提供扩展空间工具在开始排队
节点之前插入空间:
如下图, 增加一个汇聚网关
注意: 此处建模工具有个坑, 把网关拖动到开始排队
节点之后, 一定要把网关和**开始排队**
的连接线删干净, 然后重新连接, 不然可能会存在中间事件直接连接到**开始排队**
节点的情况, 由于线条重合而无法注意到.
重新部署流程, 并执行到并行网关处
正确使用并行网关, 则必须三个事件都捕获之后才会执行到开始排队
外部任务
执行外部任务
@Test
public void extService() throws ApiException {
List<LockedExternalTaskDto> lockedExternalTaskDtos = externalTaskApi.fetchAndLock(new FetchExternalTasksDto()
.maxTasks(1)
.workerId(workId)
.topics(Collections.singletonList(new FetchExternalTaskTopicDto()
.topicName(TransProcessExtTasks.EXT_1_START_QUEUE.getTopic())
.processDefinitionKey(definitionKey)
.lockDuration(1000 * 1L)
))
);
prettyPrint(lockedExternalTaskDtos);
for (LockedExternalTaskDto dto: lockedExternalTaskDtos){
externalTaskApi.completeExternalTaskResource(dto.getId(), new CompleteExternalTaskDto().workerId(workId));
}
}
执行完成后:
事件网关
事件网关: 时钟事件
依次执行消息/外部服务, 直到事件网关处:
一分钟后, 定时器事件自动执行完成, 进入下一节点:
异常事件
异常事件参考: error-events
异常事件通常指的是bpmn 异常
是业务异常, 可以预定义的, 而非技术性异常. 当然, 在嵌入式模式下, 可以用java 异常代替bpmn异常.
注意: 中间异常和结束异常, 都需要订阅异常编码和异常消息:
提交异常
@Test
public void extService6() throws ApiException {
completeExtServiceWithError(ErrorProcessExtTasks.EXT_1_CALL_OTHER_SERVICE, ErrorProcessError.EXT_1_CALL_OTHER_SERVICE);
}
private void completeExtServiceWithError(ExtTask extTask, ErrorProcessError err) throws ApiException {
List<LockedExternalTaskDto> lockedExternalTaskDtos = externalTaskApi.fetchAndLock(new FetchExternalTasksDto()
.maxTasks(1)
.workerId(workId)
.topics(Collections.singletonList(new FetchExternalTaskTopicDto()
.topicName(extTask.getTopic())
.processDefinitionKey(definitionKey)
.lockDuration(1000 * 1L)
))
);
prettyPrint(lockedExternalTaskDtos);
for (LockedExternalTaskDto dto: lockedExternalTaskDtos){
externalTaskApi.handleExternalTaskBpmnError(dto.getId(),
new ExternalTaskBpmnError()
.errorCode(err.getCode())
.errorMessage(err.getMsg())
.workerId(workId)
);
}
}
提交异常前:
提交异常后:
子流程
Camunda 中的子流程允许基于可重用性和分组进行建模。以下是 Camunda 支持的不同类型的子流程:
- 嵌入式子流程
- 调用子流程: 调用另一个独立的流程
- 事件子流程
- 事务子流程
嵌入式子流程
https://docs.camunda.org/manual/latest/reference/bpmn20/subprocesses/embedded-subprocess/
子流程有两个主要用例:
- 子流程允许分层建模。许多建模工具允许折叠子流程,隐藏子流程的所有细节并显示业务流程的高级、端到端概览。
- 子流程为事件创建了一个新范围。在子流程执行期间抛出的事件可以被子流程边界上的边界事件捕获,从而为该事件创建一个范围,仅限于子流程。
使用子流程确实会施加一些限制:
- 一个子流程只能有一个无启动事件,不允许有其他启动事件类型。一个子流程必须至少有一个结束事件。请注意,BPMN 2.0 规范允许在子流程中省略开始和结束事件,但当前的引擎实现不支持这一点。
- 序列流不能跨越子流程边界。
正常流程
嵌入式子流程和主流程有相同的变量作用域.
异常流程
调用子流程
略
更多推荐
所有评论(0)