1. Spring Boot 3.x 与 Flowable 7.x 的版本配对先跨过 javax 到 jakarta 这道坎如果你最近正想把老项目从 Spring Boot 2.x 升到 3.x同时又把工作流组件换成 Flowable 7.x那你多半会撞上第一个拦路虎包名迁移。Spring Boot 3.0 底层基于 Spring Framework 6全面切换到 Jakarta EE 9所有javax.servlet、javax.persistence、javax.validation这类包名都变成了jakarta.*。而 Flowable 6.x 还是基于旧的javax.*规范编译的直接和 Spring Boot 3.x 配套使用启动时大概率会遇到类找不到、Bean 初始化失败这类问题。这不是你代码写错了是两代技术栈的底层约定不一致。1.1 为什么 Spring Boot 3.0 之后不能继续用 Flowable 6很多做后端的老哥看到项目升级第一反应是“先跑起来再说”于是把 Spring Boot 从 2.7 升到 3.2然后顺手引入了flowable-spring-boot-starter-process的 6.8.0 版本。看起来依赖没报错但应用一启动日志里会出现类似ClassNotFoundException: javax.xml.bind.JAXBException或者持久层相关的jakarta.persistence异常。原因就在于 Flowable 6 内部很多类直接引用了javax.persistence、javax.xml.bind这些旧包而 JDK 17 加 Spring Boot 3 的环境里这些包既不在 JDK 中也不再被 Spring Boot 的管理 BOM 统一替换。那是不是加一个javax.xml.bind:jaxb-api依赖就能解决我试过能解决一部分编译问题但治标不治本。因为 Flowable 6 的整个注解体系、MyBatis 映射、Servlet 相关过滤器都是围绕javax.*写的Spring Boot 3 的自动配置在扫描这些类时依然会半残。最干净的办法就是直接升到 Flowable 7.x这是官方为了适配 Spring Boot 3 而做的重大版本调整。Flowable 7 把底层依赖的包名全部换成了jakarta.*并且最低要求 JDK 17和 Spring Boot 3 属于同一代技术底座。1.2 我这边实测稳定的版本组合与依赖清单我在生产环境验证过一套比较省心的组合JDK 17 Spring Boot 3.2.x Flowable 7.x MySQL 8.0。Flowable 7 的小版本还在快速迭代建议先用 7.0.x 或 7.1.x 的最新补丁版不要一上来就追最新大版本免得遇到社区还没填平的坑。下面是完整的 Maven 依赖核心只需要一个 starterdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.flowable/groupId artifactIdflowable-spring-boot-starter-process/artifactId version7.0.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency注意flowable-spring-boot-starter-process会自动把 Spring Boot 的DataSource接到 Flowable 引擎上不需要额外单独配置数据源给 Flowable 用。也不建议再引入flowable-ui那套东西那是官方的控制台工程包含了一堆前端资源和用户体系和我们自己集成后端 API 的诉求完全不在一个量级。1.3 启动前必调的三个配置项第一次启动 Flowable 7你会在控制台看到它默默建了一大批以ACT_开头的表。这个行为由database-schema-update控制默认情况下 starter 会做自动升级。虽然它很方便但我在本地调试时会主动把配置写明白避免同事 clone 代码后因为数据库账号权限不足导致启动失败。下面是application.yml里的关键配置spring: datasource: url: jdbc:mysql://localhost:3306/flowable_demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghainullCatalogMeansCurrenttrue username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver flowable: database-schema-update: true async-executor-activate: false process-definition-location-prefix: classpath*:/processes/ process-definition-location-suffixes: - **.bpmn20.xml - **.bpmn这几个配置里async-executor-activate最容易被忽略。它的作用是启动 Flowable 的异步任务执行器。如果我们只是跑流程的审批闭环不涉及定时器事件、异步消息、异步 continuation那完全可以把这一步关掉省掉一批后台线程的资源占用也让本地日志干净不少。等你后面真的用到TimerEventDefinition之类的能力再把它打开就行。2. 从零写出一条“最小闭环”BPMN请假审批流程的建模思路Flowable 7 做流程编排核心载体是 BPMN 2.0 文件。很多第一次接触的人会怕这个 XML总觉得工作流建模必须用可视化的流程设计器拖拽出来其实完全不是这样。BPMN 文件本质上就是一段描述节点和连线的 XML手写完全可行而且更能理解引擎的执行逻辑。我建议所有入门阶段的项目都先手写一个最小流程而不是直接上 Flowable Modeler 去画图。2.1 流程文件放哪、怎么命名才会被自动扫描Flowable 的 Spring Boot starter 默认会去 classpath 下的/processes/目录扫描流程定义文件。也就是说你新建src/main/resources/processes/目录把.bpmn20.xml或.bpmn文件放进去应用启动时就会自动部署。这个机制对单体应用非常友好改流程文件重启一次就能生效但也会带来一些需要注意的问题后面我会专门讲。文件命名我建议用“业务场景-版本”的格式比如leave-process.bpmn20.xml。如果你在同一个目录放多个流程文件千万别给它们起相近的名字Flowable 是按文件内容里的process id去区分流程定义的文件名只是资源标识。两个文件里如果出现了相同的process id后扫描到的会把前面的流程定义当成新版本部署轻则版本号混乱重则启动时直接报重复定义。2.2 手写 BPMN 的节点、连线和条件表达式下面是一个请假审批流程的完整 XML它包含开始事件、用户任务、排他网关、结束事件以及一条按请假天数分流的路由。这段 XML 可以直接复制到你的项目里跑?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 idleaveStart name发起请假 / userTask idapplyTask name提交请假申请 flowable:assignee${starter} / exclusiveGateway idgatewayJudge name请假天数判断 defaultflowToManager / userTask idmanagerTask name直属主管审批 flowable:assignee${manager} / userTask iddeptManagerTask name部门经理审批 flowable:assignee${deptManager} / endEvent idendNode name流程结束 / sequenceFlow idflowStart sourceRefleaveStart targetRefapplyTask / sequenceFlow idflowApply sourceRefapplyTask targetRefgatewayJudge / sequenceFlow idflowToManager sourceRefgatewayJudge targetRefmanagerTask conditionExpression xsi:typetFormalExpression ![CDATA[${days 3}]] /conditionExpression /sequenceFlow sequenceFlow idflowToDept sourceRefgatewayJudge targetRefdeptManagerTask conditionExpression xsi:typetFormalExpression ![CDATA[${days 3}]] /conditionExpression /sequenceFlow sequenceFlow idflowManagerEnd sourceRefmanagerTask targetRefendNode / sequenceFlow idflowDeptEnd sourceRefdeptManagerTask targetRefendNode / /process /definitions这里需要特别说明几个字段。flowable:assignee${starter}表示当流程走到这个用户任务时引擎会根据流程变量里的starter值动态决定这个任务分配给谁。我实际用下来强烈推荐这种动态指定的方式因为真正上线后节点的负责人基本都是根据表单提交人、组织架构查询出来的很少有人把用户名硬编码在 XML 里。exclusiveGateway是排他网关它会从上到下依次判断出口连线的conditionExpression一旦某个条件为 true 就顺着那条线走下去。我这段 XML 里还配了defaultflowToManager意思是如果所有条件都不满足就默认走主管审批这一条。这个兜底非常关键否则条件引擎找不到可走的分支时会直接抛异常。2.3 先想清楚变量作用域再建模很多初学者画完流程图就开始写启动接口结果流程启动后任务停在第一个用户节点上去查数据库的ACT_RU_VARIABLE表才发现starter、manager这些变量根本不在。原因很好理解flowable:assignee${starter}是流程引擎在创建任务时用表达式解析出来的表达式依赖流程实例级别的变量。也就是说你在启动流程时传进去的 Map会进入流程实例的变量作用域而不是任务局部作用域。后面查任务、完成任务时传入的变量也需要根据业务需求决定是放流程级还是任务级。我的建议是像“谁发起”“当前审批人”“请假天数”“审批结果”这类贯穿整个流程的数据都放到流程变量里。像“某一个节点上传的附件 ID”“某个审批节点的补充意见”这种每个节点需要独立记录的数据用任务局部变量更合适否则所有审批意见都互相覆盖后面想追溯历史就很被动了。3. 部署流程定义的三种姿势自动扫描、RepositoryService 与部署后校验流程定义文件写好了下一步就是让引擎认识它。Flowable 7 里部署流程定义的方式不少但很多人会因为“能用就行”而忽略不同方式之间的差异。我建议把这三种都掌握因为它们分别对应开发、测试和生产的不同场景。3.1 自动部署把 BPMN 丢进 processes 目录就算完事吗自动部署是最省事的方式。前面说的process-definition-location-prefix: classpath*:/processes/配置一旦生效应用启动时 Flowable 会把目录下所有合法流程定义文件部署进引擎。在本地开发阶段我用得最多。但要注意自动部署并不等于“每次启动都重新部署一份”。Flowable 的部署机制会按资源文件的字节码去比对如果同一个部署包里的流程定义没有变化重新启动时不会重复生成部署记录。只有当文件内容变了才会生成新的部署记录和新的流程版本号。这个设计很关键后面排查“为什么重启之后流程版本变多了”的时候第一时间要去检查有没有人改过 BPMN 文件而不是怀疑引擎有 bug。3.2 编程部署动态指定流程名和分类复杂场景下我们并不想让流程定义全部随应用启动自动部署尤其是上生产之后流程文件往往由运维或流程管理员在数据库里手动维护。这时就需要用RepositoryService编程部署。我有一个实际案例客户的多租户系统里每个租户要使用不同版本的审批流程同一个leaveProcess流程定义在 A 租户和 B 租户下的节点配置不同。如果依赖自动部署就只能在启动时按一套文件来。改成编程部署后管理端上传 BPMN 文件后端解析文件内容动态调用createDeployment()接口完成部署租户 ID 存到部署的分类字段里非常灵活。Resource private RepositoryService repositoryService; public String deployBpmn(MultipartFile file, String category) throws IOException { Deployment deployment repositoryService.createDeployment() .name(file.getOriginalFilename()) .category(category) .addBytes(file.getOriginalFilename(), file.getBytes()) .deploy(); return deployment.getId(); }编程部署的另一个好处是可以在一个部署包里塞多个流程定义文件然后统一命名。比如“2024年Q1所有审批流程”这样的一批文件只要调用一次部署接口ACT_RE_DEPLOYMENT表里就是一条完整记录后续回滚、排查看起来非常清晰。3.3 部署后确认流程定义与版本部署完成不等于万事大吉。我见过太多人部署结束后就直接去ACT_RE_PROCDEF表里查数据结果发现流程定义 key 对不上半天找不到问题。正确做法是部署后用ProcessDefinitionQuery按 key 查最新版本ListProcessDefinition list repositoryService.createProcessDefinitionQuery() .processDefinitionKey(leaveProcess) .orderByProcessDefinitionVersion() .desc() .list();这里有个细节.latestVersion()方法很方便但如果你在同一个流程定义 key 下部署了停用的旧版本查询结果里会把旧版本一并查出来。生产环境我通常还会加.active()条件过滤只保留状态为 active 的定义。Flowable 部署时会自动把同 key 的旧版本设置为挂起状态但这只在同一个 deployment 里才完全可靠跨部署包的情况建议自己校验一遍。4. 发起流程、查询待办、完成任务RuntimeService 与 TaskService 的实战用法流程定义部署好之后就到了整个系列里最核心的环节让流程真正跑起来。这一步涉及两个 ServiceRuntimeService负责任务流转和流程实例管理TaskService负责待办任务的查询和完成。我把它们拆开讲因为太多人在这一步把变量作用域搞混了。4.1 启动流程实例key、businessKey 与变量发起流程的标准动作是调用runtimeService.startProcessInstanceByKey()。第一个参数是流程定义 key也就是 BPMN 文件里process idleaveProcess的值。Flowable 会自动选择这个 key 下最新版本的流程定义来启动实例。第二个参数businessKey是业务主键强烈建议传入自己业务系统的单据号。比如请假单号LEAVE-20240001。这样一来流程表和业务表可以通过这个字段关联将来查“这条流程对应哪张请假单”“这张请假单走到哪一步了”都非常方便。Resource private RuntimeService runtimeService; public String startLeaveProcess(LeaveApplyDTO dto) { MapString, Object variables new HashMap(); variables.put(starter, dto.getStarter()); variables.put(manager, dto.getManager()); variables.put(deptManager, dto.getDeptManager()); variables.put(days, dto.getDays()); ProcessInstance processInstance runtimeService .startProcessInstanceByKey(leaveProcess, dto.getBizNo(), variables); return processInstance.getId(); }这里最容易踩的坑启动流程时传人的变量一定要保证 key 与 BPMN 里的表达式变量名完全一致。如果你在 XML 里写的是${manager}传的却是manger引擎解析表达式时拿不到值流程实例虽然能启动但创建任务时会因为 assignee 表达式无法解析而直接报错。这是新手报错的高发地。4.2 查询当前用户待办并按需完成任务流程启动后会按照 BPMN 定义自动执行到第一个用户任务也就是applyTask待办任务的 assignee 会被设成流程变量starter的值。这时候就要用TaskService来查待办了。Resource private TaskService taskService; public ListTaskVO queryTodoList(String userId) { ListTask tasks taskService.createTaskQuery() .taskAssignee(userId) .orderByTaskCreateTime() .desc() .list(); return tasks.stream().map(task - { TaskVO vo new TaskVO(); vo.setTaskId(task.getId()); vo.setName(task.getName()); vo.setProcessInstanceId(task.getProcessInstanceId()); vo.setCreateTime(task.getCreateTime()); return vo; }).collect(Collectors.toList()); }完成任务的 API 是taskService.complete()它接收任务 ID 和一个变量 Map。这里要说一个很容易犯的错误很多人以为完成任务就是点个“通过”不需要传变量。但在我们的请假流程里如果审批人在直属主管节点填了审批意见意见要传给后续的部门经理节点查看就必须在 complete 的时候作为变量传进去。public void completeTask(String taskId, Integer approveResult, String comment) { MapString, Object variables new HashMap(); variables.put(approveResult, approveResult); variables.put(comment, comment); taskService.complete(taskId, variables); }complete 之后引擎会自动判断当前节点是否有出口连线需要继续走。如果下一个节点是用户任务变量会影响 assignee 的解析如果是排他网关变量会参与条件表达式的计算。所以你会发现发起流程时和完成任务时两个变量 Map 的 key 集合往往不一样这是正常的。4.3 完成任务时变量回填的两种作用域完成任务时的变量可以放在流程实例作用域也可以放在任务局部作用域。complete(taskId, variables)里传的变量默认是流程实例级别的全局可见方便后续所有节点读取。如果你只想让某个变量对当前任务生效不影响其他节点可以用taskService.setVariableLocal(taskId, key, value)先设置局部变量再调用 complete。举个真实例子两个审批节点都需要记录“审批意见”如果用流程变量后一个节点的审批意见就会覆盖前一个。用任务局部变量就能各自独立历史记录里后来排查时也能分清哪个节点写了什么内容。刚开始写代码时我图省事全程用流程变量等到要追溯审批链路上的历史意见时才发现设计上的问题。工作流这种场景数据追溯的价值远大于写代码的便利。5. 跑通后的验证手段与最容易踩的坑一套“部署-发起-查询-完成”的最小闭环跑通之后接下来要做的不是急着写更多复杂流程而是把验证和排错的方法论建立起来否则后面流程一变复杂你会被各种隐藏问题淹没。5.1 验证闭环从历史活动与流程实例状态看结果流程走到结束事件后运行时的实例数据会从ACT_RU_*表里消失此时如果用runtimeService.createProcessInstanceQuery().processInstanceId(id).singleResult()去查会返回 null。这不是流程丢了而是它已经从运行表归档到了历史表。要看流程到底走了哪些节点应该用HistoryServiceResource private HistoryService historyService; public void showExecutePath(String processInstanceId) { ListHistoricActivityInstance activities historyService .createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .orderByHistoricActivityInstanceStartTime() .asc() .list(); for (HistoricActivityInstance activity : activities) { System.out.println(activity.getActivityId() - activity.getActivityName() - activity.getEndTime()); } }如果输出列表最后一个是endNode - 流程结束 - 有结束时间说明整个闭环是通的。如果中间某节点只有开始时间没有结束时间说明流程卡在了那个节点多半是该节点的用户任务没人处理或者条件网关没有可匹配的分支。5.2 常见异常的排查链路我把在实际集成过程中遇见过的高频问题整理成了一张排查表虽然不算全面但覆盖了绝大多数第一次跑 Spring Boot 3 Flowable 7 的人会遇到的状况。现象可能原因排查方法启动报错提示找不到javax.*类Spring Boot 3 配了 Flowable 6换 Flowable 7.x检查依赖版本启动后项目里没有ACT_*表database-schema-update为 false且数据库账号无建表权限手动执行官方 SQL 脚本或放开 DDL 权限部署提示流程定义重复多个 BPMN 文件用了同一个process id全局搜索idxxx确保唯一用户任务没有负责人待办查不到flowable:assignee表达式变量没传进去查ACT_RU_VARIABLE确认变量存在排他网关走了默认分支条件表达式变量名写错逐个对比变量 Map 的 key 与 XML 表达式流程结束后RuntimeService查不到实例正常归档行为改用HistoryService或查ACT_HI_*表这里面最浪费时间的是第二个。我刚开始接到一个 Spring Boot 3 项目启动时发现一堆ACT_*表全是空的当时还以为是表没有初始化后来发现是数据库账号权限不够自动建表被静默忽略了。这个坑在开发机不明显因为 root 用户权限够但一上测试环境就暴露了。5.3 一点更深入的扩展思考把这套最小闭环跑熟之后你可以开始把注意力放到更复杂的工作流能力上。比如排他网关和并行网关的组合、会签任务、驳回与撤回、子流程以及通过flowable:formKey把表单定义挂到任务上。但这些都是后话我建议你先刻意练习一件事每次写完一个流程定义先在本地起应用用 Postman 或 curl 走一遍“部署到发起、发起到完成”的完整链路再去看ACT_HI_*表确认节点足迹。这个肌肉记忆一旦建立之后排查任何问题都会快很多。最后分享一个我自己调试时的小技巧Flowable 7 的应用日志里只要把org.flowable这个包的日志级别调到 DEBUG就能看到引擎原生的 SQL 和执行逻辑输出。这个开关对第一次集成的人非常有帮助一开始觉得日志刷屏很烦但真到排查变量没传进去、条件判断不对的问题时你会感谢每一行 DEBUG 输出。