Flowable工作流引擎实战:从BPMN流程定义到性能优化
“工作流”这个词这些年被用得越来越泛企业里几乎每个系统都要跑几个审批流程。Flowable 作为 Java 生态里比较主流的开源工作流引擎我从最初只是调用几个 API到后来完整落地一套含条件分支、会签、驳回、历史追踪的审批项目中间踩了不少坑也把很多“文档里没写明白”的机制彻底搞清楚了。这篇就不整虚的直接从工作流引擎要解决什么问题讲起再到工程搭建、流程定义、部署启动、任务审批、进阶玩法、性能优化把 Flowable 全流程的关键环节逐个拆开揉碎。如果你正准备在 Java 项目里引入工作流引擎或者已经被 Flowable 的 API 和各种表搞得一头雾水这篇文章应该能帮你少走不少弯路。下面进入正文。1. 先想清楚工作流引擎到底帮你解决了什么问题1.1 “工作流”不是流程状态机做企业应用开发最常听到的需求就是“加一个审批流程”。很多人第一反应是给业务表加个 state 字段写一堆 if else 控制状态流转。说实话节点少、逻辑简单的时候状态机方案完全够用代码还直白。但业务一旦复杂起来多级审批、条件分支、并行会签、驳回重做、超时催办、流程版本升级这些需求接踵而至自己维护状态流转就会变成一场灾难。工作流引擎的核心价值是把业务流程从业务代码里剥离出来。开发人员不再为每个节点单独写状态流转逻辑而是把流程定义成一份标准文档由引擎负责“流程走到哪一步、下一步该由谁处理、需要满足什么条件”。业务代码只需要关心当前节点要执行的业务动作。这个思路在业界有标准就是 BPMN 2.0Flowable 就是一套把 BPMN 2.0 落地成可运行引擎的开源实现。1.2 Flowable 是什么和 Activiti、Camunda 怎么选我第一次接触 Flowable 时很好奇为什么这个引擎是从 Activiti 分叉出来的这就要说到历史了。Activiti 早年是企业内容管理厂商 Alfresco 主导的开源工作流项目后来核心团队路线分歧一部分人创建了 Flowable另一部分人继续推进 Camunda。所以三者的血缘关系很近都支持 BPMN 2.0API 风格也相似但设计重心和版本节奏差异很大。选型时最实在的一点是 Java 版本兼容性。Flowable 6.x 对 Java 8 的支持非常好这正好命中搜索引擎里“java1.8可用的开源审批工作流”这个高频需求。Camunda 7.x 在较新版本里开始要求更高的 JDK 版本对还在维护的老项目不算友好。我当时的对比情况如下引擎BPMN 支持Java 版本要求Spring Boot 集成社区资料量适合场景Flowable 6.x完整Java 8官方 Starter开箱即用大企业审批、轻量工作流、老项目改造Activiti 5/6完整Java 7/8也支持中老项目维护、历史代码迁移Camunda 7完整新版要求更高官方 Starter大复杂流程编排、需要流程可视化运营jBPM完整版本差异大相对复杂中Red Hat 生态、案件类型流程1.3 三个核心模块与五个核心 ServiceFlowable 不是只有 BPMN 流程引擎它还包含 CMMN 案例管理引擎和 DMN 决策表引擎。日常审批类工作流用到的是 BPMN 部分CMMN 适合没有固定起点终点、靠事件驱动的场景DMN 则可以把“请假天数大于 3 天走总监审批”这类规则抽出来做成决策表。我这次的项目主流程只用 BPMN但设计时给 DMN 留了扩展位。很多新手只背 API 不看底层出了问题很难排查。我建议先把 Flowable 的核心 Service 记熟RepositoryService 管流程部署和流程定义RuntimeService 管流程实例的运行TaskService 管任务处理HistoryService 管历史数据查询IdentityService 管用户、组和关系。整条全流程链路本质上就是这五个 Service 配合数据库表在跑。理解清楚这一层后面所有的代码逻辑都能串起来。2. 全流程落地第一步工程搭建与 BPMN 流程定义2.1 Spring Boot 快速接入 Flowable我这次项目用的是 Spring Boot 2.7 加 Flowable 6.7.2接入过程非常简单引入依赖、写配置、放流程文件、启动。Flowable 官方提供了 flowable-spring-boot-starter启动时会自动初始化数据源、创建数据表同时会扫描 classpath 下的流程定义文件完成自动部署。这个“自动”在开发阶段很省事但生产环境我建议关掉部分自动建表能力改用受控的部署方式。Maven 依赖要锁版本这点特别重要。Flowable 6.x 和 7.x 的部分 API 有细节差异网上搜到的旧博客直接抄代码容易编译报错。我的 pom 核心配置如下dependency groupIdorg.flowable/groupId artifactIdflowable-spring-boot-starter/artifactId version6.7.2/version /dependency启动后看日志Flowable 会打印数据库初始化和流程定义部署信息。有个容易被忽略的细节如果数据库账号没有建表权限启动会直接报错需要预先用 Flowable 提供的 SQL 脚本手动建表。第一次部署到生产环境时建议先手动执行脚本并验证权限而不是依赖应用自动建库建表。2.2 一张 BPMN 流程定义图是怎么来的Flowable 支持通过 Flowable Modeler 在线绘制流程图也支持直接编写 BPMN 2.0 XML 文件。我的经验是小规模项目直接写 XML 最可控也方便代码审查和版本管理。新增一个 bpmn20.xml 文件放到 resources/processes 目录项目启动时引擎会自动扫描部署。下面是一份完整可用的请假审批流程定义包含“3 天内经理审批3 天以上总监审批”的条件分支?xml version1.0 encodingUTF-8? definitions iddefinitions xmlnshttp://www.omg.org/spec/BPMN/20100524/MODEL xmlns:flowablehttp://flowable.org/bpmn xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance targetNamespacehttp://example.com/process/leave process idleaveProcess name请假审批流程 isExecutabletrue startEvent idstartEvent name申请发起/ sequenceFlow idflow1 sourceRefstartEvent targetRefleaveApply/ userTask idleaveApply name填写请假申请 flowable:assignee${applyUser}/ sequenceFlow idflow2 sourceRefleaveApply targetRefdaysGateway/ exclusiveGateway iddaysGateway name天数判断/ sequenceFlow idflow3 sourceRefdaysGateway targetRefmanagerApprove conditionExpression xsi:typetFormalExpression ![CDATA[${days 3}]] /conditionExpression /sequenceFlow sequenceFlow idflow4 sourceRefdaysGateway targetRefdirectorApprove conditionExpression xsi:typetFormalExpression ![CDATA[${days 3}]] /conditionExpression /sequenceFlow userTask idmanagerApprove name部门经理审批 flowable:assignee${managerUser}/ sequenceFlow idflow5 sourceRefmanagerApprove targetRefhrArchive/ userTask iddirectorApprove name总监审批 flowable:assignee${directorUser}/ sequenceFlow idflow6 sourceRefdirectorApprove targetRefhrArchive/ userTask idhrArchive name人事确认归档 flowable:assignee${hrUser}/ sequenceFlow idflow7 sourceRefhrArchive targetRefendEvent/ endEvent idendEvent name流程结束/ /process /definitions写完后启动项目日志里如果出现流程部署成功的提示说明定义没有问题。有一点要提醒实际 XML 里各种命名空间前缀必须声明完整少一个 xmlns 就可能解析失败而这类错误在部署阶段才会暴露日志里的堆栈信息又长又绕排查起来很费时间。2.3 手动部署与流程版本管理自动扫描很快但生产环境发布时我更喜欢用代码手动部署这样可以主动控制部署名称、类别也为后续排查版本问题留下明确记录。部署代码非常简单Autowired private RepositoryService repositoryService; public void deployLeaveProcess() { Deployment deployment repositoryService.createDeployment() .addClasspathResource(processes/leave.bpmn20.xml) .name(请假审批流程_20250201) .category(leave) .enableDuplicateFiltering() .deploy(); }这里面有两个细节。第一个是 enableDuplicateFiltering开启之后如果同名部署已经存在且文件内容没有变化引擎会跳过重复部署避免每次发布都生成一条无意义的部署记录。第二个是版本机制Flowable 中流程定义版本按 key 区分每次部署同 key 的流程版本号递增。新版本只对之后启动的流程实例生效老版本会继续支撑已经运行的实例互不干扰。这相当于对流程做“灰度发布”线上变更时非常实用不用担心流程走一半被新版定义打断。3. 全流程运行期核心启动实例、任务分配与网关分支3.1 启动流程实例前先把流程变量搞清楚流程定义是图纸流程实例是按图纸造出来的那辆车。每次启动实例都可以携带一批流程变量引擎会保存到运行时变量表中供后续节点读取比如上面 XML 里的${days 3}就要靠启动时传入的 days 变量来判断走哪条分支。启动流程实例的代码非常直白Autowired private RuntimeService runtimeService; public void startLeave(String applyUser, int days) { MapString, Object variables new HashMap(); variables.put(applyUser, applyUser); variables.put(days, days); variables.put(applyTime, new Date()); ProcessInstance processInstance runtimeService .startProcessInstanceByKey(leaveProcess, businessKey_ System.currentTimeMillis(), variables); }startProcessInstanceByKey 的第一个参数是流程定义 key对应 XML 中 process 标签的 id。第二个参数是可选的业务键 businessKey它负责建立业务系统与流程实例的关联。我的习惯是用业务表主键或者业务单号作为 businessKey而不是塞进流程变量里。因为 Flowable 支持按 businessKey 直接查询流程实例效率比遍历变量要高得多日志排查时也清晰。3.2 任务是怎么分配到人头上的流程启动后userTask 会生成待办任务。上面 XML 里用了 flowable:assignee${applyUser} 这种动态写法意思是把任务分配给流程变量 applyUser 指定的用户。实际项目里“填写申请”这一步通常由当前登录用户发起所以 assignee 往往就是发起人 ID。这里要分辨 assignee 和 candidate 的区别assignee 直接指定办理人用候选人机制的话任务会进入多个用户的待办列表谁先认领谁处理。如果想让某类角色的人都能看到任务可以用候选组。比如总监审批设置 flowable:candidateGroupsdirectorGroup查询接口就得改成 taskService.createTaskQuery().taskCandidateGroup(directorGroup)。另一个新手很容易踩的坑是任务完成后它就从运行时表消失了你再用 TaskQuery 去查同一个任务 ID 是查不到的必须用 HistoryService 查询历史任务才能看到。Autowired private TaskService taskService; public void completeTask(String taskId, MapString, Object taskVariables) { Task task taskService.createTaskQuery().taskId(taskId).singleResult(); if (task null) { throw new IllegalStateException(任务不存在或已处理完); } taskService.complete(taskId, taskVariables); }完成任务时传入的变量也藏着门道。setVariable 是往整个流程实例上设置变量setVariableLocal 只对当前执行节点生效。审批意见、审批人这种每个节点独立的数据用 local 变量更合适免得不同分支的变量互相覆盖。请假天数、申请人这类贯穿全程的数据则应该用全局流程变量。3.3 条件分支与网关的设计思路很多新手第一次画 BPMN 时会把排他网关理解成 if else 的图形版。这个理解方向没错但有几个关键点容易被忽视。排他网关的出线按定义顺序判断找到第一条满足条件的线就继续其他线不再判断。如果所有条件都不满足引擎会直接抛异常流程实例就“死”在那里。所以设计网关时最后一条出线最好写成默认兜底分支或者保证条件必然覆盖所有情况。我习惯在流程设计里坚持“宁可卡住也不放走”的原则。条件变量没传或者数据异常时让流程停在网关处等待人工干预比静默流向错误分支好得多。并行网关则是另一套逻辑它会分裂出多个执行分支等所有分支都到达汇合网关后再继续向下走。典型的场景是“经理审批和财务审批并行两边都通过再进入下一步”。处理并行分支时要注意流程实例下的子执行实例可能很多查询任务时不要只盯着 processInstanceId否则容易出现“明明流程还在跑却查不到任务”的错觉。4. 全流程进阶环节会签、监听器、驳回与历史追踪4.1 会签多实例审批的三种模式会签是审批场景里的高频需求Flowable 通过多实例节点实现。多实例的语义有三种所有审批人全部通过才继续叫或签其实也不完全准确更常见的说法是“会签”和“票签”。简单理解会签就是全部人都同意或签是任意一个人同意即可票签是按通过比例决定结果。这个比例可以由一个变量控制比如${nrOfCompletedInstances / nrOfInstances 0.6}。实现多实例的关键是配置 multiInstanceLoopCharacteristics并理解引擎内置的几个变量nrOfInstances 表示总实例数nrOfActiveInstances 表示当前活跃实例数nrOfCompletedInstances 表示已完成实例数loopCounter 表示当前循环索引。比如一个由任务候选人列表驱动的会签节点userTask idmultiApprove name多人会签 flowable:assignee${assignee} multiInstanceLoopCharacteristics isSequentialfalse flowable:collection${approverList} flowable:elementVariableassignee completionCondition${nrOfCompletedInstances / nrOfInstances 0.6}/completionCondition /multiInstanceLoopCharacteristics /userTask这里 flowable:collection 指向的流程变量必须是集合引擎会遍历集合为每个元素生成一个实例。会签节点上通常还要处理好“最后一个人怎么收尾”的逻辑比如最后一个审批人完成时把汇总结果写进流程变量。这个动作可以用多实例节点结束时的执行监听器来做而不是依赖任务监听器。4.2 监听器的正确定位监听器是扩展 Flowable 能力最常用的手段分两类执行监听器和服务任务相关的事件监听。执行监听器监听流程节点的启动、结束和路由事件适合做节点进入后的数据初始化、节点结束后的业务回调。任务监听器监听任务的创建、分配、完成、删除事件适合做消息通知、待办推送。监听器写起来不难难的是搞清它的执行时机和事务边界。任务监听器里抛一个异常会直接影响当前任务的完成事务导致整个任务回滚。我踩过一个真实的坑在任务完成事件里调外部接口推送审批结果外部接口超时结果流程事务一直被拖住用户点了很多次“完成”都没反应。后来改成只做本地数据写入和异步消息发送外部接口一律放到独立的消息队列里消费问题才解决。4.3 驳回、撤回与流程终止的常见玩法驳回和撤回是审批系统绕不开的需求Flowable 本身没有直接的“驳回” API但可以用运行时服务里的流程实例迁移能力实现跳转。核心是创建一个 ChangeActivityStateBuilder指定要跳转的当前节点和目标节点然后执行。这种跳转适合“驳回至发起人重新填写”的简单场景。复杂一点的驳回需求比如“驳回到上一层审批人”或者“退回到指定历史节点”我建议在流程定义里设计明确的驳回网关和驳回事件而不是在代码里硬跳。为什么因为硬跳会破坏 BPMN 轨迹的连贯性历史数据里会留下跳转痕迹给后续流程追踪和统计造成麻烦。流程终止相对简单runtimeService.deleteProcessInstance 可以删除运行时实例历史数据默认保留这样既能终止流程又不丢失审计记录。4.4 历史数据查询与流程追踪流程跑完之后所有运行时数据都会归入历史表Flowable 提供了一组专门的历史查询接口。怎么做流程追踪我的做法是三分支组合查询先用 HistoricProcessInstanceQuery 查流程实例基本信息再看 HistoricTaskInstanceQuery 查所有已经处理完的任务最后用 HistoricActivityInstanceQuery 查完整的事件轨迹包括每个节点的开始和结束时间。把三份数据拼起来就能在后台页面画出清晰的流程进度时间线。历史查询在流程量大的时候性能掉得很快这是后话。但我要提醒一个被很多人忽略的点在运行时任务上写代办查询时用任务表关联流程实例表的 join 查询往往效率不高。更合适的做法是先按用户查询任务表拿到候选流程实例 ID 列表再按 ID 集合去查流程实例详情分步查询配合缓存效果比大 join 好得多。5. 全流程工程化踩坑实录与性能优化5.1 数据库表结构先搞明白再谈优化Flowable 的表按前缀分成了几类这一点不掌握性能排查无从下手。ACT_RE_ 是流程定义和部署的静态资源表ACT_RU_ 是运行时表保存当前活跃的流程实例、任务、变量、作业等流程结束会清理ACT_HI_ 是历史表记录所有已完成流程和任务的数据ACT_GE_ 是通用数据表ACT_ID_ 是身份信息表。运行时表的增长规模直接决定大多数查询的性能历史表的增长则决定数据归档策略。我项目里就出现过 ACT_RU_TASK 表数据量飙升的问题。排查后发现原因是定时发起的大量流程实例一直没有走完任务全部卡在等待节点上运行时任务越积越多。后来加了流程超时提醒机制并定期清理僵尸流程实例运行时表才恢复正常。这里也引出一个结论运行时表数量庞大时先别急着优化 SQL先查业务上是否有流程未被正确推进。5.2 高频问题速查表结合我自己的项目经验把常见的 Flowable 问题整理成了一张速查表排查顺序和解决方案都写进去了遇到问题可以直接对照现象可能原因排查思路解决方案流程实例启动后查不到任务任务被候选人机制分走了或流程已瞬间执行完毕进入历史用 HistoryService 查历史任务确认区分运行时任务与历史任务查询指定用户看不到待办assignee 与 candidate 混用查 ACT_RU_IDENTITYLINK 确认任务分派类型统一使用 assignee 或 candidate 查询 API网关走错分支条件变量类型不匹配或变量未传查 ACT_RU_VARIABLE 查看变量值保证条件变量类型一致并设置默认分支重复部署产生多版本流程定义未开启去重或文件内容每次都在变查 ACT_RE_DEPLOYMENT 的部署时间开启 enableDuplicateFiltering避免动态生成 XML删除流程实例删不掉有子流程或子执行实例引用查 ACT_RU_EXECUTION 看嵌套实例先删子执行实例再删主流程实例完成任务时接口超时拖事务监听器内做了同步外部调用检查任务监听器是否有 RPC 调用异步化外部调用监听器只做本地写入5.3 从全流程视角看性能优化与扩展Flowable 性能优化的切入点第一个是数据库索引。运行时表查询频率最高的字段无非是任务的处理人、流程实例的 businessKey、变量的名称这些字段在默认建表脚本里不一定都有理想索引。我建议在初始化完成后检查 ACT_RU_TASK、ACT_RU_VARIABLE、ACT_HI_PROCINST 的索引情况按实际查询条件补充复合索引。第二个切入点是历史数据归档。随着流程持续运行ACT_HI_ 系列表会无限膨胀即使有索引也扛不住量级增长。常规做法是定时把超过半年的历史数据迁移到归档库生产环境只保留热数据。迁移作业用业务封装的清理逻辑比直接删表安全得多也方便将来按审计要求追溯。第三个切入点是异步执行器。Flowable 支持把定时器、异步任务交给独立的异步执行器处理避免阻塞流程事务。并行分支多、任务量大时异步执行器的线程池参数要重点关注。我项目里把默认的空闲等待启动时间调低核心线程数适当加大定时提醒和超时处理类任务的吞吐明显改善。我从这个项目里感受最深的一点是Flowable 确实强大但它不是“装上框架就完事”的工具。流程定义、运行期处理、历史归档、性能调优每一个环节都要有人真正理解原理并持续维护。最后再分享一个经验任何时候都要保证业务系统有独立的流程日志表记录每次流程事件的关键上下文。Flowable 自己的历史表再完善也替代不了业务层面的审计日志两者配合使用线上排查问题才能做到游刃有余。