ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot 3 集成 Flowable 7 实战:跑通 BPMN 请假审批全流程

Spring Boot 3 集成 Flowable 7 实战:跑通 BPMN 请假审批全流程

如果你刚把项目底座升级到 Spring Boot 3.x,又被审批、会签、状态流转这些需求反复折磨过,大概率会开始看 Flowable 7.x。我最初是在实现一个审批中心重构时接触它,踩完版本坑、跑通一条最基础的请假流程之后,才发现这引擎真正值钱的地方不只是“把流程画出来”,而是把流程定义、运行状态、历史追溯整条链路都抽象成了可查询的模型。这篇是系列第一篇,说清楚一件事:从 Spring Boot 集成开始,到设计 BPMN、部署定义、发起实例、完成任务,一条直线先把通路走完。适合刚接 Flowable、或者还在用老版本想迁移到 7.x 的 Spring Boot 开发者,照着敲就能跑起来。

1. Spring Boot 3.x 与 Flowable 7.x 的版本配对,先躲开两个大坑

1.1 为什么是 Flowable 7 而不是老版本

很多人搜索引擎一搜就是“Spring Boot 集成 Flowable”的旧文章,代码抄下来发现启动直接报ClassNotFoundException: javax.servlet.*。这不是姿势问题,是基线问题:Spring Boot 3.0 全面切换到 Jakarta EE 9+ 的jakarta.*命名空间,而 Flowable 6.x 及更早版本仍然构建在javax.*之上。你在 Spring Boot 3 项目里塞进 Flowable 6.x,Tomcat 和引擎各自认各的包,编译期可能都瞒不过去。

版本配对其实很简单:

你的 Spring Boot 版本建议的 Flowable 主版本备注
2.4 ~ 2.7Flowable 6.x老项目维护首选
3.0 ~ 3.xFlowable 7.x基于 Jakarta EE 9+,官方推荐
4.x(若有早期预览)以官方发布说明为准一般向下兼容 7.x 的模型

Flowable 7.x 是我用下来第一次感觉到“跟 Boot 3 是同一代人”的版本线。它不但改掉了命名空间问题,还对 Spring Boot 3 的自动配置做了对齐,依赖注入、配置项绑定方式都比老版本舒服。所以如果你确定要用 Spring Boot 3,不要纠结,直接跳过 6.x 的所有中文教程,只参考 7.x 的资料。

1.2 JDK 17 与数据库驱动的连带升级

Spring Boot 3.x 的最低要求是 JDK 17,Flowable 7.x 同样建立在 JDK 17 上。这里最容易忽略的是数据库驱动。一个很常见的现象:项目好不容易升级到 Boot 3,pom 里 MySQL 驱动还停留在老的 5.x 或者早期 8.x,启动时就会冒出奇怪的类加载异常或者驱动初始化失败。

我建议把连接 MySQL 的驱动版本至少提到 8.0.33 之后的版本,一方面是对 JDK 17 的模块化支持更完整,另一方面是 Flowable 建表时执行的 DDL 里会用到的字符集、时区相关特性,老驱动容易在边界上出问题。测试阶段我更推荐直接用 H2 内存库,省掉外部数据库负担,Flowable 对 H2 的兼容脚本一直维护得很好。等逻辑验证完再切回 MySQL、PostgreSQL,换数据源只是改配置的事,业务代码不用动。

2. 把 flowable-spring-boot-starter 请进项目的配置清单与启动验证

2.1 Maven 依赖的写法和依赖收敛

Flowable 在 Spring Boot 场景下提供了一堆 starter,最常用的是flowable-spring-boot-starter-process。它只引入流程引擎核心模块,BPMN 解析、运行时服务、历史服务都够用。有些文章推荐直接引flowable-spring-boot-starter,那个会把 CMMN、DMN、App、Form 等一堆引擎全部带进来,启动时多初始化好几十张表,开发调试很不痛快。

<dependency> <groupId>org.flowable</groupId> <artifactId>flowable-spring-boot-starter-process</artifactId> <version>7.0.1</version> </dependency>

版本号建议看官方 release 页拿当时最新稳定版,别锁太旧。这个 starter 会自动注册ProcessEngine,并把RuntimeService、TaskService、RepositoryService、HistoryService等 Bean 暴露到 Spring 容器里。也就是说,你不需要手动去ProcessEngineConfiguration里拼 Bean,直接@Autowired就能用,这是 7.x 省心的地方。

