💡 这不是一篇普通的技术文章,而是一份经过实战检验的完整解决方案!
� 你将获得:
- ✅ 可直接运行的企业级代码(含详细注释)
- ✅ 完整的 BPMN 流程设计文件
- ✅ 生产环境配置最佳实践
- ✅ 踩坑记录与解决方案
📦 案例源码:https://github.com/zhouByte-hub/camunda-bpmn | 如果这份实战经验对你有帮助,请给个 ⭐ Star,让更多开发者看到!
一、引言
1.1 工作流引擎在企业应用中的重要性
在现代企业信息化建设中,业务流程的自动化管理已成为提升运营效率的关键。传统的硬编码方式处理复杂业务流程存在诸多痛点:流程变更成本高、业务逻辑与技术实现耦合严重、缺乏可视化监控手段。工作流引擎应运而生,它通过标准化的流程建模语言(BPMN)和强大的流程引擎,实现了业务流程与技术实现的解耦,让业务人员能够直接参与流程设计,技术团队专注于系统实现。
工作流引擎的核心价值体现在:
- 流程可视化:通过 BPMN 图形化建模,业务流程一目了然
- 灵活变更:流程调整无需修改代码,重新部署即可生效
- 状态追踪:实时监控流程执行状态,支持审计与合规要求
- 异常处理:内置重试、超时、补偿等机制,提升系统健壮性
1.2 Camunda 8 架构概述与核心优势
Camunda 8 是 Camunda 公司推出的新一代云原生工作流引擎,相比传统的 Camunda 7,它在架构上进行了彻底的重构,具备以下核心优势:
云原生架构:
- 基于 Zeebe 分布式工作流引擎,支持水平扩展
- 采用事件溯源架构,确保数据一致性
- 无状态设计,适合容器化部署
高性能设计:
- 单个 Zeebe Broker 可处理数万流程实例
- 基于 Actor 模型的并发处理机制
- 低延迟的任务分发与执行
开发者友好:
- 提供丰富的 Java Client API
- 支持 Spring Boot 无缝集成
- Job Worker 模式简化任务处理逻辑
运维便捷:
- 提供 Operate(流程监控)、Tasklist(任务管理)、Optimize(流程优化)等工具
- 支持 Prometheus/Grafana 监控集成
- 完善的日志与诊断机制
1.3 本文案例背景:服务器采购审批流程
流程图展示:

