搞Java后端的同学,几乎都躲不过“工作流”这个坑。尤其是一旦系统里出现“审批”两个字,流程引擎的选型就绕不开。我最早也试过自己写状态机,后来被“驳回”“会签”“加签”“催办”这些需求折腾到怀疑人生。换成JAVA开源工作流平台,还特意找了带完整源码和后端源码的项目来改造,才总算把这块稳定下来。这篇我就把选型、源码结构、二次开发和排查问题的经验详细整理一下,给准备接入手工流的朋友一个参考。
这个内容适合谁?适合对工作流有一定概念但没深入跑通过源代码的同学,也适合项目里已经集成了流程引擎但经常出bug想要自己定位问题的开发。哪怕你是初学者,跟着后面的实操步骤也能把一个带后端源码的开源工作流平台跑起来,要是能通读一遍后端源码,收获会更大。
1. 为什么我建议直接用开源工作流平台
1.1 自己写工作流的状态机有多坑
先说我自己。第一版请假审批,我设计了一个简单的状态字段:0待提交、1审批中、2通过、3驳回。看起来没问题,后来产品提出,部门经理驳回后要回到申请人重新提交,提交后不能再走部门经理,直接跳到HR审批。这时候状态流转已经超过原来预想的线性路径了。再后来加“会签”,要两个人同时审批;加“或签”,一个人通过即可;加“超时自动提醒”,我连状态机都画不出来了。
自研状态机的本质是“用代码硬编码节点和跳转关系”,一旦节点数量增多,流转条件变得复杂,每次改动都需要重新发版,还要考虑并发情况下状态更新的原子性。我曾经历过一次线上BUG:两个人同时点击审批通过,由于状态判断和更新之间没有加锁,任务被重复处理,业务数据连续审批了两次。后来虽然加了分布式锁,但状态机代码已经膨胀到完全不能维护。
所以后来我有个比较坚定的观点:如果你的业务里只有两三个固定审批节点,并且永远不打算增加,自己写确实可以;但只要流程可能变,最好直接选一个成熟的开源工作流平台。况且开源平台现在还做得挺重,功能不只是画流程图,还包括流程定义、任务管理、历史追踪、变量传递,这一套自研下来的成本远比你想象得高。
1.2 开源工作流平台能省多少事
JAVA开源的流程引擎,普遍实现了BPMN 2.0规范。BPMN就是一套描述业务流程的标准图形符号,最大的好处是流程定义独立于代码,审批节点的增删改通过改流程图就能完成,不需要改Java代码。像驳回、加签、会签、并行分支这种高频需求,开源引擎都已经内置了,不用自己造轮子。
另外,带源码的项目还有几个隐形价值:
- 出了问题能自己查。生产环境流程卡住,我可以直接打开源码看引擎内部怎么处理的,而不是盲猜。
- 能二次开发。比如自定义任务监听器、自定义表单、对接自己的用户权限体系,这些需求官方文档往往写得不够细,源码就是最好的参考。
- 能避免黑盒风险。商业版或者闭源SaaS平台,一旦出现深度适配需求会很被动,开源代码永远掌握在自己手里。
我当时选的方案是:找一个开源后台管理框架,再集成开源的流程引擎模块。后来发现很多开源框架已经把工作流模块做好了,前端带设计器,后端带Controller和Service,源码齐全,直接拿来做业务二次开发会很顺手。相比从零开始搭一套流程系统,这种方式是真的能省下几周时间。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 主流JAVA开源工作流平台怎么选
2.1 四大开源引擎的对比
先不说具体集成,单看市面上常见的几款开源工作流引擎,你要有数。下表是我个人整理的主要对比:
| 引擎 | 规范支持 | 社区活跃度 | 集成难度 | 典型使用场景 |
|---|---|---|---|---|
| Activiti | BPMN2.0 | 老牌,活跃但版本分裂 | 中等 | 传统企业审批、OA系统 |
| Flowable | BPMN2.0 | 活跃,从Activiti fork出来 | 中等 | 同类,功能更丰富 |
| Camunda | BPMN2.0 + CMMN + DMN | 活跃,社区版本功能强大 | 中等偏高 | 微服务编排、决策管理 |
| jBPM | BPMN2.0 | 一般,和Drools生态绑定 | 中 | 规则引擎强绑定场景 |
如果你只是做企业内部审批,Activiti和Flowable体验差不多;如果你要轻量集成,SnakerFlow这类国产轻量引擎也是一种选择。SnakerFlow不是BPMN完整实现,而是用更简单的方式定义流程,上手很快,适合中小团队,但功能相对少一些。
我实际用的是Flowable,一是因为它在审批流程这个领域非常成熟,二是文档和示例比较多,三是有非常完整的源码工程。这篇文章后面就以Flowable为例讲,但整体思路对其他引擎也通用。选引擎的时候,不要只看Stars,还要看团队里有没有人会维护。如果身边没人用过,建议选最容易搜到答案的,否则后期被一个冷门问题卡住会非常痛苦。
2.2 基于源码二次开发的正确姿势
拿到一个开源工作流平台源码,很多人第一反应是直接改源码。比如想加一个审批按钮,就直接在Controller里改业务逻辑,这样其实很坑。因为开源项目大版本升级后,源码改动会冲突,而且团队里其他人不一定熟悉你改过的部分。
我推荐的姿势是“外挂式改造”:
- 保留源码仓库的原始tag,比如flowable-6.8.0,作为基线版本。
- 自己的业务改动全部放在独立模块里,与引擎源码解耦。
- 引擎的行为尽量通过配置项、监听器、事件机制、API调用去扩展,而不是改引擎内部代码。
- 如果确实需要修改引擎内核,宁可fork一份,形成自己的发行版本,也要保证改动可追溯。
就拿我这里用的开源后台管理框架来说,它本身有system、infra这些基础模块,工作流模块作为一个独立业务模块存在。我在里面去写自己的审批业务类,用到流程引擎的API,尽量避免去改引擎源码。后面源码升级的时候,工作流模块的改动可以单独迁移,非常省心。
3. 后端源码结构和核心原理拆解
3.1 拿到源码后,从哪里开始读
一个典型的JAVA开源工作流平台后端,通常是Spring Boot多模块工程。以我用的框架为例,目录大概长这样:
ruoyi-admin:启动模块,端口和全局配置ruoyi-system:系统管理模块,用户、角色、菜单ruoyi-workflow:工作流模块,包含流程定义、任务、审批等接口ruoyi-common:通用工具类ruoyi-framework:框架配置,安全、拦截器、AOP
如果你直接下载了Flowable引擎源码,结构会不太一样,它是围绕引擎核心拆的:
flowable-engine:流程引擎核心,ProcessEngine、RuntimeService、TaskService等都在这flowable-bpmn-converter:BPMN文件解析器,把XML转换成引擎模型flowable-engine-common:公共工具和引擎配置flowable-spring:Spring集成包
建议不要上来就到处翻,我一般是这样读的:
- 先跑通Demo,用源码里的启动类把服务拉起来,观察控制台日志。
- 从“流程启动”这个业务点入手,找到Controller入口,再追Service和ServiceImpl,最后进入引擎API。
- 用断点调试,看一次流程启动过程中引擎内部创建了哪些对象,执行了多少个节点。
只有跟着一个具体的业务请求走一遍,源码目录才会从一堆类名变成一张地图。千万不要想着把源码从头读到尾,那会让人很快放弃,最好的方式是按需求读,按异常读,按问题读。
3.2 工作流引擎最关键的几张表
后端源码里面有各种Entity和Mapper,但最终流程数据都存在数据库表里。工作流引擎表通常分为三类,这里以Flowable为例:
| 分类 | 表前缀 | 说明 |
|---|---|---|
| 流程定义和模型 | ACT_RE_* | 存放流程定义文件、流程模型、业务分类等信息,如ACT_RE_PROCDEF |
| 运行时数据 | ACT_RU_* | 正在执行的流程实例、执行流、任务、变量等,如ACT_RU_EXECUTION、ACT_RU_TASK |
| 历史数据 | ACT_HI_* | 已经结束的流程实例、任务、活动、变量等,如ACT_HI_PROCINST |
刚接触时最要关注的是ACT_RU_TASK,因为“待办任务”就是查这张表。流程流转到某个用户任务节点,引擎会往这张表插入一条任务记录。审批人完成任务后,该记录从运行表删除,同时写入历史任务表ACT_HI_TASKINST。
如果看到流程实例还在跑,但ACT_RU_TASK里查不到任务,大概率是走到了网关节点或等待状态,不是BUG,先别急着重启。你要学会把三张表联系起来看:运行表和历史表通过PROC_INST_ID_关联,任务表通过PROC_DEF_ID_关联流程定义。排查问题的时候,这个关联关系非常关键。
3.3 一次流程流转的完整过程
把一次简单审批的流转过程拆开,其实就五步:
- 发布流程定义:把BPMN 2.0 XML文件部署到引擎,引擎会解析XML内容,生成流程定义数据,存到
ACT_RE_PROCDEF。 - 启动流程实例:调用
runtimeService.startProcessInstanceByKey(),引擎根据流程定义创建流程实例,并推进到“开始事件”之后的第一个节点。 - 到达用户任务节点:引擎碰到
userTask节点时,会创建一条待办任务写入ACT_RU_TASK,同时通常还会设置候选人或候选人组。 - 审批人完成当前任务:调用
taskService.complete(taskId),引擎根据当前节点的流转路线走到下一个节点。如果连的是排他网关,会按条件表达式计算结果选择路径。 - 流程结束:走到结束事件后,运行时数据被清除,历史数据写入
ACT_HI_PROCINST,状态变成“已完成”。
整个过程中,流程变量是节点的“传话筒”。比如审批结果approved,填写在流程变量里,排他网关根据这个变量决定走通过还是驳回分支。理解了这条主线,后面二次开发就能自然展开。如果一开始对BPMN术语感到陌生,也别慌,把它当成一套流程图引擎的翻译规则就行。
4. 实操:从部署到跑通第一个流程
4.1 环境准备与源码编译
先把环境配置好,我用的版本组合仅供参考:
- JDK 1.8 或 11(如果用的是最新Spring Boot 3.x,那要JDK17)
- Maven 3.6+
- MySQL 5.7 或 8.0
- Redis(一般后台管理框架都会用,工作流本身不强制)
- 前端环境按项目说明准备好,主要用到Node.js和npm
拿到源码后,第一步先在根目录执行:
bash复制mvn clean install -DskipTests
这一步会下载依赖并执行所有模块的编译打包。如果公司内网有私服,建议把Maven配置里的镜像源调整一下,比如用阿里云的中央仓库。这里有个经验:编译时如果报“找不到symbol”或者包名不存在,多半是某些子模块没有安装到本地仓库,就再执行一次mvn install,注意模块之间的依赖顺序。
4.2 数据库初始化与启动配置
编译通过后,准备一个空白数据库。Flowable引擎可以配置成自动建表,也可以手动执行初始化SQL。在后台管理框架中,通常启动时就会自动执行建表脚本,你只需要配置好数据源。
对应的application.yml大概这样:
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/workflow_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
flowable:
database-schema-update: true
async-executor-activate: true
database-schema-update设为true后,引擎启动时会自动创建或升级所需表。第一次启动注意看日志,如果表数量不对,大概率是MySQL版本兼容问题,比如老版本驱动不支持utf8mb4,需要把编码参数去掉或者换新版本驱动。还有个小坑:如果库名里有横杠,MySQL连接可能会识别异常,所以数据库名尽量全小写字母加下划线。
4.3 用设计器画一个审批流程
启动成功后,打开前端页面,进到工作流菜单。一般开源平台都会带一个在线流程设计器。设计器产生的模型可以保存为BPMN 2.0 XML,也可以直接从模型部署。
画一个最简单的请假审批流程:
- 拖一个“开始事件”
- 添加一个“用户任务”,命名为“部门主管审批”
- 再添加一个“用户任务”,命名为“HR确认”
- 加一个“结束事件”
- 用连接线按顺序连接起来
每个用户任务都要设置“受理人”或“候选人”,在Flowable中可以通过flowable:assignee表达式指定,比如${assignee},这样启动流程时传对应的审批人ID就行。如果走的是候选人组,用flowable:candidateGroups指定角色编码,例如${deptLeader}。
画完后保存并部署。部署成功后,在流程定义列表里应该能看到刚部署的模型,流程状态是“激活”。这里的“部署”对应引擎的repositoryService,它会解析XML并生成流程定义。第一次画图经常遇到的问题是“连线没有拖到边框上”,导致流程根本走不通,检查时把XML打开看看节点和线是否是完整的。
4.4 后端API调用与流程启动
流程定义部署好后,业务系统只需要调用后端接口即可。核心代码很简单:
java复制// 启动流程实例
ProcessInstance processInstance = runtimeService
.startProcessInstanceByKey("leaveProcess",
businessKey,
variables);
// 查看某人的待办任务
List<Task> tasks = taskService.createTaskQuery()
.taskAssignee(userId)
.orderByTaskCreateTime()
.desc()
.list();
// 完成任务
taskService.complete(taskId, variables);
但实际项目中,你的Controller不应该直接依赖RuntimeService,因为这样会把流程引擎API暴露给前端,不够安全。我一般会在工作流模块里封装一层WorkflowService,统一处理流程启动参数校验、业务表单数据转换、审批记录落库等操作,再在内部调用流程引擎API。
比如启动请假申请,前端传过来的是一个DTO,包含userId、days、reason。你要把它转成Map<String,Object>变量,把businessKey设为业务单号,再启动流程。之后任务查询接口返回的不只是任务ID,还要关联请假单的业务数据,否则前端每个待办点开都是空白,这层封装是必须的。
5. 二次开发中的扩展点与集成方案
5.1 自定义业务表单与流程变量
工作流平台只能管“流程”,管不了你的具体业务字段。请假单有请假天数、事由、附件,报销单有报销金额、发票,这些业务数据不能直接扔进引擎表,而是应该存在自己的业务表,然后通过businessKey和流程实例关联。
常见做法:
- 业务表保存一个
instanceId字段,记录流程实例ID。 - 启动流程时传入
businessKey,值为业务主键。 - 查询流程详情时,根据
businessKey反查业务数据。
流程变量的使用也有讲究。只有需要参与流程流转控制、网关条件判断、监听器使用的数据,才应该放进变量。大对象、文件流这些东西不要往流程变量里塞,因为变量会存到数据库大字段,频繁读写会影响性能。我在实际项目里最常用的变量就是approveResult、approveUser、formNo这几个轻量字段。
5.2 使用监听器和Spring容器深度集成
开源流程引擎支持在流程节点上挂监听器。比如在任务结束时自动更新业务状态、发送站内信通知申请人。实现方式是实现TaskListener接口,并重新集成到Spring容器中。
java复制@Component
public class NotifyTaskListener implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
String eventName = delegateTask.getEventName();
String assignee = delegateTask.getAssignee();
// 这里可以注入自己的业务Service
// 比如给审批人发送站内信或者更新业务表单状态
System.out.println("task " + delegateTask.getId()
+ " event " + eventName
+ " assignee " + assignee);
}
}
然后在BPMN XML的userTask节点上配置:
xml复制<userTask id="leaveTask" name="部门主管审批" flowable:assignee="${assignee}">
<extensionElements>
<flowable:taskListener event="create" class="com.demo.NotifyTaskListener"/>
</extensionElements>
</userTask>
如果你用的平台是Spring Boot集成方式,也可以直接用@Bean注入监听器。这里有个注意点:监听器里尽量不要做耗时操作,比如发短信、调外部接口。如果真的要做,建议丢到消息队列异步处理,否则任务完成接口会被拖慢。我踩过这个坑之后,把通知逻辑全部改成了异步事件,效果直接好了很多。
5.3 和权限体系对接的常见方案
开源工作流的权限模型不一定匹配你的系统。比如引擎里的“组”概念,对应不上后台管理框架里的“角色”。我建议做一层映射,而不是强制让引擎感知你的角色表。
具体做法是:
- 把系统角色编码与引擎候选人组编码设计成一致的规则,比如角色
dept_leader对应引擎组dept_leader。 - 创建流程节点时,通过表达式指定
candidateGroups为角色编码。 - 查询待办任务时,根据当前用户拥有的角色编码去查引擎的任务列表。
java复制List<String> roles = roleService.queryRoleCodes(userId);
List<Task> tasks = taskService.createTaskQuery()
.taskCandidateGroupIn(roles)
.list();
还要考虑“指定审批人”的特殊场景。比如申请人提交时选择“由张三审批”,那流程变量里就带一个assignee,节点用${assignee}获取审批人。这个逻辑看起来简单,但实际中经常有用户选择了已被禁用的人,所以在启动流程前要校验人员状态。
6. 生产环境中的坑与排查经验
6.1 版本冲突和依赖冲突
Maven项目最怕依赖冲突。Flowable和Spring Boot的版本兼容性要求比较严格,比如Flowable 6.x基本对应Spring Boot 2.x,Flowable 7.x对Spring Boot 3.x支持才比较稳。如果强行混合版本,启动时会报类找不到或方法签名不一致。
我遇到过最典型的错误是:
text复制Caused by: java.lang.NoSuchMethodError:
org.flowable.spring.boot.ProcessEngineAutoConfiguration...
这种八成是Flowable依赖版本和Spring Boot自动配置版本不一致。排查思路是执行mvn dependency:tree,找到重复的Flowable依赖,统一通过dependencyManagement锁定版本,不要每个模块自己乱写版本号。最好从依赖源头就梳理清楚,否则后面调试的时间远远超过选型的时间。
6.2 流程实例卡住或任务丢失怎么办
流程在运行时突然不走了,先别急。按下面的顺序查:
- 查
ACT_RU_EXECUTION,看流程实例是否还在。 - 查
ACT_RU_TASK,看当前是否有待办任务。 - 查
ACT_RU_VARIABLE,看关键变量是否存在、值是否符合预期。 - 查引擎日志,有没有抛出异常。
最常遇到的是任务其实存在,但候选人组对不上,导致用户查不到待办。比如节点指定的candidateGroups是roleA,但当前用户的角色是ROLE_A,大小写不一致,查询结果就是空。这种问题用代码对比一下角色映射就能定位。
还有一种情况是流程异常终止了,运行表里没任务,历史表里状态是“异常结束”。这时候要看ACT_HI_ACTINST的最后一个活动节点是什么,再结合流程定义XML,基本能还原卡在哪个环节。排查工作流问题一定要有耐心,把日志和表数据联合起来看,否则很容易被表象误导。
6.3 高并发下的性能优化
工作流引擎的性能瓶颈通常不在引擎本身,而在数据库和接口设计。如果你一个审批系统日请求量很大,注意以下几点:
- 表索引:Flowable自带索引,但自定义的
businessKey关联字段最好也建索引。 - 查询尽量走引擎提供的Query API,不要手动写SQL去查引擎表。
- 如果待办任务列表经常全表扫描,可以考虑把热门字段冗余到业务侧,比如在业务表里冗余
taskId和assignee,避免每次都去查引擎任务表。 - 开启引擎的异步执行器,把事件处理、定时任务异步化。
async-executor-activate: true就是干这个的。
还有一点容易被忽略:流程历史数据增长非常快,生产环境一定要有定时清理策略。Flowable提供历史数据清理机制,但你要结合业务定制保留周期,比如只保留一年内的历史数据。否则表数据量上来后,查询历史审批记录会越来越慢。
6.4 给新手的源码阅读路径
你要是刚开始接触工作流源码,我的建议是先不要碰设计器,也不用一开始就啃ProcessEngine内部实现。先从API使用开始,把官方文档里那几个核心Service过一遍:RepositoryService、RuntimeService、TaskService、HistoryService。然后用Demo工程打断点,观察一次完整的启动和完成操作。等你能解释出每个Service对应数据库里哪几张表,再去读源码里的ProcessEngineConfigurationImpl,理解引擎初始化过程。最后如果你想做深度定制,再去看Command模式和拦截器链,Flowable内部大量使用了命令模式,每个API操作实际上都是提交一个Command到引擎执行。
对一个Java后端来说,工作流引擎能带来的技术积累并不比微服务少。读了源码之后,你甚至能有意识地在自己的业务系统里借鉴它的命令模式、事件分发、状态机建模这些设计思想,这对职业生涯是很加分的。我这里也只是把实际项目中会用到的选型思路、源码结构、核心API、二次扩展和排查方法完整串了一遍,具体到你的项目,业务流程可能完全不同,但大方向是一致的:选成熟引擎、理解核心表、善用扩展点、保留源码基线。跑通一个流程只是开始,能把它稳定地跑在生产环境里,才算真正掌握了工作流平台。