2.2 application.yml 里必须说明白的几个配置项

spring: datasource: url: jdbc:h2:mem:flowable;DB_CLOSE_DELAY=-1 driver-class-name: org.h2.Driver username: sa password: "" flowable: database-schema-update: true async-executor-activate: false db-history-used: true

database-schema-update: true表示让 Flowable 启动时自动检查并创建所需表。开发阶段这个开关非常省事,第一次启动会刷出一批ACT_*表。如果你换成 MySQL,同样可以先开成 true,让引擎自己建表,但要注意数据库账号必须有 DDL 权限。

async-executor-activate: false是很多教程没写但很关键的配置。Flowable 默认带异步执行器,负责处理异步连续、定时器等场景。如果你现在就跑最简单的同步流程,把它开成 true 其实也不影响,但会额外起 JobExecutor 线程。我习惯在纯同步演示里直接关掉,少一个干扰源。

db-history-used: true打开历史记录。流程引擎如果不写历史,后面排查问题会非常痛苦,建议从一开始就开着。

2.3 启动验证要点

启动日志里看到类似Flowable Engine启动耗时、数据库建表语句执行成功的输出,基本就成功了。然后进入数据库,你会看到四大类表:

表前缀含义典型表
ACT_RE_*Repository,流程定义、部署包等静态资源ACT_RE_DEPLOYMENT、ACT_RE_PROCDEF
ACT_RU_*Runtime,运行中的流程实例、任务、变量ACT_RU_EXECUTION、ACT_RU_TASK、ACT_RU_VARIABLE
ACT_HI_*History,历史活动、任务、流程实例ACT_HI_PROCINST、ACT_HI_TASKINST、ACT_HI_ACTINST
ACT_GE_*General,通用数据ACT_GE_BYTEARRAY、ACT_GE_PROPERTY

看到这些表结构自动生成,意味着引擎和 Spring Boot 的集成链路已经通了。第一次跑通后一定要去 H2 控制台或者 Navicat 里翻一翻这几个表族,后面的“发起”“完成”再也不是黑盒。

3. 流程设计的三种姿势与 BPMN 关键元素拆解

3.1 设计手段怎么选:在线模型器、bpmn-js 还是手写 XML

Flowable 官方提供了独立的 Flowable UI,可以在本地跑起来画 BPMN 图,适合快速原型验证。如果你要把流程设计器嵌进自己的产品里,前端领域更主流的方案是集成 bpmn-js 做画布,后端只负责接收 bpmn20.xml 文件并部署。两者都不是非此即彼,甚至可以先在线模型器画好,导出 XML 再交给后端。

但我的观点很明确:刚入门阶段,强烈建议手写一份 XML。原因有三个:第一,BPMN 的语义细节(事件、网关、任务类型)在 XML 里一览无余,比可视化拖拽更能建立心智模型;第二,XML 是 Git 里可 diff 的文本,比二进制模型文件更容易做版本管理;第三,后续排查线上流程问题,你要读的最终就是引擎解析后的 XML 定义。设计器可以帮你生成初始文件,但理解还得靠 XML。

3.2 一个最小可用的请假审批 BPMN 文件

下面这份文件是标准 BPMN 2.0,兼容 Flowable 7.x。流程逻辑是:发起后先到“部门主管审批”,主管通过后判断请假天数,超过 3 天加一道“总监审批”,否则直接结束。

<?xml version="1.0" encoding="UTF-8"?> <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:flowable="http://flowable.org/bpmn" targetNamespace="http://flowable.org/bpmn20"> <process id="leaveProcess" name="请假审批流程" isExecutable="true"> <startEvent id="startEvent" name="发起请假"/> <sequenceFlow id="flow1" sourceRef="startEvent" targetRef="managerTask"/> <userTask id="managerTask" name="部门主管审批" flowable:assignee="${manager}"> </userTask> <sequenceFlow id="flow2" sourceRef="managerTask" targetRef="daysGateway"/> <exclusiveGateway id="daysGateway" name="天数判断" default="flowDirectEnd"/> <sequenceFlow id="flowToDirector" sourceRef="daysGateway" targetRef="directorTask"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${days > 3}]]></conditionExpression> </sequenceFlow> <sequenceFlow id="flowDirectEnd" sourceRef="daysGateway" targetRef="endEvent"/> <userTask id="directorTask" name="总监审批" flowable:assignee="director"> </userTask> <sequenceFlow id="flow5" sourceRef="directorTask" targetRef="endEvent"/> <endEvent id="endEvent" name="结束"/> </process> </definitions>

