租赁申请需要店员审核,审核期间服务可能重启。后来流程增加主管复核,已经提交的旧申请是否也要补一道审核,不能只靠替换一个流程文件回答。等待中的实例、任务归属和流程版本都必须有持久记录。

本篇将两份 BPMN 模型部署到真实 Camunda 7.23 引擎,以 H2 文件数据库保存等待状态。第一次创建人任务后直接终止 JVM,再用新进程恢复,随后部署第二版,比较新旧实例的路径及普通代码的同一转换规则。

引擎承担等待和继续的执行

普通业务代码可以计算下一步是审核还是结束,但跨越数小时的人工等待还涉及任务持久化、恢复、查询和历史记录。工作流引擎把这些执行机制集中起来,领域规则仍需由应用明确提供。流程节点存在,不表示审批权限或租赁条件已经正确。

BPMN 是流程模型与表示规范;具体引擎只执行自己支持的元素和扩展。本实验使用开始事件、顺序流、人任务和结束事件,没有覆盖整个 BPMN 标准,也没有把能够解析 XML 等同于全部语义通过。OMG BPMN 2.0.2

Camunda 的 User Task 在流程到达时创建等待处理的任务,应用可以查询并完成任务。样本直接调用引擎 API 完成它,模拟合法处理动作,未部署 Tasklist 或登录界面。因此验证的是持久化人任务的执行语义,用户界面的可用性和权限控制需要单独验证。User Task

flowchart LR
  M1[BPMN版本一] --> R[Repository部署]
  M2[BPMN版本二] --> R
  R --> E[Camunda执行引擎]
  E --> D[H2文件库 任务和实例]
  A[应用TaskService] -->|完成指定任务| E
  E --> H[历史实例与任务]
  X[JVM退出] --> N[新JVM重建引擎]
  D --> N

引擎采用 standalone 配置,固定 H2 2.1.214,历史级别为 full,关闭 job executor 和运行指标。数据库建表由引擎自动完成,适合隔离实验;正式环境通常需要受控数据库迁移和运维策略,不能直接把自动建表配置复制过去。Process Engine Bootstrapping

关闭后台 job executor 不影响本例通过 API 完成人任务,但意味着没有实际验证定时器、异步重试或后台作业恢复。实验中的恢复来自重新启动引擎并查询同一个数据库,不依赖一条后台线程持续存活。

创建等待以后终止整个进程

第一版模型使用固定 process key rentalApproval,流程从开始事件进入 approve 人任务,再到结束。任务 assignee 为 clerk。部署以后按业务键 R1 启动实例,查询任务定义键确认为 approve,说明引擎确实停在等待节点。

随后程序输出实例编号并调用 Runtime.halt,以七十三结束,跳过正常 engine.close。此时任务创建命令已经完成提交。运行器保留数据库文件并记录预期退出码,再启动全新的 Java 命令,进程内对象没有继续沿用。

重启步骤按业务键找到 R1 实例,再查询该实例只有一个待办任务。持久状态来自引擎数据库,不是运行器在 JSON 中记住“下一步审核”以后重新创建一个假任务。最终 SQL 导出也包含引擎真实表结构与行数据,可检查执行与历史记录。

这个故障点位于引擎命令返回之后,因此证明的是已提交等待状态能够恢复。它没有在引擎数据库事务中途断电,也没有测试磁盘损坏。若需要后者,应改变故障注入位置并核对数据库恢复行为,不能用同一个七十三退出码概括所有崩溃条件。

新版本不改写旧实例的承诺

第二版保留相同 process key,在 approve 之后增加 supervisor 人任务,再结束。新进程部署第二版,然后按相同流程键启动 R2。程序分别查询两个实例引用的流程定义,断言 R1 对应版本一,R2 对应版本二。

flowchart TD
  V1[版本一 定义] --> A1[R1 店员审核]
  A1 --> E1[R1结束]
  V2[版本二 定义] --> A2[R2 店员审核]
  A2 --> S2[R2 主管复核]
  S2 --> E2[R2结束]
  S2 --> P[进程关闭后再次启动]
  P --> W[恢复同一主管待办]

完成 R1 的 approve 后,运行实例查询为零,旧流程按原定义结束。完成 R2 的 approve 后,查询到 supervisor 待办。相同的第一步完成动作产生不同后续路径,差异来自实例绑定的定义版本,而不是根据当前文件内容临时猜测。

