资讯详情 Spring Boot 3 集成 Flowable 7 实战:跑通 BPMN 请假审批全流程
📅 2026/10/15 5:41:51
如果你刚把项目底座升级到 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.xTomcat 和引擎各自认各的包编译期可能都瞒不过去。版本配对其实很简单你的 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 17Flowable 7.x 同样建立在 JDK 17 上。这里最容易忽略的是数据库驱动。一个很常见的现象项目好不容易升级到 Boot 3pom 里 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 groupIdorg.flowable/groupId artifactIdflowable-spring-boot-starter-process/artifactId version7.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: truedatabase-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_PROCDEFACT_RU_*Runtime运行中的流程实例、任务、变量ACT_RU_EXECUTION、ACT_RU_TASK、ACT_RU_VARIABLEACT_HI_*History历史活动、任务、流程实例ACT_HI_PROCINST、ACT_HI_TASKINST、ACT_HI_ACTINSTACT_GE_*General通用数据ACT_GE_BYTEARRAY、ACT_GE_PROPERTY看到这些表结构自动生成意味着引擎和 Spring Boot 的集成链路已经通了。第一次跑通后一定要去 H2 控制台或者 Navicat 里翻一翻这几个表族后面的“发起”“完成”再也不是黑盒。3. 流程设计的三种姿势与 BPMN 关键元素拆解3.1 设计手段怎么选在线模型器、bpmn-js 还是手写 XMLFlowable 官方提供了独立的 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 version1.0 encodingUTF-8? definitions xmlnshttp://www.omg.org/spec/BPMN/20100524/MODEL xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlns:flowablehttp://flowable.org/bpmn targetNamespacehttp://flowable.org/bpmn20 process idleaveProcess name请假审批流程 isExecutabletrue startEvent idstartEvent name发起请假/ sequenceFlow idflow1 sourceRefstartEvent targetRefmanagerTask/ userTask idmanagerTask name部门主管审批 flowable:assignee${manager} /userTask sequenceFlow idflow2 sourceRefmanagerTask targetRefdaysGateway/ exclusiveGateway iddaysGateway name天数判断 defaultflowDirectEnd/ sequenceFlow idflowToDirector sourceRefdaysGateway targetRefdirectorTask conditionExpression xsi:typetFormalExpression![CDATA[${days 3}]]/conditionExpression /sequenceFlow sequenceFlow idflowDirectEnd sourceRefdaysGateway targetRefendEvent/ userTask iddirectorTask name总监审批 flowable:assigneedirector /userTask sequenceFlow idflow5 sourceRefdirectorTask targetRefendEvent/ endEvent idendEvent 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:flowablehttp://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()ListProcessDefinition 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 关联MapString, 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引擎直接抛ActivitiObjectNotFoundExceptionFlowable 7 还兼容这个异常体系另一个是流程定义处于挂起状态说明有管理员用suspend挂了定义发起时会收到被禁止操作的异常。看日志定位都不难。6. 完成任务的关键链条TaskService 查询、complete 与条件流向6.1 待办任务查询的常用姿势流程发起后部门主管的待办里已经出现一条任务。查询待办是 TaskService 的核心场景ListTask 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 时如何带变量与改变流向完成当前任务可能还需要向后传递变量。主管审批不只是点一下“通过”还要给出审批意见和确认请假天数MapString, 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 idflowToDirector sourceRefdaysGateway targetRefdirectorTask conditionExpression xsi:typetFormalExpression![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) { MapString, Object variables new HashMap(); variables.put(manager, manager); variables.put(days, days); variables.put(starterUserId, user01); return runtimeService.startProcessInstanceByKey(leaveProcess, businessKey, variables); } public ListTask queryTasks(String businessKey, String assignee) { return taskService.createTaskQuery() .processInstanceBusinessKey(businessKey) .taskAssignee(assignee) .list(); } public void completeTask(String taskId, Boolean approved, Integer days, String comment) { MapString, 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 MapString, 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 ListMapString, 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?businessKeyLEAVE-20250101-001managermanager01days5返回processInstanceId。此时ACT_RU_TASK中出现一条待办任务办理人是manager01。第二步主管查待办curl http://localhost:8080/leave/tasks?businessKeyLEAVE-20250101-001assigneemanager01返回任务列表里面有一个taskId对应managerTask。第三步主管审批通过curl -X POST http://localhost:8080/leave/complete?taskIdtaskIdapprovedtruedays5comment同意因为days5 3流程从排他网关进入directorTask。此时再查 manager01 的待办列表已经空了ACT_RU_TASK中出现新的任务办理人director。第四步总监查待办并完成curl http://localhost:8080/leave/tasks?businessKeyLEAVE-20250101-001assigneedirector curl -X POST http://localhost:8080/leave/complete?taskIdnewTaskIdapprovedtruedays5comment同意这一步完成后ACT_RU_TASK里该流程实例的任务全部消失ACT_RU_EXECUTION中根执行实例也没了说明流程实例已正常结束。7.3 用 HistoryService 验证完整执行轨迹运行时表清空不代表流程“没了”历史表才是完整档案。用 HistoryService 可以确认整个流程已经结束HistoricProcessInstance historicProcessInstance historyService .createHistoricProcessInstanceQuery() .processInstanceBusinessKey(LEAVE-20250101-001) .singleResult(); if (historicProcessInstance.getEndTime() ! null) { // 流程实例已结束 }想看每一步详细的流转轨迹则查历史活动ListHistoricActivityInstance 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 自动注册失效这类问题。这个系列的第一步就先到这里。后面我打算接着聊流程实例的挂起与恢复、会签节点、多实例任务、驳回与退回这类真实业务里绕不开的操作。集成跑通只是开头真正的复杂度都在这些细节里。