放到src/main/resources/processes/leave.bpmn20.xml下,启动时 Flowable 的自动部署机制会扫描到并部署它。注意process元素的id是leaveProcess,后面发起流程时这个 id 就是流程定义 key。name属性只是展示名称,不参与引擎寻址。

3.3 BPMN 元素命名规范与容易踩的命名陷阱

startEvent表达流程起点,endEvent表达终点,sequenceFlow就是节点之间的有向连线。userTask表示人工任务,必须有办理人维度。上例里用的flowable:assignee="${manager}"是 Flowable 的扩展方式,等于说“这个任务的办理人由流程变量 manager 的值动态决定”。既然用了flowable:前缀,就必须在definitions根节点上声明xmlns:flowable="http://flowable.org/bpmn",否则部署会直接报解析错误。

命名时两个原则:一是所有元素 id 尽量用英文驼峰,不要带中文、空格和奇怪符号,后续在 API 里拼条件表达式、按 id 定位节点会非常痛苦;二是process id一旦确定就不要随意改,它是业务对流程定义的稳定引用。内容展示用 name 去改就够了,id 改了等于所有历史流程定义对不上号。

4. 部署流程定义:RepositoryService 的完整用法与校验

4.1 部署的标准姿势与背后的资源存储

你可以不依赖启动时自动扫描,而是在应用启动后主动调用部署接口。这样更可控,也方便做权限控制。部署代码基本长这样:

Deployment deployment = repositoryService.createDeployment() .addClasspathResource("processes/leave.bpmn20.xml") .name("leaveProcess-部署-1") .deploy();

addClasspathResource会从 classpath 读取 BPMN 文件,name是部署包的名称,deploy()执行后返回Deployment对象。引擎会把 XML 原始字节保存到ACT_GE_BYTEARRAY,把解析出来的流程定义写入ACT_RE_PROCDEF。以后查询“我的流程是怎么部署的”只需要盯这两张表,一个是部署包,一个是部署包里解析出的定义。

4.2 部署结果验证:API 查询与表结构双查

部署完成别急着发起,先用 API 确认流程定义是否可用。我用的是RepositoryService.createProcessDefinitionQuery():

