对账流程的 OGNL 变量完整数据流

对账流程的 OGNL 变量完整数据流
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启动时怎么传 variablesController侧WorkflowController.startReconciliationPostMapping(/reconciliation/start)public MapString, Object startReconciliation(RequestParam String businessKey) { MapString, 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 是全局变量整个流程实例的所有节点都能读。关键点 2ServiceTask 设变量设了 Gateway 就能读AutoMatchDelegate.javademo 简化版真实场景会去 DB 查两方数据Component RequiredArgsConstructor Slf4j public class AutoMatchDelegate implements JavaDelegate { Override public void execute(DelegateExecution execution) { log.info( [对账] 自动比对两方数据); // 真实场景伪代码 // ListCustodyRecord custody custodyMapper.findAll(...); // ListClientRecord 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); } }关键 APIexecution.setVariable(key, value)跟 HashMap 一模一样。一旦 setBPMN 的${key}立即能读到。关键点 3Gateway 用 OGNL 表达式读变量最核心reconciliation.bpmn20.xml第 33-39 行exclusiveGateway idexclusiveGatewayDiff name差异0?/sequenceFlow idflow4_yes name是 sourceRefexclusiveGatewayDiff targetRefuserTaskAdjust conditionExpression xsi:typetFormalExpression${diffCount 0}/conditionExpression/sequenceFlowsequenceFlow idflow4_no name否 sourceRefexclusiveGatewayDiff targetRefserviceTaskWriteReconciliation conditionExpression xsi:typetFormalExpression${diffCount 0 || diffCount null}/conditionExpression/sequenceFlowOGNL 表达式能做啥表达式含义${diffCount 0}简单数值比较${diffAmount.compareTo(new BigDecimal(10000)) 0}BigDecimal 比较${riskLevel HIGH amount 1000000}复合条件${approved true}布尔值${businessType CUSTODY ? CUSTODY_REVIEW : GENERAL}三元${vars.containsKey(fastTrack)}检查变量是否存在踩坑OGNL 是JVM 内部语言new BigDecimal(...)写起来很丑。实战建议复杂计算放到 ServiceTask 里Gateway 只做简单比较。踩坑 2XML 里字符要写成gt;!-- 错误 -- conditionExpression${diffCount 0}/conditionExpression !-- 正确 -- conditionExpression${diffCount gt; 0}/conditionExpression本 demo 用了 实际能跑通是因为 Flowable 容错但严格场景要转义。关键点 4userTask 的assignee也是 OGNL 表达式userTask iduserTaskAdjust name调账处理 flowable:assignee${operator} documentation处理差异笔数: ${diffCount} 笔, 差异金额: ${diffAmount}/documentation/userTaskflowable:assignee${operator}Flowable 引擎在 userTask 创建时会从 execution variables 里找operator这个 key把值赋给 assignee。好处同一个 BPMN 文件可以服务不同的 assignee不用为每个人复制流程。实战场景业务发起流程 → 传入operator: 李四李四登录 → 看到自己的待办完成 → 流程往下走documentation也是 OGNL前端调账页面会显示处理差异笔数: 3 笔, 差异金额: 1500.00关键点 5ServiceTask 读变量Get 回来写业务表WriteReconciliationDelegate.javaComponent 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校验会更安全javaif (diffCount instanceof Integer) { // 安全使用}关键点 6local vs global 变量进阶常见踩坑java// 全局变量整个流程实例都能读最常用execution.setVariable(diffCount, 0);// Local 变量只在当前 ServiceTask / 当前 execution token 可见execution.setVariableLocal(tempCalc, 0);实战取舍场景用法原因业务数据diffCount, operatorsetVariable全局后续所有节点都需要中间计算结果临时累加setVariableLocal避免污染其他节点的命名空间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. 查两方数据 ListCustodyRecord custodyList custodyMapper.findByBusinessKey(businessKey); ListClientRecord clientList clientMapper.findByBusinessKey(businessKey); // 2. 业务比对用业务主键做差集 SetString custodyIds custodyList.stream() .map(CustodyRecord::getRecordId) .collect(Collectors.toSet()); SetString clientIds clientList.stream() .map(ClientRecord::getRecordId) .collect(Collectors.toSet()); // 3. 计算差异差集 SetString 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); } }关键设计RequiredArgsConstructorfinal字段 Lombok 自动生成构造器走 Spring 容器注入Mapper 是 MyBatis 的Spring Boot 启动时已自动配置业务主键关联businessKey把 Flowable 流程实例跟业务单据关联起来永远不要在业务表里存act_ru_execution.id这种内部 IDsetVariable 后立刻 log方便排查Gateway 走错分支这种问题4 个实战 tips踩坑现象修法1. OGNL 用了字符没转义部分 IDE 报错运行时 OK严格场景写gt;2. setVariableLocal 传业务变量后续节点读不到业务变量用setVariableglobal3. getVariable 不做 null check强转 NPEvalue ! null ? value : default4. 业务主键没贯穿到底业务表关联不上 act_ru_execution永远用businessKey关联一句话总结ServiceTask 负责算setVariableBPMN 负责判OGNL Gateway 读userTask 负责人assignee documentation下一个 ServiceTask 负责落getVariable 写业务表。