开始 → 项目组长审批 → 部门经理审批 → [金额判断]
↓
金额 >= 2000?
↙ ↘
是 否
↓ ↓
CEO 审批 财务审批
↓ ↓
→ 财务审批 ←
↓
[审批结果判断]
↓
通过 → 拨款购买 → 发送邮件 → 结束
不通过 → 发送邮件 → 结束
本文将以一个典型的企业级审批流程------"服务器采购审批流程"为例,深入讲解 Camunda 8 的实际应用。该流程包含以下业务场景:
业务流程:
- 项目组长提交采购申请
- 项目组长审批(初审)
- 部门经理审批(复审)
- 根据金额大小路由:
- 金额 ≥ 2000 元:需要CEO 审批
- 金额 < 2000 元:财务直接审批
- 财务审批通过后执行拨款购买
- 发送邮件通知申请人
技术挑战:
- 多级审批的条件路由
- 审批超时的自动处理
- 审计日志的自动记录
- 流程撤销与消息订阅
1.4 技术选型与项目架构总览
本项目采用以下技术栈:
| 技术组件 | 版本 | 用途 |
|---|---|---|
| Java | 21 | 开发语言 |
| Spring Boot | 3.x | 应用框架 |
| Camunda 8 | 8.8.0 | 工作流引擎 |
项目架构:
camunda_api/
├── src/main/java/org/zhoubyte/camunda_api/
│ ├── CamundaApiApplication.java # 应用启动类
│ ├── process/ # 流程控制层
│ │ ├── ProcessController.java # 流程实例管理
│ │ └── ProcessConstant.java # 流程常量定义
│ ├── jobs/ # Job Worker 实现
│ │ ├── PurchaseWorkerJob.java # 拨款购买任务
│ │ ├── AuditLogsWorkerJob.java # 审计日志监听器
│ │ ├── OverdueNoticeWorkerJob.java # 逾期通知任务
│ │ └── SendMessageWorkerJob.java # 发送邮件任务
│ ├── opt/ # 运维管理
│ │ ├── DeployController.java # 流程部署管理
│ │ └── Topology.java # 集群拓扑查询
│ └── util/ # 工具类
│ └── TimerCycleUtil.java # 定时器周期工具
└── src/main/resources/
├── bpmn/ # BPMN 流程定义
│ ├── purchase_server_process.bpmn # 采购审批流程
│ ├── apply_form.form # 申请表单
│ └── confirm_audit_form.form # 审批表单
└── application.yaml # 应用配置
二、核心功能介绍
2.1 流程部署与管理
2.1.1 BPMN 流程定义部署
Camunda 8 的流程部署采用资源化方式,通过 CamundaClient 的 newDeployResourceCommand() 方法实现。在 DeployController.java 中,我们实现了流程的部署功能:
java
/**
* 部署 BPMN 流程定义及相关资源
*
* @return 部署事件信息,包含流程定义Key、版本等
*/
@GetMapping(value = "/bpmn_resource")
public Optional<DeploymentEvent> deployBpmnResource() {
// 创建部署命令,同时部署流程文件和表单文件
DeploymentEvent join = camundaClient.newDeployResourceCommand()
.addResourceFromClasspath("bpmn/purchase_server_process.bpmn") // 流程定义文件
.addResourceFromClasspath("bpmn/apply_form.form") // 申请表单
.addResourceFromClasspath("bpmn/confirm_audit_form.form") // 审批表单
.send() // 异步发送请求
.join(); // 阻塞等待结果
return Optional.of(join);
}
技术要点:
- 支持同时部署 BPMN 流程文件和表单文件
- 资源从 classpath 加载,便于打包部署
- 返回
DeploymentEvent包含部署的流程定义信息
2.1.2 表单资源管理
Camunda 8 支持将表单与用户任务绑定,实现表单的独立管理和复用。本项目中定义了两个表单:
apply_form.form:采购申请表单confirm_audit_form.form:审批确认表单
在 BPMN 中通过 formId 引用表单:
xml
<userTask id="project_leader_approval" name="项目组长审批">
<bpmn:extensionElements>
<zeebe:userTask />
<zeebe:formDefinition formId="confim_audit_form" />
</bpmn:extensionElements>
</userTask>
2.1.3 流程定义查询与版本控制
通过 newProcessDefinitionSearchRequest() 可以查询已部署的流程定义:
java
/**
* 查询所有已部署的流程定义
*
* @return 流程定义列表
*/
@GetMapping(value = "/process_definition")
public List<ProcessDefinition> bpmnProcessDefinition(){
// 创建搜索请求并获取所有流程定义
return camundaClient.newProcessDefinitionSearchRequest()
.send() // 异步发送请求
.join() // 阻塞等待结果
.items(); // 获取流程定义列表
}
Camunda 8 支持流程版本管理,每次部署同名流程会自动递增版本号,运行中的流程实例继续使用旧版本,新启动的实例使用新版本。
2.2 流程实例生命周期管理
2.2.1 流程实例启动与变量注入
在 ProcessController.java 中实现了流程实例的启动:
java
/**
* 启动流程实例并注入初始变量
*
* @param money 采购金额(必填),用于条件路由判断
* @param overdue 逾期天数(可选),用于设置定时器触发时间
* @return 流程实例信息
*/
@GetMapping(value = "/start")
public Map<String, Object> startProcess(@RequestParam(value = "money") Integer money,
@RequestParam(value = "overdue", required = false) Integer overdue) {
// 初始化流程变量
Map<String, Object> variables = new HashMap<>();
variables.put(ProcessConstant.MONEY, money); // 采购金额
variables.put(ProcessConstant.PROCESS_INSTANCE_REVOKE_KEY, UUID.randomUUID()); // 流程撤销关联键
variables.put(ProcessConstant.RESPONSIBILITY, "项目组长"); // 当前责任人
// 设置逾期定时器周期
if(overdue == null || overdue <= 0) {
// 未指定逾期时间或逾期时间无效,立即触发(测试场景)
variables.put(ProcessConstant.OVERDUE_TIMER_CYCLE, TimerCycleUtil.immediately());
} else {
// 设置指定的逾期天数
variables.put(ProcessConstant.OVERDUE_TIMER_CYCLE, TimerCycleUtil.ofDays(overdue));
}
// 创建并启动流程实例
ProcessInstanceEvent processInstanceEvent = camundaClient.newCreateInstanceCommand()
.bpmnProcessId(ProcessConstant.PROCESS_INSTANCE_ID) // 流程定义ID
.latestVersion() // 使用最新版本
.variables(variables) // 注入流程变量
.send() // 异步发送请求
.join(); // 阻塞等待结果
// 返回流程实例信息
Map<String, Object> result = new HashMap<>();
result.put("processInstance", processInstanceEvent);
return result;
}
关键变量说明:
money:采购金额,用于条件路由process_instance_revoke_key:流程撤销的关联键responsibility:当前责任人overdue_timer_cycle:逾期定时器周期
2.2.2 用户任务完成与审批流转
用户任务需要人工介入完成,通过 API 调用完成任务并传递审批结果:
java
/**
* 完成用户任务并传递审批结果
*
* @param taskKey 用户任务Key
* @param variables 任务变量(包含审批结果等)
* @return 审批结果信息
*/
@PostMapping(value = "/userTask/{task_key}:complete")
public Map<String, Object> completeUserTask(@PathVariable("task_key") Long taskKey,
@RequestBody Map<String, Object> variables) {
// 完成用户任务并提交变量
camundaClient.newCompleteUserTaskCommand(taskKey)
.variables(variables) // 携带审批结果等变量
.send() // 异步发送请求
.join(); // 阻塞等待结果
// 查询任务详情(任务已完成,但可获取任务名称等信息)
UserTask userTask = camundaClient.newUserTaskGetRequest(taskKey).send().join();
// 获取审批结果,默认为false(不通过)
Object auditResult = variables.getOrDefault("audit_result", false);
// 构建返回结果
Map<String, Object> result = new HashMap<>();
result.put(userTask.getName(), auditResult); // 任务名称 -> 审批结果
result.put("audit_result", auditResult); // 统一的审批结果字段
return result;
}
审批流转逻辑:
- 审批结果通过
audit_result变量传递 - BPMN 中的条件表达式根据
audit_result决定流转方向 - 审批不通过会回退到项目组长重新审批
2.2.3 流程实例取消与终止
当需要提前结束流程时,可以通过取消命令终止流程实例:
java
/**
* 取消流程实例
*
* @param processInstanceId 流程实例ID
* @return 操作结果
*/
@GetMapping(value = "/end")
public String endProcess(@RequestParam("process_instance_id") Long processInstanceId) {
// 发送取消流程实例命令
camundaClient.newCancelInstanceCommand(processInstanceId)
.send() // 异步发送请求
.join(); // 阻塞等待结果
return "success";
}
2.3 Job Worker 任务处理机制
2.3.1 Service Task 自动化处理
Camunda 8 通过 Job Worker 模式处理 Service Task。在 PurchaseWorkerJob.java 中实现了拨款购买任务:
java
/**
* 拨款购买 Job Worker
* 处理 BPMN 中的 Service Task(拨款购买)
*/
@Component
@Slf4j
public class PurchaseWorkerJob {
/**
* 执行拨款购买任务
*
* @param client Job客户端,用于完成或失败Job
* @param activatedJob 激活的Job,包含流程变量等信息
*/
@JobWorker(type = "to_do_purchase_service", autoComplete = false)
public void purchaseServerWorkerJob(final JobClient client, final ActivatedJob activatedJob) {
log.info("[拨款购买] elementId={}, processInstanceKey={} 开始执行",
activatedJob.getElementId(), activatedJob.getProcessInstanceKey());
try {
// TODO: 实际拨款购买业务逻辑
// 可以通过 activatedJob.getVariable("money") 获取采购金额等变量
// 业务处理完成,手动完成Job
client.newCompleteCommand(activatedJob.getKey())
.send() // 异步发送请求
.join(); // 阻塞等待结果
log.info("[拨款购买] elementId={} 执行完成", activatedJob.getElementId());
} catch (Exception e) {
log.error("[拨款购买] elementId={} 执行失败: {}", activatedJob.getElementId(), e.getMessage(), e);
// 失败时上报错误,减少重试次数
client.newFailCommand(activatedJob.getKey())
.retries(activatedJob.getRetries() - 1) // 重试次数减1
.errorMessage(e.getMessage()) // 错误信息
.send() // 异步发送请求
.join(); // 阻塞等待结果
}
}
}
技术要点:
@JobWorker注解声明 Job Worker,type必须与 BPMN 中的taskDefinition type一致autoComplete = false表示手动完成模式,需要显式调用newCompleteCommand- 异常处理通过
newFailCommand上报错误,触发重试机制
2.3.2 Task Listener 事件监听
Task Listener 是 Camunda 8 的高级特性,用于监听用户任务的生命周期事件。在 AuditLogsWorkerJob.java 中实现了审计日志记录:
java
/**
* 审计日志 Task Listener
* 监听用户任务的 creating 事件,记录审计日志并设置责任人
*/
@Component
@Slf4j
public class AuditLogsWorkerJob {
private final CamundaClient camundaClient;
/**
* 记录审计日志并设置责任人
* 注意:Task Listener 必须手动完成Job,否则用户任务会卡在CREATING状态
*
* @param jobClient Job客户端
* @param activatedJob 激活的Job
*/
@JobWorker(type = "record_audit_logs", autoComplete = false)
public void recordAuditLogs(JobClient jobClient, ActivatedJob activatedJob) {
// 根据任务ID获取责任人
String responsibility = this.getResponsibility(activatedJob.getElementId());
try {
if (!StringUtils.isEmpty(responsibility)) {
// 设置责任人变量
Map<String, Object> variables = new HashMap<>();
variables.put(ProcessConstant.RESPONSIBILITY, responsibility);
// Task Listener不支持在完成时携带变量,需要单独设置
// 使用异步方式设置变量,确保变量设置完成后再完成Job
camundaClient.newSetVariablesCommand(activatedJob.getProcessInstanceKey())
.variables(variables)
.send()
.thenAccept(response -> jobClient.newCompleteCommand(activatedJob.getKey()).send().join());
} else {
// 必须完成Job,用户任务才能从CREATING转为CREATED状态
jobClient.newCompleteCommand(activatedJob.getKey())
.send()
.join();
}
log.info("[审计日志] responsibility = {}, elementId={}, userTaskKey={}, processInstanceKey={} 任务正在执行",
responsibility,
activatedJob.getElementId(),
activatedJob.getUserTask().getUserTaskKey(),
activatedJob.getProcessInstanceKey());
} catch (Exception e) {
log.error("[审计日志] elementId={} 执行失败: {}", activatedJob.getElementId(), e.getMessage(), e);
// 失败时上报错误
jobClient.newFailCommand(activatedJob.getKey())
.retries(activatedJob.getRetries() - 1)
.errorMessage(e.getMessage())
.send()
.join();
}
}
/**
* 根据任务ID获取责任人
*/
private String getResponsibility(String elementId) {
return switch (elementId) {
case ProcessConstant.PROJECT_LEADER_APPROVAL -> "项目组长";
case ProcessConstant.DEPARTMENT_MANAGER_APPROVAL -> "部门经理";
case ProcessConstant.FINANCIAL_APPROVAL -> "财务";
case ProcessConstant.CEO_APPROVAL -> "CEO";
default -> "未知";
};
}
}
关键技术点:
- Task Listener 监听
creating事件,在用户任务创建时触发 - 必须手动完成 Job,否则用户任务会卡在 CREATING 状态
- Task Listener 不支持在完成时携带变量,需要通过
newSetVariablesCommand单独设置
2.3.3 逾期通知与消息发布
在 OverdueNoticeWorkerJob.java 中实现了逾期通知和消息发布:
java
/**
* 逾期通知 Job Worker
* 处理用户任务超时事件,发送逾期通知并触发流程撤销
*/
@Component
@Slf4j
public class OverdueNoticeWorkerJob {
private final CamundaClient camundaClient;
/**
* 发送逾期通知
* 当用户任务超时时触发,发布消息以触发事件子流程
*
* @param activatedJob 激活的Job
*/
@JobWorker(type = "send-overdue-notice")
public void sendOverdueNotice(ActivatedJob activatedJob) {
// 获取流程撤销关联键
String revokeKey = (String) activatedJob.getVariable(ProcessConstant.PROCESS_INSTANCE_REVOKE_KEY);
if(!StringUtils.isEmpty(revokeKey)) {
// 发布消息,触发事件子流程中的消息开始事件
camundaClient.newPublishMessageCommand()
.messageName(ProcessConstant.TASK_REVOKE) // 消息名称
.correlationKey(revokeKey) // 关联键,用于匹配流程实例
.send() // 异步发送请求
.join(); // 阻塞等待结果
}
// 记录日志
Object responsibility = activatedJob.getVariable(ProcessConstant.RESPONSIBILITY);
log.info("Send overdue-notice job start, responsibility={}", responsibility);
}
}
消息发布机制:
- 通过
newPublishMessageCommand发布消息 messageName对应 BPMN 中的消息定义correlationKey用于关联流程实例
2.4 集群拓扑与运维监控
2.4.1 Zeebe 集群拓扑查询
在 Topology.java 中实现了集群拓扑查询:
java
/**
* 集群拓扑查询控制器
* 用于查询 Zeebe 集群的拓扑信息
*/
@RestController
@RequestMapping(value = "/topology")
public class Topology {
private final CamundaClient camundaClient;
/**
* 查询集群拓扑信息
* 返回Broker节点列表、分区分布、Gateway版本等信息
*
* @return 集群拓扑信息
*/
@GetMapping(value = "/all")
public io.camunda.client.api.response.Topology showTopology() {
// 发送拓扑查询请求
return camundaClient.newTopologyRequest()
.send() // 异步发送请求
.join(); // 阻塞等待结果
}
}
拓扑信息包含:
- Broker 节点列表
- 分区分布情况
- Gateway 版本信息
- 集群健康状态
三、技术实现细节
3.1 Spring Boot 集成 Camunda 8
3.1.1 Maven 依赖配置与版本管理
在 pom.xml 中配置了核心依赖:
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.camunda</groupId>
<artifactId>camunda-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
重要配置:
xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
<parameters>true</parameters> 配置保留方法参数名,Camunda Client 依赖此项将流程变量自动绑定到方法参数。
3.1.2 Camunda Client 配置详解(自托管模式)
在 application.yaml 中配置了 Camunda Client:
yaml
camunda:
client:
mode: self-managed
grpc-address: ENC(3Oz5CdOP1Mxu4Y7u/sn92ZpRMXeUNUJgJoDTXuv0riYpiZcfm5ngZ714RNjkWA1EfS4LxdL/lWuGuLhGIQo5UQ==)
rest-address: ENC(WkzegAwa1WfH++IAkRDSGo0OBc6DfiDIDPpnNP58e1TlGoZeaicQP7wORpiDg7O15Znt/VEPceVpwevWX+10Ow==)
prefer-rest-over-grpc: false
request-timeout: 60s
auth:
method: basic
username: ENC(Xf/tTg4Ji8hl6e21AYzCH8nTUnene5z2Z/Umxq7DKXX54WGYlgiVQeIkh/6+tEql)
password: ENC(k15ZkwmrfZeoh+qNf/j1TdvW0MURzPwI44U69rF+5/dChupoXNA0N7mD58tB3Xia)
enabled: true
worker:
defaults:
timeout: 30s
max-jobs-active: 32
配置说明:
mode: self-managed:自托管模式,区别于 Camunda SaaS 云服务grpc-address:Zeebe 网关的 gRPC 地址,用于流程部署、实例创建等操作rest-address:Zeebe 网关的 REST 地址,用于 HTTP REST API 交互prefer-rest-over-grpc: false:优先使用 gRPC,Task Listener 依赖 REST APIrequest-timeout: 60s:HTTP 请求超时,需大于服务端长轮询等待时间auth.method: basic:使用 HTTP Basic 认证worker.defaults.timeout: 30s:Job 锁定超时时间worker.defaults.max-jobs-active: 32:单个 Worker 最大并发处理 Job 数
3.1.3 认证机制:Basic Auth 配置实践
本项目使用 Jasypt 对敏感配置进行加密,配置值以 ENC() 包裹:
yaml
auth:
method: basic
username: ENC(Xf/tTg4Ji8hl6e21AYzCH8nTUnene5z2Z/Umxq7DKXX54WGYlgiVQeIkh/6+tEql)
password: ENC(k15ZkwmrfZeoh+qNf/j1TdvW0MURzPwI44U69rF+5/dChupoXNA0N7mD58tB3Xia)
认证方式对比:
| 认证方式 | 适用场景 | 配置复杂度 |
|---|---|---|
| none | 开发环境,无安全要求 | 低 |
| basic | 自托管环境,简单认证 | 中 |
| identity | 生产环境,集成 Keycloak | 高 |
3.2 BPMN 2.0 流程建模深度解析
3.2.1 用户任务与表单绑定
在 BPMN 中定义用户任务并绑定表单:
xml
<userTask id="project_leader_approval" name="项目组长审批">
<bpmn:extensionElements>
<zeebe:userTask />
<zeebe:formDefinition formId="confim_audit_form" />
<zeebe:taskListeners>
<zeebe:taskListener eventType="creating" retries="3" type="record_audit_logs" />
</zeebe:taskListeners>
<zeebe:taskHeaders>
<zeebe:header key="responsibility" value="项目组长" />
</zeebe:taskHeaders>
</bpmn:extensionElements>
</userTask>
关键元素:
<zeebe:userTask />:声明为用户任务<zeebe:formDefinition formId="confim_audit_form" />:绑定表单<zeebe:taskListener>:配置任务监听器<zeebe:taskHeaders>:任务元数据
3.2.2 服务任务与 Job Worker 映射
服务任务通过 taskDefinition 映射到 Job Worker:
xml
<serviceTask id="purchase_of_funds" name="拨款购买">
<bpmn:extensionElements>
<zeebe:taskDefinition type="to_do_purchase_service" retries="1" />
</bpmn:extensionElements>
</serviceTask>
映射关系:
- BPMN 中的
type="to_do_purchase_service" - Java 代码中的
@JobWorker(type = "to_do_purchase_service") - 两者必须完全一致
3.2.3 排他网关与条件表达式
排他网关用于条件路由:
xml
<exclusiveGateway id="Gateway_1mm01bz">
<bpmn:incoming>Flow_05v6331</bpmn:incoming>
<bpmn:outgoing>Flow_152vbpg</bpmn:outgoing>
<bpmn:outgoing>Flow_0y79hu3</bpmn:outgoing>
</exclusiveGateway>
<sequenceFlow id="Flow_152vbpg" name="组长审批通过" sourceRef="Gateway_1mm01bz" targetRef="department_manager_approval">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=audit_result=true</bpmn:conditionExpression>
</sequenceFlow>
<sequenceFlow id="Flow_0y79hu3" name="组长审批不通过" sourceRef="Gateway_1mm01bz" targetRef="Event_10jc7ry">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=audit_result=false</bpmn:conditionExpression>
</sequenceFlow>
条件表达式语法:
- 使用
=开头表示表达式 - 支持
=、!=、>、<、>=、<=等运算符 - 可引用流程变量,如
=money>=2000
3.2.4 边界事件与定时器配置
边界事件用于处理超时场景:
xml
<boundaryEvent id="Event_133pqmq" name="超时处置" cancelActivity="false" attachedToRef="project_leader_approval">
<bpmn:outgoing>Flow_1f0k2on</bpmn:outgoing>
<bpmn:timerEventDefinition>
<bpmn:timeCycle xsi:type="bpmn:tFormalExpression">=overdue_timer_cycle</bpmn:timeCycle>
</bpmn:timerEventDefinition>
</boundaryEvent>
定时器类型:
timeDate:指定时间触发timeDuration:指定持续时间后触发timeCycle:周期性触发(本项目使用)
cancelActivity 属性:
true:中断当前活动(取消用户任务)false:不中断当前活动(并行执行逾期处理)
3.2.5 事件子流程与消息订阅
事件子流程用于处理消息事件:
xml
<subProcess id="Activity_1eux7iq" name="接受逾期事件并发送邮件提醒" triggeredByEvent="true">
<serviceTask id="Activity_06bae2p" name="发送逾期邮件">
<bpmn:extensionElements>
<zeebe:taskDefinition type="send_overdue_message" retries="3" />
</bpmn:extensionElements>
</serviceTask>
<startEvent id="revoke" name="revoke" isInterrupting="false">
<bpmn:messageEventDefinition messageRef="Message_2tobcm6" />
</startEvent>
</subProcess>
<message id="Message_2tobcm6" name="task_revoke">
<bpmn:extensionElements>
<zeebe:subscription correlationKey="=process_instance_revoke_key" />
</bpmn:extensionElements>
</message>
消息订阅机制:
triggeredByEvent="true":声明为事件子流程isInterrupting="false":非中断事件,不影响主流程correlationKey:关联键,用于匹配消息与流程实例
3.3 Job Worker 开发最佳实践
3.3.1 @JobWorker 注解与类型匹配
@JobWorker 注解用于声明 Job Worker:
java
@JobWorker(type = "to_do_purchase_service", autoComplete = false)
public void purchaseServerWorkerJob(final JobClient client, final ActivatedJob activatedJob) {
// Job 处理逻辑
}
注解参数:
type:Job 类型,必须与 BPMN 中的taskDefinition type一致autoComplete:是否自动完成,默认truetimeout:Job 锁定超时时间maxJobsActive:最大并发 Job 数
3.3.2 手动完成模式
手动完成模式适用于以下场景:
- 需要在完成前执行异步操作
- 需要根据业务逻辑决定是否完成
- Task Listener 必须使用手动完成
java
@JobWorker(type = "record_audit_logs", autoComplete = false)
public void recordAuditLogs(JobClient jobClient, ActivatedJob activatedJob) {
// 异步设置变量
camundaClient.newSetVariablesCommand(activatedJob.getProcessInstanceKey())
.variables(variables)
.send()
.thenAccept(response -> jobClient.newCompleteCommand(activatedJob.getKey()).send().join());
}
3.3.3 错误处理与重试机制
Job 执行失败时,通过 newFailCommand 上报错误:
java
try {
// 业务逻辑
client.newCompleteCommand(activatedJob.getKey()).send().join();
} catch (Exception e) {
client.newFailCommand(activatedJob.getKey())
.retries(activatedJob.getRetries() - 1)
.errorMessage(e.getMessage())
.send()
.join();
}
重试机制:
retries在 BPMN 中定义,每次失败减 1- 当
retries = 0时,Job 进入 Incident 状态 - Incident 需要人工干预或通过 API 解决
3.3.4 Task Listener 特殊处理(creating 事件)
Task Listener 的 creating 事件在用户任务创建时触发,有以下特殊要求:
问题:Task Listener 不支持在完成时携带变量
解决方案 :使用 newSetVariablesCommand 单独设置变量
java
camundaClient.newSetVariablesCommand(activatedJob.getProcessInstanceKey())
.variables(variables)
.send()
.thenAccept(response -> jobClient.newCompleteCommand(activatedJob.getKey()).send().join());
异步处理原因:
send()返回CompletableFuturethenAccept()在设置变量完成后异步执行 Job 完成- 避免阻塞 Job Worker 线程
3.4 流程变量与数据流转
3.4.1 变量作用域与生命周期
Camunda 8 的流程变量有以下作用域:
| 作用域 | 生命周期 | 使用场景 |
|---|---|---|
| 流程实例变量 | 流程实例启动到结束 | 全局共享数据 |
| 本地变量 | 任务执行期间 | 任务临时数据 |
变量操作 API:
newSetVariablesCommand:设置流程实例变量newCompleteCommand.variables():完成任务时携带变量activatedJob.getVariablesAsMap():获取所有变量activatedJob.getVariable(key):获取单个变量
3.4.2 条件表达式中的变量引用
在 BPMN 条件表达式中引用变量:
xml
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=money>=2000</bpmn:conditionExpression>
表达式示例:
=audit_result=true:布尔值判断=money>=2000:数值比较=responsibility="项目组长":字符串比较
3.4.3 动态定时器周期生成
在 TimerCycleUtil.java 中实现了定时器周期工具:
java
public class TimerCycleUtil {
public static String ofDays(int days) {
if (days <= 0) {
throw new IllegalArgumentException("days 必须大于 0,当前值:" + days);
}
return "R/P" + days + "D";
}
public static String ofDays(int days, int repeat) {
if (days <= 0) {
throw new IllegalArgumentException("days 必须大于 0,当前值:" + days);
}
if (repeat <= 0) {
throw new IllegalArgumentException("repeat 必须大于 0,当前值:" + repeat);
}
return "R" + repeat + "/P" + days + "D";
}
public static String never() {
return "R0/P1D";
}
public static String immediately() {
return "R1/PT1S";
}
}
ISO 8601 时间周期格式:
R/P1D:每天触发一次,无限重复R3/P1D:每天触发一次,共触发 3 次R/PT1H:每小时触发一次,无限重复R0/P1D:永不触发R1/PT1S:1 秒后触发一次
Camunda 8 使用 ISO 8601 重复时间间隔格式:
格式规范:
R[重复次数]/P[年]Y[月]M[天]DT[时]H[分]M[秒]S
示例:
| 格式 | 说明 |
|---|---|
R/P1D |
每天触发一次,无限重复 |
R3/P1D |
每天触发一次,共触发 3 次 |
R/PT1H |
每小时触发一次,无限重复 |
R5/PT30M |
每 30 分钟触发一次,共触发 5 次 |
R/P1DT2H |
每 1 天 2 小时触发一次,无限重复 |
3.5 消息中间件事件机制
3.5.1 消息订阅与关联键
消息订阅通过 correlationKey 关联流程实例:
xml
<message id="Message_2tobcm6" name="task_revoke">
<bpmn:extensionElements>
<zeebe:subscription correlationKey="=process_instance_revoke_key" />
</bpmn:extensionElements>
</message>
关联机制:
- 流程启动时设置
process_instance_revoke_key变量 - 消息发布时使用相同的
correlationKey - Camunda 自动匹配消息与流程实例
3.5.2 消息发布与流程撤销实现
在 OverdueNoticeWorkerJob.java 中实现了消息发布:
java
@JobWorker(type = "send-overdue-notice")
public void sendOverdueNotice(ActivatedJob activatedJob) {
String revokeKey = (String) activatedJob.getVariable(ProcessConstant.PROCESS_INSTANCE_REVOKE_KEY);
if(!StringUtils.isEmpty(revokeKey)) {
camundaClient.newPublishMessageCommand()
.messageName(ProcessConstant.TASK_REVOKE)
.correlationKey(revokeKey)
.send()
.join();
}
}
消息发布 API:
messageName:消息名称,对应 BPMN 中的消息定义correlationKey:关联键,用于匹配流程实例variables:消息携带的变量(可选)
四、关键技术难点与解决方案
4.1 Task Listener Job 完成时机问题
4.1.1 问题现象:UserTask 卡在 CREATING 状态
在实现审计日志功能时,发现用户任务一直处于 CREATING 状态,无法进入 CREATED 状态,导致审批人无法操作任务。
4.1.2 根因分析:Job 未完成导致状态转换阻塞
问题根源:
- Task Listener 在用户任务创建时触发
- Task Listener 本质上是一个 Job,需要 Job Worker 处理
- 如果 Job 未完成,用户任务的状态转换会被阻塞
- 用户任务从 CREATING 到 CREATED 需要等待 Task Listener Job 完成
错误代码示例:
java
@JobWorker(type = "record_audit_logs", autoComplete = true)
public void recordAuditLogs(ActivatedJob activatedJob) {
// autoComplete = true 会自动完成 Job
// 但如果在此处执行异步操作,可能导致时序问题
}
4.1.3 解决方案:异步变量设置与 Job 完成
正确实现:
java
@JobWorker(type = "record_audit_logs", autoComplete = false)
public void recordAuditLogs(JobClient jobClient, ActivatedJob activatedJob) {
String responsibility = this.getResponsibility(activatedJob.getElementId());
try {
if (!StringUtils.isEmpty(responsibility)) {
Map<String, Object> variables = new HashMap<>();
variables.put(ProcessConstant.RESPONSIBILITY, responsibility);
// 异步设置变量,完成后异步完成 Job
camundaClient.newSetVariablesCommand(activatedJob.getProcessInstanceKey())
.variables(variables)
.send()
.thenAccept(response -> jobClient.newCompleteCommand(activatedJob.getKey()).send().join());
} else {
// 必须完成此 Job,UserTask 才能从 CREATING 转为 CREATED 状态
jobClient.newCompleteCommand(activatedJob.getKey())
.send()
.join();
}
} catch (Exception e) {
jobClient.newFailCommand(activatedJob.getKey())
.retries(activatedJob.getRetries() - 1)
.errorMessage(e.getMessage())
.send()
.join();
}
}
关键点:
- 使用
autoComplete = false手动控制 Job 完成时机 - 使用
thenAccept()确保变量设置完成后再完成 Job - 异常情况必须调用
newFailCommand或newCompleteCommand,避免 Job 卡住
4.2 流程变量动态设置与并发问题
4.2.1 Task Listener 不支持完成时携带变量
问题 :Task Listener 的 newCompleteCommand 不支持 variables() 方法
错误尝试:
java
jobClient.newCompleteCommand(activatedJob.getKey())
.variables(variables) // Task Listener 不支持
.send()
.join();
原因 :Camunda 8 的 Task Listener 设计上不允许在完成时修改流程变量。
4.2.2 使用 newSetVariablesCommand 独立设置变量
解决方案:
java
camundaClient.newSetVariablesCommand(activatedJob.getProcessInstanceKey())
.variables(variables)
.send()
.thenAccept(response -> jobClient.newCompleteCommand(activatedJob.getKey()).send().join());
技术细节:
newSetVariablesCommand独立于 Job 完成- 使用
thenAccept()确保变量设置完成后再完成 Job - 避免并发问题:变量设置和 Job 完成是两个独立操作
4.3 定时器周期动态配置
4.3.1 TimerCycleUtil 工具类设计
为了简化定时器周期的配置,设计了 TimerCycleUtil 工具类:
java
public class TimerCycleUtil {
public static String ofDays(int days) {
if (days <= 0) {
throw new IllegalArgumentException("days 必须大于 0,当前值:" + days);
}
return "R/P" + days + "D";
}
public static String ofDays(int days, int repeat) {
if (days <= 0) {
throw new IllegalArgumentException("days 必须大于 0,当前值:" + days);
}
if (repeat <= 0) {
throw new IllegalArgumentException("repeat 必须大于 0,当前值:" + repeat);
}
return "R" + repeat + "/P" + days + "D";
}
public static String never() {
return "R0/P1D";
}
public static String immediately() {
return "R1/PT1S";
}
}
4.3.2 立即触发与永不触发场景
立即触发:
- 使用场景:测试、演示
- 实现:
TimerCycleUtil.immediately()→R1/PT1S(1 秒后触发一次)
永不触发:
- 使用场景:禁用逾期处理
- 实现:
TimerCycleUtil.never()→R0/P1D(重复次数为 0)
使用示例:
java
if(overdue == null || overdue <= 0) {
variables.put(ProcessConstant.OVERDUE_TIMER_CYCLE, TimerCycleUtil.immediately());
} else {
variables.put(ProcessConstant.OVERDUE_TIMER_CYCLE, TimerCycleUtil.ofDays(overdue));
}
五、常见问题解答(FAQ)
5.1 如何避免流程重复部署?
问题:每次启动应用都会部署流程,导致流程版本不断递增。
解决方案:部署前检查流程定义是否已存在:
java
@GetMapping(value = "/bpmn_resource")
public Optional<DeploymentEvent> deployBpmnResource() {
boolean alreadyDeployed = !camundaClient.newProcessDefinitionSearchRequest()
.filter(f -> f.processDefinitionId("purchase_server_process"))
.send()
.join()
.items().isEmpty();
if (alreadyDeployed) {
log.info("流程定义已存在,跳过部署");
return Optional.empty();
}
DeploymentEvent join = camundaClient.newDeployResourceCommand()
.addResourceFromClasspath("bpmn/purchase_server_process.bpmn")
.addResourceFromClasspath("bpmn/apply_form.form")
.addResourceFromClasspath("bpmn/confirm_audit_form.form")
.send()
.join();
return Optional.of(join);
}
5.2 Job Worker 的 autoComplete 模式何时使用?
autoComplete = true(自动完成):
- 适用场景:简单的同步处理逻辑
- 优点:代码简洁,无需手动完成
- 缺点:无法控制完成时机,无法执行异步操作
autoComplete = false(手动完成):
- 适用场景:
- 需要执行异步操作
- 需要根据业务逻辑决定是否完成
- Task Listener(必须手动完成)
- 优点:完全控制 Job 生命周期
- 缺点:代码复杂,必须显式调用完成命令
5.3 如何处理 Job 执行失败?
重试机制:
java
try {
// 业务逻辑
client.newCompleteCommand(activatedJob.getKey()).send().join();
} catch (Exception e) {
if (activatedJob.getRetries() > 1) {
// 还有重试次数
client.newFailCommand(activatedJob.getKey())
.retries(activatedJob.getRetries() - 1)
.errorMessage(e.getMessage())
.send()
.join();
} else {
// 重试次数耗尽,进入 Incident
log.error("Job 执行失败,进入 Incident 状态: {}", e.getMessage());
client.newFailCommand(activatedJob.getKey())
.retries(0)
.errorMessage(e.getMessage())
.send()
.join();
}
}
Incident 处理:
- 通过 Camunda Operate 查看 Incident
- 解决问题后,通过 API 或 Operate 重试 Job
5.4 流程变量如何在不同任务间传递?
流程实例变量:
- 在流程启动时设置
- 所有任务都可以访问
- 通过
activatedJob.getVariable(key)获取
任务完成时设置变量:
java
camundaClient.newCompleteUserTaskCommand(taskKey)
.variables(variables)
.send()
.join();
独立设置变量:
java
camundaClient.newSetVariablesCommand(processInstanceKey)
.variables(variables)
.send()
.join();
5.5 如何实现流程的动态撤销?
消息订阅方式:
- 在 BPMN 中定义消息事件子流程:
xml
<subProcess id="Activity_1eux7iq" triggeredByEvent="true">
<startEvent id="revoke" isInterrupting="false">
<bpmn:messageEventDefinition messageRef="Message_2tobcm6" />
</startEvent>
<!-- 撤销逻辑 -->
</subProcess>
<message id="Message_2tobcm6" name="task_revoke">
<zeebe:subscription correlationKey="=process_instance_revoke_key" />
</message>
- 流程启动时设置关联键:
java
variables.put("process_instance_revoke_key", UUID.randomUUID().toString());
- 发布消息触发撤销:
java
camundaClient.newPublishMessageCommand()
.messageName("task_revoke")
.correlationKey(revokeKey)
.send()
.join();
六、性能优化与最佳实践
6.1 Job Worker 并发配置优化
配置参数:
yaml
camunda:
client:
worker:
defaults:
timeout: 30s
max-jobs-active: 32
优化建议:
max-jobs-active:根据业务处理时间和服务器资源调整- IO 密集型任务:可设置较高值(64-128)
- CPU 密集型任务:设置较低值(8-16)
timeout:根据任务处理时间设置- 短任务:10-30 秒
- 长任务:60-300 秒
6.2 长轮询超时时间设置
配置参数:
yaml
camunda:
client:
request-timeout: 60s
优化建议:
- 必须显著大于服务端长轮询等待时间(默认约 10 秒)
- 建议设置为 60 秒,保留足够余量
- 过短会导致请求超时,过长会浪费资源
6.3 流程变量设计原则
最佳实践:
- 最小化变量数量:只保留必要的变量
- 避免大对象:不要在变量中存储大对象(如文件内容)
- 使用简单类型:优先使用 String、Integer、Boolean
- 变量命名规范:使用有意义的变量名,避免歧义
反例:
java
// 不要这样做
variables.put("data", largeJsonObject); // 大对象
variables.put("temp", temporaryValue); // 临时变量
variables.put("x", value); // 无意义命名
正例:
java
// 应该这样做
variables.put("purchase_amount", 3000);
variables.put("audit_result", true);
variables.put("approver_comment", "同意采购申请");
6.4 错误处理与重试策略
重试策略设计:
| 错误类型 | 重试次数 | 重试间隔 | 处理方式 |
|---|---|---|---|
| 网络超时 | 3 | 指数退避 | 自动重试 |
| 业务异常 | 1 | 无 | 进入 Incident |
| 系统异常 | 5 | 固定间隔 | 自动重试 |
指数退避实现:
java
@JobWorker(type = "my_service", autoComplete = false)
public void myServiceJob(JobClient client, ActivatedJob job) {
try {
// 业务逻辑
client.newCompleteCommand(job.getKey()).send().join();
} catch (RetryableException e) {
// 计算退避时间
long backoffTime = (long) Math.pow(2, job.getRetries()) * 1000;
client.newFailCommand(job.getKey())
.retries(job.getRetries() - 1)
.errorMessage(e.getMessage())
.retryBackoff(Duration.ofMillis(backoffTime))
.send()
.join();
}
}