OGNL 变量 6 个来回传的关键点
数据流总览
启动流程 (Controller) │ businessKey + 初始 variables ▼ ┌─────────────────────────────────────────┐ │ serviceTaskFetchBoth (FetchBothDelegate)│ │ setVariable("custodyRecords", 1000) │ ← (1) ServiceTask 设变量 │ setVariable("clientRecords", 1000) │ └────────┬────────────────────────────────┘ ▼ ┌─────────────────────────────────────────┐ │ serviceTaskAutoMatch (AutoMatchDelegate)│ │ setVariable("diffCount", 0) │ ← (2) 算差异关键 │ setVariable("diffAmount", ZERO) │ └────────┬────────────────────────────────┘ ▼ exclusiveGatewayDiff (BPMN) ${diffCount > 0} → userTaskAdjust ← (3) Gateway 读 OGNL ${diffCount == 0} → serviceTaskWriteReconciliation ▼ (假设走 Adjust) ┌─────────────────────────────────────────┐ │ userTaskAdjust (BPMN) │ │ assignee="${operator}" │ ← (4) assignee 也是 OGNL │ documentation="差异: ${diffCount} 笔" │ ← (5) 文档可读 OGNL │ 调账 → 完成时 setVariable("operator") │ └────────┬────────────────────────────────┘ ▼ ┌─────────────────────────────────────────┐ │ serviceTaskWriteReconciliation │ ← (6) ServiceTask 读回来 │ getVariable("diffCount") │ │ getVariable("diffAmount") │ │ reconciliationService.writeReconciliation(...) │ └─────────────────────────────────────────┘关键点 1:启动时怎么传 variables
Controller侧(WorkflowController.startReconciliation):
@PostMapping("/reconciliation/start")public Map<String, Object> startReconciliation(@RequestParam String businessKey) { Map<String, Object> variables = new HashMap<>(); variables.put("operator", "李四"); // 调账操作员 variables.put("reconDate", "2026-07-31"); // 流程定义里 userTask 的 ${operator} 会被解析成 "李四" ProcessInstance pi = runtimeService.startProcessInstanceByKey( "reconciliation", businessKey, variables); return Map.of("processInstanceId", pi.getId());}注意:这里传的 variables 是全局变量,整个流程实例的所有节点都能读。
关键点 2:ServiceTask 设变量(设了 Gateway 就能读)
AutoMatchDelegate.java(demo 简化版,真实场景会去 DB 查两方数据):
@Component @RequiredArgsConstructor @Slf4j public class AutoMatchDelegate implements JavaDelegate { @Override public void execute(DelegateExecution execution) { log.info("🔍 [对账] 自动比对两方数据"); // 真实场景(伪代码): // List<CustodyRecord> custody = custodyMapper.findAll(...); // List<ClientRecord> client = clientMapper.findAll(...); // int diffCount = custody.stream() // .filter(c -> !client.contains(c)) // .collect(toList()).size(); // Demo 简化:直接设 0 走"通过"分支 execution.setVariable("diffCount", 0); execution.setVariable("diffAmount", BigDecimal.ZERO); } }关键 API:execution.setVariable("key", value)跟 HashMap 一模一样。一旦 set,BPMN 的${key}立即能读到。
关键点 3:Gateway 用 OGNL 表达式读变量(最核心)
reconciliation.bpmn20.xml第 33-39 行:
<exclusiveGateway id="exclusiveGatewayDiff" name="差异>0?"/><sequenceFlow id="flow4_yes" name="是" sourceRef="exclusiveGatewayDiff" targetRef="userTaskAdjust"> <conditionExpression xsi:type="tFormalExpression">${diffCount > 0}</conditionExpression></sequenceFlow><sequenceFlow id="flow4_no" name="否" sourceRef="exclusiveGatewayDiff" targetRef="serviceTaskWriteReconciliation"> <conditionExpression xsi:type="tFormalExpression">${diffCount == 0 || diffCount == null}</conditionExpression></sequenceFlow>OGNL 表达式能做啥:
表达式 | 含义 |
|---|---|
| 简单数值比较 |
| BigDecimal 比较 |
| 复合条件 |
| 布尔值 |
| 三元 |
| 检查变量是否存在 |
踩坑:OGNL 是JVM 内部语言,new BigDecimal(...)写起来很丑。实战建议:复杂计算放到 ServiceTask 里,Gateway 只做简单比较。
踩坑 2:XML 里>字符要写成>:
<!-- 错误 --> <conditionExpression>${diffCount > 0}</conditionExpression> <!-- 正确 --> <conditionExpression>${diffCount > 0}</conditionExpression>(本 demo 用了 > 实际能跑通是因为 Flowable 容错,但严格场景要转义。)
关键点 4:userTask 的assignee也是 OGNL 表达式
<userTask id="userTaskAdjust" name="调账处理" flowable:assignee="${operator}"> <documentation>处理差异笔数: ${diffCount} 笔, 差异金额: ${diffAmount}</documentation></userTask>flowable:assignee="${operator}":Flowable 引擎在 userTask 创建时,会从 execution variables 里找operator这个 key,把值赋给 assignee。
好处:同一个 BPMN 文件可以服务不同的 assignee,不用为每个人复制流程。
实战场景:
业务发起流程 → 传入
operator: "李四"李四登录 → 看到自己的待办
完成 → 流程往下走
documentation也是 OGNL,前端调账页面会显示:
处理差异笔数: 3 笔, 差异金额: 1500.00
关键点 5:ServiceTask 读变量(Get 回来写业务表)
WriteReconciliationDelegate.java:
@Component @RequiredArgsConstructor @Slf4j public class WriteReconciliationDelegate implements JavaDelegate { private final ReconciliationService reconciliationService; // 走 Spring 容器 @Override public void execute(DelegateExecution execution) { // 3 个变量全部 get 回来 String businessKey = execution.getProcessInstanceBusinessKey(); Integer diffCount = (Integer) execution.getVariable("diffCount"); BigDecimal diffAmount = (BigDecimal) execution.getVariable("diffAmount"); String operator = (String) execution.getVariable("operator"); log.info("💾 [对账] 写入结果: businessKey={}, diffCount={}, diffAmount={}", businessKey, diffCount, diffAmount); reconciliationService.writeReconciliation( businessKey, diffCount != null ? diffCount : 0, diffAmount != null ? diffAmount : BigDecimal.ZERO, operator); } }注意:
getProcessInstanceBusinessKey()是流程实例级别的,从启动时一路贯穿getVariable("key")可能返回null,永远要做 null check(上面diffCount != null ? diffCount : 0就是兜底)类型转换:
execution.getVariable返回Object,需要强转。转换前用instanceof校验会更安全:java
if (diffCount instanceof Integer) { // 安全使用}
关键点 6:local vs global 变量(进阶,常见踩坑)
java
// 全局变量:整个流程实例都能读(最常用)execution.setVariable("diffCount", 0);// Local 变量:只在当前 ServiceTask / 当前 execution token 可见execution.setVariableLocal("tempCalc", 0);实战取舍:
场景 | 用法 | 原因 |
|---|---|---|
业务数据(diffCount, operator) |
| 后续所有节点都需要 |
中间计算结果(临时累加) |
| 避免污染其他节点的命名空间 |
userTask 完成时传变量 | 默认是 local | 完成时仅供 outgoing flow 用 |
踩坑案例:你在 userTask 完成事件里 set 了一个变量,下一个 ServiceTask 读不到。99% 是因为用了setVariableLocal。
真实场景下 AutoMatchDelegate 长啥样
demo 简化版(默认无差异),真实场景应该是这样:
@Component @RequiredArgsConstructor @Slf4j public class AutoMatchDelegate implements JavaDelegate { private final CustodyRecordMapper custodyMapper; private final ClientRecordMapper clientMapper; @Override public void execute(DelegateExecution execution) { String businessKey = execution.getProcessInstanceBusinessKey(); log.info("🔍 [对账] 自动比对两方数据, businessKey={}", businessKey); // 1. 查两方数据 List<CustodyRecord> custodyList = custodyMapper.findByBusinessKey(businessKey); List<ClientRecord> clientList = clientMapper.findByBusinessKey(businessKey); // 2. 业务比对(用业务主键做差集) Set<String> custodyIds = custodyList.stream() .map(CustodyRecord::getRecordId) .collect(Collectors.toSet()); Set<String> clientIds = clientList.stream() .map(ClientRecord::getRecordId) .collect(Collectors.toSet()); // 3. 计算差异(差集) Set<String> diffIds = new HashSet<>(custodyIds); diffIds.removeAll(clientIds); // 托管方有,委托方没 // 4. 计算差异金额 BigDecimal diffAmount = custodyList.stream() .filter(c -> diffIds.contains(c.getRecordId())) .map(CustodyRecord::getAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); // 5. 设变量(Gateway 立即能读) execution.setVariable("diffCount", diffIds.size()); execution.setVariable("diffAmount", diffAmount); log.info("🔍 [对账] 差异笔数={}, 差异金额={}", diffIds.size(), diffAmount); } }关键设计:
@RequiredArgsConstructor+final字段 = Lombok 自动生成构造器,走 Spring 容器注入(Mapper 是 MyBatis 的,Spring Boot 启动时已自动配置)业务主键关联:
businessKey把 Flowable 流程实例跟业务单据关联起来,永远不要在业务表里存act_ru_execution.id这种内部 IDsetVariable 后立刻 log:方便排查"Gateway 走错分支"这种问题
4 个实战 tips
踩坑 | 现象 | 修法 |
|---|---|---|
1. OGNL 用了 | 部分 IDE 报错,运行时 OK | 严格场景写 |
2. setVariableLocal 传业务变量 | 后续节点读不到 | 业务变量用 |
3. getVariable 不做 null check | 强转 NPE |
|
4. 业务主键没贯穿到底 | 业务表关联不上 act_ru_execution | 永远用 |
一句话总结
ServiceTask 负责"算"(setVariable),BPMN 负责"判"(OGNL Gateway 读),userTask 负责"人"(assignee + documentation),下一个 ServiceTask 负责"落"(getVariable + 写业务表)。