SpringBoot集成Flowable快速落地工作流审批实战指南

发布时间:2026/10/10 13:53:52
SpringBoot集成Flowable快速落地工作流审批实战指南 先说个背景。我之前在一家公司接手过一个“自制审批状态机”节点一多代码里全是 if-else 判断当前状态该往哪走每加一个审批环节就要动业务代码光梳理状态流转就花了两天。后来痛定思痛把审批流整体换成了 flowable前后大概用了一周把核心功能全部接完。这篇文章就是把这个过程里最关键的部分拆出来给你一条基于 SpringBoot flowable 快速落地工作流的路径适合那些被审批逻辑折磨过、想正经引入流程引擎的后端开发同学。1. 为什么我最终选了 flowable 而不是自研或换壳1.1 自研审批流的问题在哪里很多人一开始都会想审批不就是“状态 角色 条件分支”吗自己用状态机写就行了为什么非要引入一个流程引擎我自己的真实感受是自研状态机能撑住前两三个节点但一旦出现会签、或签、驳回、撤回、动态指定审批人这些真实业务需求状态机的复杂度就会爆炸。最典型的问题是状态流转逻辑散落在 service 层每次加节点都要改代码重新发版流程的“下一步去哪”和业务规则混在一起业务方想要调整审批层级开发就得跟着加班没有流程历史记录出了纠纷想追溯“谁在什么时间审批过”非常费劲没有流程图可视化审批到哪一步全靠猜。而且这还只是功能层面的问题。自研状态机最大的隐性成本是“规则变更”。业务方的审批流程几乎是季度一变今天多一个分管领导审批明天少于三天的假不用走人事。你要是每次都硬编码那这个项目就永远处于“改不完的需求”状态。1.2 Activiti、Camunda、Flowable 到底怎么选市面上的主流开源工作流引擎就那么几个我简单列个对比都是我在选型时实际考量过的点对比项ActivitiFlowableCamunda血缘关系Activiti 5/6 的老牌血统Activiti 5 分支出来的Activiti 5 分支出来的社区活跃度一般活跃迭代较快活跃商业化较重BPMN 2.0 支持完整完整完整SpringBoot 集成尚可官方有 starter很顺滑官方有 starter开源协议Apache 2.0Apache 2.0部分组件商业授权Apache 2.0核心上手难度中等中等偏低中等偏高文档体验更新慢文档较清晰文档多但偏商业化个人建议很直接如果你是在国内中小团队、以 SpringBoot 为主技术栈、需要在国内技术社区里找到大量中文踩坑资料flowable 是当前最舒服的选择。原因很简单它的 API 设计和 SpringBoot starter 封装得最贴合 Spring 生态文档里的例子基本可以直接跑起来。2. 快速接入依赖、配置、表结构一次到位2.1 版本搭配是第一个坑接入 flowable 第一步不是写业务代码而是先把版本对齐。这里我必须强调一下版本选错后面全是坑。我测试过两个主流组合SpringBoot 2.x JDK 8/11 flowable-spring-boot-starter 6.7.2稳定国内用的最多资料最好查SpringBoot 3.x JDK 17 flowable-spring-boot-starter 6.8.0兼容但部分老教程接口有变化踩坑时要注意区分。如果你跟着网上老教程做用 SpringBoot 3 去跑 6.7.2 的 starter大概率会直接启动失败报的错还特别隐蔽往往是 Jackson 或 MyBatis 相关。我当时就被这个折腾了好一阵最后老老实实把项目降回 SpringBoot 2.7一把跑通。pom.xml 里引入依赖很简单dependency groupIdorg.flowable/groupId artifactIdflowable-spring-boot-starter/artifactId version6.7.2/version /dependency一个 starter 就带全了引擎核心、流程定义管理、任务管理、历史管理等模块不需要再加其他 flowable 包。2.2 核心配置项逐条解读依赖加完接下来是 application.yml 里的配置。我每次新建项目都会先配这一份按我的使用习惯这几项是从生产环境里沉淀下来的spring: datasource: url: jdbc:mysql://localhost:3306/flowable_demo?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/ShanghainullCatalogMeansCurrenttrue username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver flowable: # 启动时如何对待数据库表结构 database-schema-update: true # 关闭异步执行器避免启动时多出一堆后台线程 async-executor-activate: false # 历史级别full 记录最完整可查流程图表 history: full # 自动部署 resources 目录下的 BPMN 文件 process-definition-location-prefix: classpath*:/processes/ # 校验流程文件格式有误时启动直接报错 check-process-definitions: true # 流程定义缓存失效时间生产环境建议调大 process-definition-cache-limit: 128单独说一下async-executor-activate: false。默认情况下 flowable 会启动一个异步执行器线程池用于处理定时任务、异步消息等。如果业务里暂时用不到我建议先关掉否则你会在启动日志里看到一堆AsyncExecutor的活动记录排查问题的时候非常干扰。真要用到异步任务或定时器时再打开不迟。history: full也必须开。flowable 的历史级别有 none、activity、audit、full 四档。默认 audit 其实也能满足大多数场景但要做流程图高亮追踪把当前节点在流程图上标出来就必须 full。我建议直接从 full 起步后面想查什么都有不会抓瞎。2.3 启动时自动建的表长什么样配置完成启动 SpringBootflowable 会自动检测并创建一套以ACT_开头的表。第一次启动后数据库里大概会出现 40 张左右的表按前缀可以快速判断用途表前缀用途代表表ACT_RE_流程定义、流程模型等静态资源ACT_RE_PROCDEF流程定义表、ACT_RE_DEPLOYMENT部署表ACT_RU_运行时数据流程实例、任务、变量ACT_RU_EXECUTION执行实例、ACT_RU_TASK待办任务、ACT_RU_VARIABLE流程变量ACT_HI_历史数据历史实例、历史任务、历史活动ACT_HI_PROCINST历史流程实例、ACT_HI_TASKINST历史任务、ACT_HI_ACTINST历史活动ACT_ID_身份管理用户、用户组ACT_ID_USER、ACT_ID_GROUPACT_GE_通用数据属性配置、二进制数据ACT_GE_PROPERTY、ACT_GE_BYTEARRAY注意flowable 对表管理有一套自己的版本机制存放在ACT_GE_PROPERTY表里。你在任何情况下都不要手动删这张表的数据否则引擎会认为库结构是旧版本启动时可能因为 schema 版本冲突报错。3. BPMN 流程定义从画图到部署3.1 手写 BPMN XML 还是用在线设计器网上很多人一上来就推荐用 flowable 自带的在线流程设计器Flowable Modeler画图然后导出 BPMN。我的建议是本地开发阶段先手写 XML。理由有两点。第一手写 XML 能逼着你理解 BPMN 模型的本质节点是userTask、exclusiveGateway连线是sequenceFlow条件判断是conditionExpression。这些概念一旦搞清楚后续用任何可视化工具都很轻松遇到问题也能直接看 XML 排查。第二在线设计器生成的 XML 里会有很多冗余的flowable:命名空间属性不熟悉的人反而容易看晕。当然实际画流程图还是建议用 IDEA 插件Flowable BPMN visualizer或在线工具。做法是先用插件可视化调整布局再打开 XML 视图微调关键参数。这个流程我一直在用。3.2 一个可跑的请假审批流程 XML我拿最常用的请假审批流程举例员工提交申请、部门经理审批、超过 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 typeLanguagehttp://www.w3.org/2001/XMLSchema expressionLanguagehttp://www.w3.org/1999/XPath targetNamespacehttp://www.flowable.org/processdef process idleaveProcess name请假审批流程 isExecutabletrue startEvent idstartEvent name开始 flowable:initiatorinitiator/ userTask idapplyTask name提交请假申请 flowable:assignee${initiator} flowable:formKeyleaveApplyForm/ userTask idmanagerTask name部门经理审批 flowable:assignee${managerAssignee} flowable:formKeymanagerApproveForm/ userTask idhrTask name人事确认 flowable:assigneehrUser flowable:formKeyhrConfirmForm/ exclusiveGateway idgateway1 name是否超过3天/ sequenceFlow idflow1 sourceRefstartEvent targetRefapplyTask/ sequenceFlow idflow2 sourceRefapplyTask targetRefmanagerTask/ sequenceFlow idflow3 sourceRefmanagerTask targetRefgateway1/ sequenceFlow idflow4 sourceRefgateway1 targetRefhrTask conditionExpression xsi:typetFormalExpression ![CDATA[${approved true days 3}]] /conditionExpression /sequenceFlow sequenceFlow idflow5 sourceRefgateway1 targetRefendEvent conditionExpression xsi:typetFormalExpression ![CDATA[${approved false || days 3}]] /conditionExpression /sequenceFlow sequenceFlow idflow6 sourceRefhrTask targetRefendEvent/ endEvent idendEvent name结束/ /process /definitions这个文件放在src/main/resources/processes/leave-process.bpmn20.xml下。因为前面配置了process-definition-location-prefix项目启动时 flowable 会自动扫描并部署不需要写一行部署代码。3.3 部署后的流程定义版本机制每次改动 XML 并重启flowable 会生成一个新的流程定义版本同一个key这里是leaveProcess会累积出多个版本。这里有个关键的细节用processDefinitionKey启动流程实例时默认启动的是最新版本如果你用processDefinitionId启动就锁定了某个具体版本。实际生产里我强烈建议业务上明确规则旧流程实例走完旧版本新流程实例走新版本。flowable 天然支持这个特性前提是你的代码不要写死processDefinitionId。3.4 手动部署流程文件的三种方式虽然我推荐自动部署但有时候你需要动态部署比如管理后台支持上传 BPMN 文件这时要用RepositoryServiceService public class ProcessDeployService { private final RepositoryService repositoryService; public ProcessDeployService(RepositoryService repositoryService) { this.repositoryService repositoryService; } /** * 通过文件流部署流程 */ public Deployment deployProcess(InputStream bpmnStream, String name) { return repositoryService.createDeployment() .addInputStream(name .bpmn20.xml, bpmnStream) .name(name) .deploy(); } }createDeployment()这套链式 API 就是流式构建器的经典风格一气呵成。部署成功后可以去ACT_RE_DEPLOYMENT和ACT_RE_PROCDEF两张表里确认部署记录和流程定义。4. 启动流程实例与任务流转实战4.1 启动一个流程实例部署完成后核心操作就是启动流程实例了。以请假为例启动时要把审批人、请假天数这些业务参数作为流程变量传进去Service public class LeaveService { private final RuntimeService runtimeService; public LeaveService(RuntimeService runtimeService) { this.runtimeService runtimeService; } /** * 员工提交请假申请即启动流程 */ public ProcessInstance startLeaveProcess(LeaveRequest request) { return runtimeService.startProcessInstanceByKey( leaveProcess, request.getBizKey(), Variables.builder() .value(initiator, request.getApplicant()) .value(days, request.getDays()) .value(reason, request.getReason()) .value(approved, null) .value(managerAssignee, request.getManagerId()) .build() ); } }代码里有几个细节值得说。第一个参数是流程定义 key第二个是业务 key。很多初学者不知道业务 key 是干嘛的。它本质上就是你的业务主键比如请假单号。通过runtimeService.createProcessInstanceQuery().processInstanceBusinessKey(bizKey).singleResult()能反查流程实例这是把流程和业务关联起来的最常用手段。Variables.builder()是 flowable 提供的变量构建器比直接塞 Map 更安全能正确处理类型。流程变量会以序列化形式存到ACT_RU_VARIABLE表这就是审批条件表达式中${days 3}能取到值的原因。4.2 查询待办任务流程启动后第一个任务就到了员工名下。查询“我待办的请假审批”用TaskServiceListTask todoTasks taskService.createTaskQuery() .processDefinitionKey(leaveProcess) .taskAssignee(user123) .active() .orderByTaskCreateTime() .desc() .list();其中.active()很关键。flowable 里任务有两种状态active正常运行中和 suspended被挂起。如果流程实例被挂起任务查询不出来是正常的别以为是 BUG。4.3 完成审批任务员工提交后部门经理会看到待办任务。完成审批时把审批结果作为流程变量传进去驱动排他网关判断走向public void completeApprove(String taskId, boolean approved, String comment) { taskService.complete(taskId, Variables.builder() .value(approved, approved) .value(managerComment, comment) .build()); }这个过程中最容易踩的坑是complete 之后网关的条件表达式approved true拿不到值。原因往往是流程变量没传进来或者条件表达式写的是approvedtrue把布尔和字符串比较了。我见过太多人把精力花在排查引擎上最后发现是自己表达式写得不对。统一用${approved true}这种写法不要加引号。4.4 审批人设置三种方式按场景选实际项目中审批人很少是写死的。我常用三种方式按复杂程度递增方式一XML 里写死 assignee。适合角色固定、人员不变的场景比如人事确认节点 assigneehrUser。优点是简单缺点是换人要改 XML。方式二启动流程时通过流程变量指定。就是上面请假例子里的${managerAssignee}启动流程时动态传入当前申请人的部门经理。这个方式使用频率最高。缺点是审批人如果中途换了或需要多个候选人抢办就不够用。方式三用监听器动态设置。在任务创建时通过监听器查库并设置审批人适合审批人需要根据业务数据实时计算的场景。比如根据费用类型找对应的分管领导写 XML 时根本不知道是谁只能在运行时决定。4.5 监听器TaskListener 的实用场景flowable 的监听器分为 ExecutionListener执行监听器监听流程实例级事件和 TaskListener任务监听器监听用户任务事件。日常开发中 TaskListener 接触最多事件类型主要有create任务创建时触发最常用动态指定审批人就是在这个时机assignment任务被分配时触发complete任务完成时触发delete任务被删除时触发。一个简单的任务创建监听器实现public class ManagerTaskListener implements TaskListener { Override public void notify(DelegateTask delegateTask) { String eventName delegateTask.getEventName(); if (TaskListener.EVENTNAME_CREATE.equals(eventName)) { // 根据发起人所在部门动态查审批人 String initiator (String) delegateTask.getVariable(initiator); String managerId queryDepartmentManager(initiator); delegateTask.setAssignee(managerId); delegateTask.setVariable(managerAssignee, managerId); } } }注意delegateTask.setVariable()设置的变量是流程变量整个流程实例全局可见而setAssignee()只修改当前任务的处理人。这两个别搞混。XML 中挂监听器userTask idmanagerTask name部门经理审批 flowable:formKeymanagerApproveForm extensionElements flowable:taskListener eventcreate classcom.example.listener.ManagerTaskListener/ /extensionElements /userTask有些场景不想写 Java 类也可以用表达式直接设置审批人flowable:taskListener eventcreate expression${task.setAssignee(task.getVariable(\initiator\))}/但这种写法和 Java 类方式相比可调试性差很多我一般只在临时改数据时用。4.6 会签与或签多实例节点核心参数审批流里最常让人头疼的是会签所有人必须同意和或签一人同意即可。flowable 里多实例节点主要看三个参数参数含义典型值flowable:collection循环的审批人集合流程变量approverListflowable:elementVariable集合中取出的单个元素变量名approvercompletionCondition多实例完成条件${nrOfCompletedInstances nrOfInstances}一个会签节点的 XML 片段userTask idsignTask name部门会签 flowable:formKeysignForm multiInstanceLoopCharacteristics isSequentialfalse flowable:collectionapproverList flowable:elementVariableapprover completionCondition ${nrOfCompletedInstances nrOfInstances} /completionCondition /multiInstanceLoopCharacteristics /userTask启动时传入approverList变量注意必须是ListString类型ListString approvers List.of(u001, u002, u003); runtimeService.startProcessInstanceByKey( leaveProcess, Variables.builder() .value(approverList, approvers) .build());这里有个特别常见的坑如果传的是逗号分隔的字符串 u001,u002,u003flowable 的 collection 解析会失败。网上很多老教程写flowable:collectionapproverList配合flowable:collectionString属性但 6.x 版本推荐直接用 List 类型变量。我建议统一走 List 方式。或签只需把完成条件改成${nrOfCompletedInstances 1}或者用比例比如 50% 同意就通过${nrOfCompletedInstances / nrOfInstances 0.5}。注意多实例节点默认会为每个审批人生成一个独立任务且编号相同。查询“待办”时一个人会同时查到多个相同名称的任务这属于正常现象。千万不要在代码里用任务名去做唯一判断。5. 驳回、撤回与流程图追踪5.1 驳回的两种设计方案驳回是所有审批系统里的高频需求也是自研状态机最容易写崩的地方。flowable 里实现驳回有两条路线我按踩坑程度从高到低说。方案一真正的流程回退ChangeState 跳转runtimeService.createChangeActivityStateBuilder() .processInstanceId(processInstanceId) .moveExecutionToActivityId(applyTask) .changeState();这行代码能把当前执行实例直接跳回“提交请假申请”节点生成新的待办任务。优点是节点关系和流程图上完全一致历史轨迹干净。缺点是回退之后之前的节点会重复执行一遍如果节点上有任务监听器比如发送通知你需要自行控制避免重复通知。方案二业务层兜底推荐给新手在排他网关前加一条分支走“驳回”路径。审批人传一个rejected变量条件表达式rejected true就回到提交节点。这种方式把驳回当成“正常分支”而不是“异常跳转”代码逻辑反而更纯粹适合流程相对固定的业务。两种方案不能说谁绝对好。如果你的流程需要自由跳转任意节点驳回到任意节点方案一是唯一选择如果只是常见的“驳回重填”方案二更稳。我的经验是先把方案二跑通等业务明确要任意跳转了再上方案一不要在第一天过度设计。5.2 撤回员工提交后还能改吗员工提交后在经理还没审批之前应该允许撤回。实现的本质就是查询当前流程实例是否还在第一个审批节点如果还没完成就中止或删除流程实例。public void withdraw(String processInstanceId, String reason) { long taskCount taskService.createTaskQuery() .processInstanceId(processInstanceId) .taskDefinitionKey(managerTask) .count(); if (taskCount 0) { runtimeService.deleteProcessInstance(processInstanceId, reason); } else { throw new IllegalStateException(当前节点不允许撤回); } }撤回后如果有业务单据需要回到“编辑中”状态记得在删除流程实例后同步更新业务表这部分 flowable 不管得你自己动手。5.3 流程图追踪与历史记录history: full配置在这里就发挥作用了。通过历史服务可以查到流程实例的完整节点轨迹ListHistoricActivityInstance activities historyService .createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .finished() .orderByHistoricActivityInstanceStartTime() .asc() .list();把每个HistoricActivityInstance的activityId和当前正在执行的节点 ID 匹配就可以在流程图上高亮显示“走到哪一步了”。这是企业里很看重的一个功能领导就爱看这种图。6. 常见问题与排查技巧实录6.1 启动时卡在表结构版本校验现象日志报错Could not find a valid property with name schema.version或类似内容。原因ACT_GE_PROPERTY表损坏或缺失。排查先看这张表在不在有没有schema.version行数据。如果是全新库把database-schema-update临时改成drop-create重建只限开发环境如果已有数据千万不要 drop想办法从备份恢复。我的独门建议每次升级 flowable 版本前先备份数据库再用官方升级脚本处理。flowable 的升级不完全兼容旧表结构直接替换 jar 包启动是会炸的。6.2 流程图中文乱码现象生成的流程图片中文显示为方块。原因JVM 没找到支持中文的字体或者 flowable 默认字体不是中文字体。解决在配置里指定中文字体flowable: activity-font-name: 宋体 label-font-name: 宋体 annotation-font-name: 宋体Linux 服务器上尤其要注意很多精简版系统确实没装中文字体。装一个fontconfig和中文字体包再重启服务就行。6.3 异步执行器引发的事务问题现象任务完成时报No process definition found for id或事务提交异常。原因如果async-executor-activate: true流程节点上有一个asynctrue属性事件的执行就会切换到异步线程和你的业务事务不在一个上下文中容易出诡异问题。排查看 BPMN XML 里有没有flowable:asynctrue。如果没有检查代码里是否有startProcessInstance和complete之间跨了多个事务。我的建议很直接异步只在特定场景开比如发送消息不阻塞主流程不要全局默认开更不要在入门阶段开。关闭着异步执行器绝大多数问题都能用同步思路排查。6.4 多实例 collection 传值失败这个我在前文提过但值得再列一次。如果你发现会签节点任务没生成十有八九是approverList变量类型不对。flowable 期望的是一个List不能是逗号分隔字符串更不能是数组。我在代码里加的防御if (approvers instanceof List) { // 正常放行 } else { log.error(approverList 必须是 List 类型当前类型: {}, approvers.getClass()); }6.5 SpringBoot 3 兼容性踩坑速查如果你非要上 SpringBoot 3 flowable 6.8.0有几个典型问题提前有个心理预期配置文件里的flowable.process-definition-location-prefix依然有效Variables.builder()依然有效但部分老教程里的new HashMap()传参方式也能用flowable-spring-boot-starter6.8.0 对 Jakarta EE 的适配已经完成javax包名要替换成jakarta如果遇到 MyBatis 版本冲突大概率是项目里手动引入了旧版 mybatis把 dependency 调整一致就好。我把 SpringBoot 2.7 flowable 6.7.2 作为默认组合原因就是社区资料更多、踩坑的人更多、能预见的坑我都提前知道了。6.6 流程表单数据存哪最后说一个经常被忽略的设计问题流程变量到底该存什么我见过有人把整个请假单对象 JSON 序列化后塞进流程变量结果流程变量表越来越大查询越来越慢。正确做法是流程变量只存流程流转需要的数据审批人、天数、金额、审批结果业务详细信息存自己的业务表通过业务 key 关联。流程引擎只负责“流”不负责“存业务数据”。这个边界划清楚后续维护会轻松很多。写在最后的一些个人体会踩过这么多坑之后我想说的是流程引擎不是银弹但它确实解决了审批流这个特定领域里最难的部分。我现在的选型标准很明确流程节点超过 3 个、流转规则会变、需要会签或签、需要流程图追踪这四个条件满足任意两个就值得上 flowable如果只是两个节点一个 if那完全没必要引入引擎。真要在项目里落地 flowable我最想劝你的就一条开工前先跟业务方把流程图一笔一笔画清楚。节点、分支条件、审批人规则、驳回路径全部确认完再编码。flowable 的场景是“流程驱动代码”不是“代码驱动流程”。你先在纸上把流程理清了后面敲代码就是流水线作业要是上来就写代码后面大概率会反复推翻重来。我最后一次做审批流改造就是先拉着业务方在白板上把请假、报销、用款三个流程画了一遍确认所有分支和角色然后才动手写第一个 BPMN XML。结果后续几乎没有返工。希望这篇分享也能帮你在 SpringBoot flowable 的路上少走几趟弯路。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询