
先说结论Spring Boot 3.x 集成 Flowable 7.x这组搭配如今已经是后端项目接入工作流的主流方案之一。我刚在一套内部运营系统里把审批流从自研状态机迁到 Flowable整条链路从工程初始化、BPMN 设计、流程部署到发起流程、完成任务跑通一个最小闭环只需要半天时间。这篇是系列文章的第一篇重点讲清楚 Spring Boot 与 Flowable 的集成方式以及如何用最少的代码完成“简单流程”的部署、发起和结束。内容面向刚接触 Flowable 的 Java 开发者也适合正在做技术选型、想知道这套流程引擎到底怎么落地的同学。我尽量不写那种“官网文档复读”式的内容而是把我在实际集成中必须知道的知识点、配置参数、常见坑都摊开讲包括哪些地方会报错、报错后去哪查、查到了怎么处理。1. 版本选型与集成思路Flowable 7.x 为什么能搭 Spring Boot 3.x1.1 看版本先别急着写代码Spring Boot 3.x 和 Spring Boot 2.x 之间是跨越式的变化最直观的就是 JDK 基线提升到 17并且整个底层的 javax 命名空间迁移成了 jakarta。这意味着很多老牌中间件如果没有做适配直接扔进 Spring Boot 3.x 项目里是跑不起来的典型的报错就是 ClassNotFoundError: javax.servlet 或者启动时 Bean 创建失败。Flowable 在 6.x 后期就开始适配 Spring Boot 3但真正把“适配”作为默认姿态是从 7.x 开始的。7.x 版本直接面向 Spring Boot 3 构建所以你在 Spring Boot 3.2、3.3 这类版本上引入 flowable-spring-boot-starter依赖冲突会少很多。我自己用的组合是 Spring Boot 3.2.5 Flowable 7.0.0测试下来比较稳。选型时要注意三点不要用 Spring Boot 2.x 项目直接硬上 Flowable 7.x版本对不上。如果项目还锁在 JDK 8 或者 Spring Boot 2.x那更应该考虑 Flowable 6.7.x 系列而不是 7.x。关注 flowable-spring-boot-starter 的传递依赖它会把 mybatis、spring-boot-autoconfigure 等一整套引擎依赖带进来版本管理要统一别在 pom 里随便覆盖。1.2 引擎的三个核心 Service先建立整体认知Flowable 的 API 不算少但最核心的就三个RepositoryService、RuntimeService、TaskService。RepositoryService 负责流程定义和部署你可以把它想象成“流程文件的仓库管理员”。你把 BPMN 文件交给它它负责校验、存库、记录版本。RuntimeService 负责流程实例的运行一个流程定义可以启动多个流程实例就像一份请假单模板走出了无数张请假单。TaskService 负责任务操作用户任务创建之后通过它查询任务、认领任务、完成任务。心里有这三个 Service 的边界后面写代码就不会乱。很多新手一开始就把所有东西往上堆结果不知道某个操作该用哪个 API本质上是因为没理解这三个阶段是独立的。我觉得这个系列的思路应该这样展开第一篇文章先把“部署—发起—完成”这条主链路跑通第二篇再深入监听器、网关、会签这些进阶能力。基础链路稳了后面都是加法。2. 工程搭建依赖、配置与启动自检2.1 环境清单和 Maven 依赖开始之前先确认环境避免后面踩版本坑。JDK 17Maven 3.6Spring Boot 3.2.xFlowable 7.0.x数据库本文演示用 H2 内存库实际项目推荐 MySQL 8.0创建 Spring Boot 工程后pom.xml 里核心依赖是这样parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version /parent properties java.version17/java.version flowable.version7.0.0/flowable.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.flowable/groupId artifactIdflowable-spring-boot-starter/artifactId version${flowable.version}/version /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这里用的是flowable-spring-boot-starter它已经把引擎核心、Spring Boot 自动配置、MyBatis 映射都组合好了对入门来说最省事。如果你只想引入流程引擎部分也可以拆成flowable-spring-boot-starter-process但一般不需要大而全的 starter 不会造成额外负担只是 jar 体积大一点。数据库这块我要多说一句。新手最忌讳一上来就配 MySQL因为 Flowable 会自动创建一大堆 ACT_ 开头的表如果账号权限不够、表名大小写配置有问题、时区不对启动直接就失败。先用 H2 内存库把代码逻辑验证通再切 MySQL这是效率最高的路线。2.2 application.yml 配置与数据库策略配置文件里最关键的几个参数spring: datasource: url: jdbc:h2:mem:flowable;DB_CLOSE_DELAY-1 driver-class-name: org.h2.Driver username: sa password: h2: console: enabled: true path: /h2-console flowable: database-schema-update: true async-executor-activate: falsedatabase-schema-update: true表示引擎启动时自动检查并更新数据库结构。对于 H2、开发环境、演示环境来说非常方便你不需要手动执行建表 SQL。async-executor-activate: false很重要。Flowable 默认有异步执行器在处理定时器、异步任务时会拉起线程池。如果你只是跑简单流程开着它不会出大问题但会产生一些异步线程在单元测试时还会造成“明明流程走完了线程还没退出”的假象。初学阶段建议关掉专注流程主链路。启动应用后如果一切正常控制台会打出 Flowable 的版本日志并且数据库里会出现几十张 ACT_ 开头的表。其中 ACT_RE_PROCDEF 是流程定义表ACT_RU_TASK 是运行时任务表ACT_HI_TASKINST 是历史任务表。我会在讲部署和完成任务时反复提到这些表方便你出了问题直接查库。2.3 启动一个小测试确认引擎活过来了集成是否成功最快的方式是启动时看一眼有没有报错。更稳妥的做法是写一个ApplicationRunner启动后主动查一次引擎信息Component RequiredArgsConstructor public class FlowableStartupCheck implements ApplicationRunner { private final RepositoryService repositoryService; Override public void run(ApplicationArguments args) { long count repositoryService.createProcessDefinitionQuery().count(); System.out.println(当前流程定义数量: count); } }如果这个 bean 能正常执行说明 RepositoryService 已经被 Spring 容器正确装配引擎核心没问题。此时你还没有放入任何 BPMN 文件所以查询结果应该是 0。这是集成阶段最直接的“自检”方法比盯着启动日志猜状态踏实得多。3. 设计一个最小 BPMN 流程从申请到审批3.1 BPMN 文件其实没那么神秘Flowable 的流程模型基于 BPMN 2.0 标准文件后缀通常是.bpmn20.xml或者.bpmn。初次接触的同学会觉得里面的 XML 很复杂其实抽掉命名空间声明核心就几类元素process、startEvent、userTask、sequenceFlow、endEvent。process 是整个流程的容器id 就是流程定义的 key后面启动流程要靠这个 key。startEvent 是起点endEvent 是终点。userTask 表示需要人工处理的任务节点。sequenceFlow 连接各个节点决定流程下一步往哪走。我见过很多教程把 BPMN 文件写得特别长又是 pool 又是 lane又是边界事件对新手非常劝退。实际上做一个简单审批流最精简的模型只要五类节点就够了。3.2 一个“提交申请 - 审批 - 结束”的流程文件我这里设计一个最简单的“申请审批”流程申请人在一个任务节点提交申请然后审批人审批审批通过直接到结束节点没有驳回、网关和会签。等这个模型跑通了再加条件表达式和监听器也不迟。新建src/main/resources/processes/simple-approval.bpmn20.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 idsimpleApproval name简单审批流程 isExecutabletrue startEvent idstartEvent name开始/ sequenceFlow idflow1 sourceRefstartEvent targetRefapplyTask/ userTask idapplyTask name提交申请 flowable:assignee${applyUser}/ sequenceFlow idflow2 sourceRefapplyTask targetRefapproveTask/ userTask idapproveTask name审批 flowable:assignee${approver}/ sequenceFlow idflow3 sourceRefapproveTask targetRefendEvent/ endEvent idendEvent name结束/ /process /definitions注意这里的flowable:assignee${applyUser}它表示任务的办理人不是写死的而是从流程变量里动态取值。比如启动流程时传入applyUserzhangsan那么“提交申请”这个任务就会分配给 zhangsan。这种写法在实际项目里几乎是刚需因为你不可能把人员名单写在 BPMN 文件里。很多新手在这里会犯迷糊以为是写死字符串用户 ID其实不是。有人会问flowable:assignee能不能直接写死一个用户可以比如flowable:assigneeadmin但不推荐。因为流程一旦换个环境或者人员调整就得改 BPMN 文件重新部署这违背了流程引擎应该“按变量动态路由”的设计初衷。3.3 流程文件的自动部署约定Flowable 的 Spring Boot Starter 默认会扫描classpath:/processes/目录把目录下所有合法的 BPMN 文件自动部署到引擎里。所以我上面把文件直接放到src/main/resources/processes/是有原因的只要应用启动流程定义就会进入 ACT_RE_PROCDEF 表。这个机制对开发期很友好不用手动调部署接口。但也有个小心思自动部署是增量叠加的如果同一个流程 key 的文件内容有变化再次启动时引擎会创建新版本而不是覆盖旧版本。也就是说流程定义版本会递增旧版本仍然可以被已发起的流程实例继续使用。这其实是符合业务预期的比如老单据还没审完流程定义不能直接变只能让新发起的流程走新版本。如果你想完全掌控部署时机那就别把文件放在 processes 目录而是放到src/main/resources/bpmn/这种自定义目录然后用 RepositoryService 手动部署。我建议刚开始学习时就用自动部署少一个环节排查问题也更容易。4. 部署流程自动部署与编程式部署的区别4.1 自动部署看起来什么都没做其实它干了三件事把 BPMN 文件放进 processes 目录后启动应用引擎会做三件事解析 XML 文件、校验流程模型是否合法、把流程定义写入 ACT_RE_PROCDEF 表。每张表存什么我习惯用一个简单记忆法ACT_RE_* 是流程定义、部署包等“静态”资源。ACT_RU_* 是流程实例、任务、变量等“运行中”的数据。ACT_HI_* 是历史数据流程结束后记录仍然保留。自动部署适合开发阶段因为 BPMN 文件一旦变动重启即生效。但它也容易让新手产生“不知道流程什么时候部署的”的错觉。建议你启动后去查一下 ACT_RE_PROCDEF 表亲眼看到那条记录印象会深很多。4.2 编程式部署什么时候需要手动做自动部署很省事但如果流程文件存放在数据库、远程配置中心、或者由用户在页面上传自动部署就满足不了了。这个时候需要自己调 RepositoryService。我这里提供一段手动部署的逻辑放在一个 Service 里Service RequiredArgsConstructor public class FlowableDeployService { private final RepositoryService repositoryService; public String deploySimpleApproval() { Deployment deployment repositoryService.createDeployment() .addClasspathResource(processes/simple-approval.bpmn20.xml) .name(simple-approval-deployment) .enableDuplicateFiltering() .deploy(); return deployment null ? 重复部署未创建新部署 : deployment.getId(); } }addClasspathResource表示从 classpath 加载资源文件。如果文件放在src/main/resources/processes/下同时又放进去了自动部署目录而且你还手动调了一次部署那就会出现两个版本的定义。我在实际项目里碰到过这种“定义版本怎么莫名其妙变成 2”的问题原因就是自动部署和手动部署重叠了。enableDuplicateFiltering()这个方法比较实用。它表示如果部署包名称相同且资源文件内容没有变化就跳过重复部署。注意它的语义是“不让完全一样的东西重复部署”不是“版本相同就不部署”理解这一点就能避开判断逻辑上的坑。4.3 部署后如何确认流程已生效部署是否成功不能光靠代码不报错来判断。最稳妥的方式是查询流程定义ListProcessDefinition list repositoryService.createProcessDefinitionQuery() .processDefinitionKey(simpleApproval) .orderByProcessDefinitionVersion() .desc() .list(); for (ProcessDefinition pd : list) { System.out.println(流程定义ID: pd.getId()); System.out.println(版本: pd.getVersion()); System.out.println(名称: pd.getName()); System.out.println(是否挂起: pd.isSuspended()); }这里的processDefinitionKey对应 BPMN 里process idsimpleApproval的 id不是文件里的显示名称。很多初学者把 key 写成文件的 display name结果启动流程时一直报“未找到流程定义”十有八九是 key 填错了。我归纳了一个部署后的核对清单ACT_RE_PROCDEF 里能看到流程定义并且 KEY_ 是 simpleApproval。如果没有记录先看 resources/processes 目录是否存在文件后缀是否是 .bpmn20.xml。如果看到多条同 key 记录检查版本号确认是否因为自动部署和手动部署重叠。5. 发起流程与完成任务从零跑通闭环5.1 启动流程实例RuntimeService流程定义是模板流程实例才是真正跑起来的“那一次业务过程”。启动流程用 RuntimeServiceService RequiredArgsConstructor public class FlowableProcessService { private final RuntimeService runtimeService; public ProcessInstance startSimpleApproval(String applyUser, String approver) { MapString, Object variables new HashMap(); variables.put(applyUser, applyUser); variables.put(approver, approver); return runtimeService.startProcessInstanceByKey(simpleApproval, variables); } }startProcessInstanceByKey的第一个参数就是 BPMN 里 process 的 id。第二个参数是流程变量引擎在创建第一个 userTask 时会用${applyUser}和${approver}这两个变量去解析任务办理人。如果启动时没有传入 approver审批任务创建时会直接抛表达式解析异常因为变量不存在。从启动那一刻开始流程实例就产生了一个唯一的 ID流程定义 ID 和流程实例 ID 是完全不同的概念。定义 ID 类似一个类的全限定名实例 ID 是这个类 new 出来的对象地址。查任务时一定要用流程实例 ID而不是流程定义 ID这是初学阶段最容易混的地方。5.2 查询任务和完成任务TaskService流程一旦启动引擎会自动走到“提交申请”这个 userTask并在 ACT_RU_TASK 表里生成待办任务。接下来用 TaskService 查任务、完成任务。Service RequiredArgsConstructor public class FlowableTaskService { private final TaskService taskService; public ListTask findTodoTasks(String processInstanceId, String assignee) { return taskService.createTaskQuery() .processInstanceId(processInstanceId) .taskAssignee(assignee) .list(); } public void completeTask(String taskId) { taskService.complete(taskId); } }很多刚入门的人会问任务的 assignee 到底是什么它就是一个字符串用户标识。因为我们在 BPMN 里写的是${applyUser}流程变量 applyUser 的值是 zhangsan所以引擎创建任务时会把 assignee 解析成 zhangsan。查询待办任务时taskAssignee(zhangsan)就能查到这个人名下的任务。taskService.complete(taskId)表示完成任务引擎会沿着 sequenceFlow 走到下一个节点。如果用单元测试驱动会发现调用 complete 之后ACT_RU_TASK 表里原来的任务消失了新的审批任务出现了。这个“任务记录消失/新增”的过程就是流程推进最直观的体现。5.3 完整走一遍发起、提交、审批、结束把前面几个 Service 串起来跑一次完整流程Test void fullFlow() { MapString, Object variables Map.of( applyUser, zhangsan, approver, lisi ); ProcessInstance pi runtimeService.startProcessInstanceByKey(simpleApproval, variables); String processInstanceId pi.getId(); // 申请人 zhangsan 查询并完成任务 Task applyTask taskService.createTaskQuery() .processInstanceId(processInstanceId) .taskAssignee(zhangsan) .singleResult(); taskService.complete(applyTask.getId()); // 审批人 lisi 查询并完成任务 Task approveTask taskService.createTaskQuery() .processInstanceId(processInstanceId) .taskAssignee(lisi) .singleResult(); taskService.complete(approveTask.getId()); // 确认流程已结束 ProcessInstance query runtimeService.createProcessInstanceQuery() .processInstanceId(processInstanceId) .singleResult(); System.out.println(流程实例是否还在: (query ! null)); }这段代码跑完流程实例应该不存在了说明整个流程走到了结束节点。如果query ! null说明流程还在运行中那就需要检查是不是中间某个 userTask 没有从启动变量里正确解析办理人或者 sequenceFlow 没有按预想路径走到 endEvent。这里还有一个值得养成的好习惯查询任务时优先用processInstanceId加上taskAssignee两个过滤条件避免不同流程实例的任务互相干扰。如果只按 assignee 查线上数据多的时候会查出大量无关任务这对排错没有帮助。5.4 关于任务变量和流程变量的边界任务完成时可以通过taskService.complete(taskId, variables)传入变量变量会被保存到流程实例级别后续节点和表达式都能读取。比如审批人可以 set 一个approvedtrue然后下一个网关根据这个变量判断走哪个分支这是后续文章会展开的内容。我在这里想强调的是流程变量存储在 ACT_RU_VARIABLE 表中任务本地变量存在 ACT_RU_TASK 的局部信息里两者作用域完全不同。如果你在 complete 时传了一个变量下一步节点读不到先检查是不是把变量放错作用域了。这个坑我在项目里见过好多次虽然是细节但排错成本很高。6. 实战中容易踩的坑6.1 版本与依赖冲突启动时直接“暴雷”最常见的启动异常之一是 NoClassDefFoundError 或者 BeanDefinitionStoreException 里藏着一堆 mybatis 相关类错误。出现这种情况先检查 Spring Boot 和 Flowable 版本是不是匹配的。比如 Spring Boot 3.2 要对上 Flowable 7.0.x如果误把 Flowable 6.6 装进来几乎必然报错。排查思路也很简单IDEA 里打开 Maven 依赖树过滤 org.flowable 和 org.springframework看看有没有多个版本同时存在。如果项目里其他组件覆盖了 Spring Boot 版本优先保留 spring-boot-starter-parent 的版本管理再在 properties 里显式固定 flowable.version。我给一个参考表场景Spring Boot 版本Flowable 版本建议全新项目3.2.x7.0.x推荐老项目升级3.x7.0.x需要全面回归测试强制 JDK82.7.x6.7.x不建议用 7.x快速 demo3.2.x H27.0.x最省心6.2 数据库自动建表失败用 MySQL 时会遇到两种典型问题一种是连接串没有加时区参数导致引擎初始化时报错另一种是数据库账号没有建表权限。MySQL 的 JDBC URL 建议这样spring: datasource: url: jdbc:mysql://localhost:3306/flowable?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghainullCatalogMeansCurrenttrue driver-class-name: com.mysql.cj.jdbc.Driver username: root password: your_password flowable: database-schema-update: truenullCatalogMeansCurrenttrue这个参数在 Flowable 与 MySQL 配合时比较关键它避免引擎拿到 MySQL 实例级的 catalog 列表导致建表判断异常。这个参数不是所有人都知道网上很多报错案例最后都是靠它解决的。如果你用的是 MySQL 8.0驱动必须是 com.mysql.cj.jdbc.Driver旧版 com.mysql.jdbc.Driver 已经不支持。6.3 启动流程时报错“no process definition found”这个报错 90% 是 key 写错了。BPMN 里 process 的 id 是 simpleApproval启动代码里却可能写成了 simple-approval或者写成了文件名。流程 key 和文件名没有任何必然关系只和process id一致。不要急着改代码先去查表SELECT ID_, KEY_, NAME_, VERSION_ FROM ACT_RE_PROCDEF;KEY_ 列写的什么启动时就用什么。这个习惯一旦建立你会少走很多弯路。6.4 任务查不到或者完成任务后流程不动任务查不到先看两个字段流程实例 ID 和 assignee。流程实例 ID 可以从 ACT_RU_EXECUTION 表确认assignee 需要和 ACT_RU_TASK 表里的值比对。很多时候你以为启动时传了 applyUser实际传的是 applyUser 的变量名和值搞混了比如变量名成了 applUser表达式解析出来的就是 null。完成任务后流程不继续走优先检查 BPMN 里的 sequenceFlow 是否都连接正确。如果 userTask 后面没有连向下一节点的线流程就会卡在当前节点。Flowable 对这类模型错误在部署时不一定严格拦截要运行到那一瞬间才会表现为“任务完成后流程不动”。我总结了一个快速排查表现象排查方向启动报 key not foundACT_RE_PROCDEF 是否存在该 KEY_任务查不到ACT_RU_TASK 里 ASSIGNEE_ 是否为预想值complete 后流程卡住检查 sequenceFlow 目标节点是否存在表达式解析报错检查流程变量是否传入、变量名是否拼错重复部署导致版本混乱查看 DEPLOYMENT_ID_确认部署来源6.5 关于监听器、条件表达式先留个口子顺着热搜词里的“flowable 监听器”“flowable 条件”我多说一句第一篇文章不展开这些但你要知道它们将来的位置。监听器可以在任务创建、任务完成、流程启动等节点挂载用于在流程事件发生时回调业务代码。条件表达式则是让 sequenceFlow 上带着 conditionExpression例如${approved true}引擎会根据条件选择分支走向。也就是说等到你需要驳回、条件分流、会签的时候现在这个简单模型里引入的流程变量机制会继续复用。建议你先把变量的传递路线走顺再碰监听器不要倒过来一口吃成胖子。最后再分享一个小技巧跑通这段最小闭环之后我最大的感受是Flowable 的 API 本身不难难的是流程模型思维。你写普通业务代码时逻辑是显式的if-else 一条路走到底上了流程引擎逻辑变成了 BPMN 里节点之间的连线变量在节点间传递状态不再藏在你自己的表里而是散落在 ACT_RU_* 和 ACT_HI_* 这些引擎表中。刚开始会觉得不习惯但一旦接受这个模型后面做会签、驳回、加签都会顺很多。我个人建议后面做这个系列实验时每次写完一段流程逻辑就用单元测试把“发起—完成任务—查询结果”的全过程跑一遍并且盯一下 ACT_RU_TASK 表的变化。不要只依赖接口调试因为接口层会引入很多无关噪声。把引擎这条链路的输入输出摸透比记住某个 API 更值钱。下一篇文章可以深入监听器和条件表达式到时候我们再继续折腾。