List<ProcessDefinition> definitions = repositoryService .createProcessDefinitionQuery() .processDefinitionKey("leaveProcess") .orderByProcessDefinitionVersion() .desc() .list(); for (ProcessDefinition pd : definitions) { System.out.println(pd.getId()); // leaveProcess:1:xxx System.out.println(pd.getVersion()); // 1 System.out.println(pd.isSuspended()); // false }

ProcessDefinition.getId()返回形如leaveProcess:1:4e2b1a9c-a1bc-4a23-9bcd-abc的字符串,这是流程定义在引擎里的稳定主键。中间的那个1是版本号,后面那段是随机生成的内部 id。发起流程时既可以用这个完整 id,也可以用 key。

数据库层面,ACT_RE_PROCDEF表中会出现一行,VERSION_为 1。看到这一行,部署环节才算闭环。

4.3 重复部署、版本爆炸与 schema 升级的坑

deploy()每次执行都会生成新版本。开发阶段如果每次重启都调一次部署,版本号会一路涨到几十上百。我一开始不以为然,后来在测试环境发现ACT_RE_PROCDEF里积累了二十多份同样的定义,虽然不影响功能,但排查问题时看着心烦,而且生产环境版本无意义增长不等于资源浪费,等于把历史包袱越堆越大。

更严重的问题是:如果你改变了流程 XML 里的流程定义结构,旧实例还在按老版本跑,新实例走新版本,两边行为不一致。解决思路是给部署加幂等判断:部署前先按processDefinitionKey查一遍,如果已存在且且业务要求保持当前版本,就不重复部署;只有确认需要变更时才调 deploy。另外database-schema-update: true在生产环境一定要关掉,改用手动执行官方 SQL 脚本。引擎升级时表结构的变更应该由 DBA 评审后再执行,而不是让应用启动时偷偷改表。

5. 发起流程实例:RuntimeService 与业务数据的绑定

5.1 发起方式的对比与推荐姿势

Flowable 提供多种发起方式:按定义 id、按定义 key、按 message 事件等。实际业务里我几乎只用startProcessInstanceByKey,因为 key 是业务语义稳定的标识,不受版本号影响,代码里写死也不容易断。按 id 发起虽然精确,但 id 里带着版本号,代码稍微一变动版本就漂移。

发起时带上业务流程自己的单据号,通过 businessKey 关联:

Map<String, Object> variables = new HashMap<>(); variables.put("manager", "manager01"); variables.put("days", 5); variables.put("starterUserId", "user01"); ProcessInstance processInstance = runtimeService .startProcessInstanceByKey("leaveProcess", "LEAVE-20250101-001", variables);

第三个参数businessKey是业务系统自己的业务单据号。我这里用的LEAVE-20250101-001就是请假单号。之后你想知道“这条请假单跑到哪了”,直接createProcessInstanceQuery().processInstanceBusinessKey("LEAVE-20250101-001")查就行,不用把流程实例 id 到处传。

5.2 流程变量的作用域与存储位置

发起时传入的 variables 会成为流程实例级变量,存储在ACT_RU_VARIABLE表里。这些变量在整个流程实例生命周期内可见,任务节点、网关条件、服务任务都能读取。变量类型需要注意:数字类型默认是 Integer 还是 Long,取决于你传的 Java 类型。条件表达式里如果是days > 3,传整型不会出错,但涉及金额、比率时我建议一律传 BigDecimal,否则浮点精度问题会莫名其妙地让条件判断失灵。

流程实例和流程变量不是一对一关系,同一流程实例的不同并发分支可能有自己的局部变量。但第一篇文章先不要碰并发分支,记住“发起时传的 variables 是全局的”就够了。

5.3 发起后立刻要做的状态检查

发起成功后,ProcessInstance对象里最有用的两个方法是getId()和getProcessDefinitionId()。前者是流程实例的唯一主键processInstanceId,后者记录了当前走的哪一版定义。

这时去查ACT_RU_EXECUTION,能看到一条根执行流记录;再查ACT_RU_TASK,能看到当前待办任务,也就是 BPMN 里定义的“部门主管审批”。如果发起后ACT_RU_TASK里没有任务,别慌,先看流程是不是结束得很快,比如第一个节点后面直接是结束事件。如果ProcessInstance还在但没任务,那多半是流程定义里根本没画用户任务,或者节点被设成了自动跳过。

发起阶段最容易报的两个错:一个是 key 不存在,ProcessDefinition没部署或者拼错 key,引擎直接抛ActivitiObjectNotFoundException(Flowable 7 还兼容这个异常体系);另一个是流程定义处于挂起状态,说明有管理员用suspend挂了定义,发起时会收到被禁止操作的异常。看日志定位都不难。

6. 完成任务的关键链条:TaskService 查询、complete 与条件流向

6.1 待办任务查询的常用姿势

流程发起后,部门主管的待办里已经出现一条任务。查询待办是 TaskService 的核心场景:

List<Task> tasks = taskService.createTaskQuery() .processInstanceBusinessKey("LEAVE-20250101-001") .taskAssignee("manager01") .list(); for (Task task : tasks) { System.out.println(task.getId()); // 任务主键 System.out.println(task.getName()); // 部门主管审批 System.out.println(task.getProcessInstanceId()); }

taskAssignee是“这个任务已经明确分配给某人”,也就是 BPMN 里flowable:assignee指定的值。另有taskCandidateUser对应候选人的场景,候选人是“一堆人都可以认领”的任务,必须先调taskService.claim(taskId, username)完成认领后才变成某人的待办。这个区别很容易混,记住一句话:assignee 是已经指名道姓,candidate 是放在池子里等认领。

6.2 complete 时如何带变量与改变流向

完成当前任务,可能还需要向后传递变量。主管审批不只是点一下“通过”,还要给出审批意见和确认请假天数:

Map<String, Object> taskVariables = new HashMap<>(); taskVariables.put("approved", true); taskVariables.put("days", 5); taskVariables.put("managerComment", "同意"); taskService.complete(task.getId(), taskVariables);

complete(taskId, variables)里的变量会被设置到流程实例的作用域中,也就是说后面网关节点再判断approved、days都能拿到最新值。任务完成后ACT_RU_TASK中对应记录消失,而ACT_HI_TASKINST中会写入该任务的完成时间。老手查看流程到哪了从来不看运行时表,只看历史表,因为运行时表是“当前那一刻”的瞬时状态,历史表才是完整履历。

6.3 排他网关与 conditionExpression:审批分支到底怎么走

在 BPMN 里控制分支的标准方式是sequenceFlow上加条件表达式:

<sequenceFlow id="flowToDirector" sourceRef="daysGateway" targetRef="directorTask"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${days > 3}]]></conditionExpression> </sequenceFlow>