这个实验采取旧实例继续旧版本的策略,没有调用流程实例迁移 API。若业务要求旧申请也必须主管复核,就需要明确迁移范围、当前节点映射和已完成动作的处理。部署新定义本身不能替代这一决策,旧实例自动改变路径也不一定符合原先承诺。

第二个进程在主管任务等待时正常关闭引擎。第三个 JVM 启动以后,重新找到 R2 的 supervisor 任务,并断言 assignee 为 supervisor,再完成任务。两次跨进程恢复分别覆盖异常终止后的店员待办和正常关闭后的主管待办。

assignee 字段说明任务分派结果,但不是权限检查的完整证据。测试程序持有引擎 API 访问能力,直接完成指定任务。真实应用仍需判断操作者是否为主管、是否允许代理以及是否要求双人复核,不能只凭任务上有一个名字就宣称授权有效。

与普通代码比较的范围

实验同时提供 plainNext 函数:版本一从 approve 到 END,版本二从 approve 到 supervisor,再从 supervisor 到 END。每次引擎推进以后,把实际下一节点与这份普通代码规则比较,确认流程模型没有改变预期业务顺序。

这份基线只比较状态转换,没有实现完整的持久化任务系统。因此不能拿它的代码行数直接证明引擎更省代码,也不能说它已经具备相同的崩溃恢复能力。公平比较应当把任务存储、领取、恢复、审计和版本兼容都纳入成本,而不是只比较一个 switch 和一份 XML。

如果业务只有一次同步校验,普通函数通常足够直接。出现长期等待、人工任务、多个版本并存以及运维查询需求时,引擎才有更明确的作用。即使采用引擎,设备可租条件、计价规则和权限判断也不应全部写进顺序流表达式,否则领域语义会散布到多个模型文件。

规则引擎与工作流引擎解决的核心问题不同。前者根据输入决定结果,后者保存执行进度并协调后续动作。本篇没有运行规则引擎,没有测试规则重叠或优先级;两版审批模型的路径变化也不能充当规则表覆盖证明。需要这些能力时,应建立独立规则输入、输出和冲突验收。

用历史数据检查真正结束

主管完成以后,运行实例总数为零,历史服务查到两个已结束实例和三个已完成的人任务。程序还通过 JDBC 读回 ACT_RU_TASK 行数为零,并输出 ACT_HI_PROCINST 中的流程定义编号、业务键和终态。

这几项检查对应不同结论。没有运行任务说明当前没有待办,历史实例说明曾经启动并结束,历史任务说明审核节点实际执行过。单独查询待办为零,也可能只是流程从未启动,因此需要这些结果组合起来支持完整路径。

独立负例重新创建实验场景,在第二版主管待办仍存在时调用 complete 处理不存在的任务编号。真实引擎抛出异常并使进程退出一,没有静默推进其他任务。负例数据库快照保留尚未完成的流程,便于确认错误调用没有被误判成正常结束。

历史保存也有成本。模型配置历史存活期限,但本实验关闭后台作业,没有执行清理。合规保留、个人信息删除、历史索引和恢复工具的权限都需要部署层方案,不能因为历史表自动生成就省略这些安排。

采用和退出引擎的条件

采用之前可以用本例的三个问题验收候选方案:等待以后进程退出能否恢复,新旧版本能否同时存在,异常操作是否留下可查询结果。还应由业务决定哪些人工任务需要记录负责人和处理凭证,避免仅为了画图而引入新的运行平台。

退出引擎也应先处理在途实例。可以停止接收新流程,让旧实例在原引擎完成,同时由新实现承接新请求;若必须迁移,则要保存业务键、当前等待节点、任务归属和历史关联。只导出 BPMN 文件无法恢复已经运行到一半的实例。

这个实验选择固定历史版本以保证复跑,不构成对该版本长期生产支持的推荐。引擎升级涉及数据库结构、扩展属性和已有实例,应按目标版本文档重新验证。标准模型能减少部分表达差异,却不会自动消除运行时 API 和运维工具的迁移成本。

读者复跑

下载 实验包,保留 examples 目录,在 Java 21 环境执行入口。依赖清单锁定 Camunda Engine 7.23.0 及传递依赖摘要,缺失 jar 从官方 Maven Central 下载。

1
bash examples/enterprise-application-architecture/labs/19/run.sh

正常入口接受第一次七十三退出,继续执行两个恢复进程,最终返回零。追加 negative 则在不存在任务的真实引擎错误处返回一。原始日志、退出码、引擎数据库 SQL 快照和临时目录路径保存在证据目录,临时数据库保留供复核。