这段 XML 的含义是:当引擎执行到daysGateway这个排他网关时,检查从网关出去的每一条连线上的条件表达式,按 XML 里定义的顺序依次求值,碰到第一个为 true 的连线就走那条。如果所有条件都不为 true,就走default指定的连线。所以排他网关的“兜底”一定要在 XML 里显式配置,否则引擎会报“没有符合条件的出线”错误。

我在第一步 BPMN 里给网关配的默认连线是flowDirectEnd,意味着主管审批通过后,如果请假天数不大于 3 天就直接结束;大于 3 天则流到directorTask。这正好解释了为什么主管审批的 complete 调用里必须带上days,不然引擎在网关处会拿不到变量而抛异常。

7. 串起来看:一条请假申请从发起到审批结束的全链路实测

7.1 项目结构与本例的代码骨架

按前面思路,最小项目结构如下:

src/main/java/com/example/flowabledemo/ FlowableDemoApplication.java service/LeaveProcessService.java web/LeaveController.java src/main/resources/ application.yml processes/leave.bpmn20.xml

启动类不需要特殊代码,标准@SpringBootApplication即可。Service 层把发起、查待办、完成任务三个操作封装好:

@Service public class LeaveProcessService { private final RuntimeService runtimeService; private final TaskService taskService; private final HistoryService historyService; public LeaveProcessService(RuntimeService runtimeService, TaskService taskService, HistoryService historyService) { this.runtimeService = runtimeService; this.taskService = taskService; this.historyService = historyService; } public ProcessInstance startLeave(String businessKey, String manager, Integer days) { Map<String, Object> variables = new HashMap<>(); variables.put("manager", manager); variables.put("days", days); variables.put("starterUserId", "user01"); return runtimeService.startProcessInstanceByKey("leaveProcess", businessKey, variables); } public List<Task> queryTasks(String businessKey, String assignee) { return taskService.createTaskQuery() .processInstanceBusinessKey(businessKey) .taskAssignee(assignee) .list(); } public void completeTask(String taskId, Boolean approved, Integer days, String comment) { Map<String, Object> variables = new HashMap<>(); variables.put("approved", approved); variables.put("days", days); variables.put("comment", comment); taskService.complete(taskId, variables); } }

Controller 层对外暴露三个接口,分别对应发起、查询待办、完成任务。

@RestController @RequestMapping("/leave") public class LeaveController { private final LeaveProcessService leaveProcessService; public LeaveController(LeaveProcessService leaveProcessService) { this.leaveProcessService = leaveProcessService; } @PostMapping("/start") public Map<String, String> start(@RequestParam String businessKey, @RequestParam String manager, @RequestParam Integer days) { ProcessInstance pi = leaveProcessService.startLeave(businessKey, manager, days); return Map.of("processInstanceId", pi.getId()); } @GetMapping("/tasks") public List<Map<String, String>> tasks(@RequestParam String businessKey, @RequestParam String assignee) { return leaveProcessService.queryTasks(businessKey, assignee) .stream() .map(task -> Map.of( "taskId", task.getId(), "taskName", task.getName(), "assignee", task.getAssignee() )) .toList(); } @PostMapping("/complete") public String complete(@RequestParam String taskId, @RequestParam Boolean approved, @RequestParam(required = false) Integer days, @RequestParam(required = false) String comment) { leaveProcessService.completeTask(taskId, approved, days, comment); return "complete ok"; } }

7.2 接口调用顺序与每步的状态变化

场景:业务单号LEAVE-20250101-001,发起人 user01,主管 manager01,请假 5 天,主管审批通过。

第一步发起:

curl -X POST "http://localhost:8080/leave/start?businessKey=LEAVE-20250101-001&manager=manager01&days=5"

返回processInstanceId。此时ACT_RU_TASK中出现一条待办,任务办理人是manager01。

第二步主管查待办:

curl "http://localhost:8080/leave/tasks?businessKey=LEAVE-20250101-001&assignee=manager01"

返回任务列表,里面有一个taskId,对应managerTask。

第三步主管审批通过:

curl -X POST "http://localhost:8080/leave/complete?taskId=<taskId>&approved=true&days=5&comment=同意"

因为days=5 > 3,流程从排他网关进入directorTask。此时再查 manager01 的待办列表,已经空了;ACT_RU_TASK中出现新的任务,办理人director。

第四步总监查待办并完成:

curl "http://localhost:8080/leave/tasks?businessKey=LEAVE-20250101-001&assignee=director" curl -X POST "http://localhost:8080/leave/complete?taskId=<newTaskId>&approved=true&days=5&comment=同意"

这一步完成后,ACT_RU_TASK里该流程实例的任务全部消失,ACT_RU_EXECUTION中根执行实例也没了,说明流程实例已正常结束。

7.3 用 HistoryService 验证完整执行轨迹

运行时表清空不代表流程“没了”,历史表才是完整档案。用 HistoryService 可以确认整个流程已经结束:

HistoricProcessInstance historicProcessInstance = historyService .createHistoricProcessInstanceQuery() .processInstanceBusinessKey("LEAVE-20250101-001") .singleResult(); if (historicProcessInstance.getEndTime() != null) { // 流程实例已结束 }

想看每一步详细的流转轨迹,则查历史活动:

List<HistoricActivityInstance> activities = historyService .createHistoricActivityInstanceQuery() .processInstanceId(historicProcessInstance.getId()) .orderByHistoricActivityInstanceStartTime() .asc() .list();

这个列表会按时间顺序给出startEvent、managerTask、exclusiveGateway、directorTask、endEvent的执行记录,包括每个节点的进入和离开时间。以后业务方问你“这条单子谁批的、批了多久”,答案全在ACT_HI_ACTINST里,不用再去翻运行时表。

我再提醒一句:如果你发现流程实例结束了但历史表查不到明细,先检查db-history-used是否开了。这个配置默认就开,但有些人为了让启动日志干净会把它关掉,代价是永久失去追溯能力。

8. 集成完成后最容易忽视的三件事

8.1 生产模式必须切换数据库初始化策略

开发期database-schema-update: true很爽,引擎自动把表建好。但到了生产环境,这条配置必须关掉,改由 DBA 手动执行官方对应版本的 SQL 脚本,并且严格按库选择脚本。原因很直接:自动建表只能保证“表存在”,不能保证表结构变更经过评审,而流程引擎的表结构直接关系到已运行实例的稳定性。最稳妥的发布流程是:先备好 SQL 脚本,在预发布库验证,再同步到生产库,最后才把应用切过去。

8.2 别一上来就往流程里塞大量脚本任务

看着 BPMN 里可以写serviceTask、脚本任务、JavaDelegate,老手都会先忍住。把业务逻辑大量写进流程引擎的脚本里,后期调试、版本比对、权限管理都会很别扭。我目前比较推崇的方式是:流程只负责编排和人工审批节点,真正的业务动作通过 Delegate 指向 Spring 管理的 Bean,让外部服务去执行。流程引擎负责“什么时候做”,业务代码负责“做什么”,边界干净,后续扩展也容易。

8.3 异步执行器和流程引擎的版本升级计划

如果你不需要异步连续、定时器事件、事务边界后置处理,第一版尽量保持async-executor-activate: false,减少 JobExecutor 对数据库的额外轮询。等真正出现异步场景再打开也不迟。另一点就是 Flowable 7.x 和 Spring Boot 3.x 的版本耦合比看起来更紧,升级流程引擎时不要只看主版本号,要顺手核对 starter 对应的 Spring Boot 兼容范围,不然会遇到配置项不识别或 Bean 自动注册失效这类问题。

这个系列的第一步就先到这里。后面我打算接着聊流程实例的挂起与恢复、会签节点、多实例任务、驳回与退回这类真实业务里绕不开的操作。集成跑通只是开头,真正的复杂度都在这些细节里。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进