
做中后台系统,OA、ERP、CRM 这些,迟早要和工作流引擎打交道。Activiti 停更了,Camunda 学习曲线太陡,Flowable 算是目前 Java 圈里的香饽饽。轻量、对 Spring Boot 友好,社区也算活跃。
但看官方文档和网上那些 demo 是一回事,真要在生产环境跑起来,全是坑。今天不扯虚的,直接聊聊我在项目里用 Spring Boot 整合 Flowable 攒下来的实战经验。从建表、表单解耦到多实例会签,再到最后怎么防着它把数据库搞崩,咱们一点点捋。
1. 核心概念:别死记硬背,当成状态机来理解
Flowable 底层是死磕 BPMN 2.0 规范的。看官方文档那些高大上的名词容易晕,其实你把它当成一个"带业务逻辑的状态机"就明白了:
- Process Definition(流程定义) :相当于 Java 里的
Class,就是那个 BPMN XML 解析后的元数据。 - Process Instance(流程实例) :相当于
Object,是定义的一次具体执行。 - Execution(执行令牌):这个概念很关键。单线流程里,它和流程实例是 1:1 的。但一旦遇到并行网关,令牌就会分裂,产生多个 Execution。
- Task(任务):分 UserTask(给人干的)和 ServiceTask(给机器干的)。
- Variable(流程变量):流程流转的"血液"。节点间传数据、网关判断走哪条线,全靠它。
BPMN 的核心元素其实就三类:Events(事件,比如开始、结束、定时器)、Tasks(任务)、Gateways(网关,排他、并行、包容)。别去死记硬背 XML 标签,用 Flowable 官方的建模工具或者 IDEA 插件拖拽几下就明白了。
2. 集成与建表:生产环境的第一道鬼门关
引入依赖很简单,现在 Flowable 都出 7.x 了,别死磕老版本:
xml
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-process</artifactId>
<version>7.0.0</version>
</dependency>
重点说下数据库初始化策略 。Flowable 通过 database-schema-update 控制 DDL:
yaml
flowable:
database-schema-update: true
true:启动时检查表结构,没有就建,版本不对就升级。false:不执行 DDL,表不存在直接报错。create-drop/drop-create:启动建/关时删,或者先删后建。
老鸟忠告 :本地开发你随便用 true,生产环境绝对、千万不要用 true 。让框架自动改线上表结构是找死。正确的姿势是:把 Flowable 的 DDL 脚本抠出来,交给 Flyway 或 Liquibase 做版本化管理,然后把配置改成 false。
3. 流程部署与版本控制:老实例走老流程
Flowable 的版本控制核心就两个字:KEY 和 VERSION。
你可以把 .bpmn20.xml 扔在 classpath:processes/ 下让它启动时自动部署,也可以用 RepositoryService 通过 API 动态传 XML 部署。
当部署一个流程时,引擎会查数据库有没有同 KEY 的流程。有,VERSION 就 +1;没有,VERSION 就是 1。
这里有个极易踩坑的地方 :部署新版本后,新启动的实例会走新流程,但已经跑在路上的老实例,依然会死死咬住它启动时的老版本流程定义 。
如果业务方非逼着老实例也走新流程,Flowable 提供了 ProcessInstanceMigrationValidator 做强制迁移。但我劝你三思,线上跑着的数据给你迁移崩了,背锅的可是你。通常的做法是:老实例老办法走完,新实例走新办法。
4. 动态表单与变量:坚决弃用自带表单引擎
企业级避坑第一法则:千万别用 Flowable 自带的 Form Engine! 那玩意儿简陋得令人发指,根本应付不了企业级复杂的动态表单。
标准做法是:业务表单与流程引擎彻底解耦。
- 表单数据老老实实存在你的业务表(比如
leave_request)里。 - 启动流程时,只把"业务主键 ID"和"核心路由指标"塞进流程变量。
java
Map<String, Object> variables = new HashMap<>();
variables.put("businessKey", "LEAVE-20231024-001"); // 业务主键,用来反查业务表
variables.put("days", 5); // 核心路由指标:请假天数
runtimeService.startProcessInstanceByKey("leave_process", variables);
网关条件路由 :
在排他网关(Exclusive Gateway)的连线上写 UEL 表达式:
- 连线 1(经理批):
${days <= 3} - 连线 2(总监批):
${days > 3 && days <= 7} - 连线 3(CEO 批):
${days > 7}
注意 :排他网关必须配一条 Default Flow(默认连线)。万一哪天业务数据脏了,所有条件都没命中,流程直接卡死,半夜叫你起来修数据的时候你就知道错了。
5. 多实例会签与驳回逻辑
5.1 多实例(Multi-Instance)配置
在 UserTask 上配多实例,就能搞会签或串行。
- 集合变量:
assigneeList(比如["zhangsan", "lisi"]) - 元素变量:
currentAssignee(当前循环到的审批人) isSequential:false 是并行会签,true 是串行。
5.2 完成条件(Completion Condition)
通过 completionCondition 决定多实例啥时候结束:
- 会签(全票通过) :
${nrOfCompletedInstances == nrOfInstances} - 或签(一票通过) :
${nrOfCompletedInstances > 0} - 按比例 :
${nrOfCompletedInstances / nrOfInstances >= 0.5}
注:nrOfCompletedInstances 这些是引擎内置的局部变量,直接用就行。
5.3 驳回与撤回
BPMN 规范里其实没有"驳回"这个概念,本质就是节点跳转 。从 6.4 版本开始,官方给了 ChangeActivityStateBuilder:
java
// 驳回:把当前任务节点强行跳转到指定的历史节点
processRuntime.changeActivityState(
ChangeActivityStateBuilder.builder()
.processInstanceId(processInstanceId)
.moveSingleExecutionToActivityIds(currentExecutionId, targetActivityId)
.build()
);
至于"撤回",其实就是审批人刚点同意,下一节点的人还没处理。你查一下当前任务,把执行令牌挪回原节点,顺便把多余的流程变量清掉就行了。
6. 监听器:一定要用 Spring Bean 注入
监听器是解耦的利器。
- Execution Listener :绑在连线、事件、任务上。触发
start、end、take。用来记流转日志。 - Task Listener :只能 绑在 UserTask 上。触发
create、assignment、complete、delete。用来动态算审批人。
血泪教训 :千万别在 BPMN XML 里写死 Java 类的全限定名(class="com.xxx.MyListener")。一旦你在监听器里想调个 Service,还得自己去 Spring 容器里捞,恶心死。
直接用 delegateExpression 注入 Spring Bean:
java
@Component("dynamicAssigneeListener")
public class DynamicAssigneeListener implements TaskListener {
@Autowired
private UserService userService;
@Override
public void notify(DelegateTask delegateTask) {
String deptId = (String) delegateTask.getVariable("deptId");
String leaderId = userService.getDeptLeader(deptId);
delegateTask.setAssignee(leaderId);
}
}
XML 里这么配:<flowable:taskListener event="create" delegateExpression="${dynamicAssigneeListener}" />。清清爽爽。
7. 历史数据归档:Flowable 最大的痛点
Flowable 的 ACT_HI_*(历史表)是个无底洞。随着时间推移,数据量无限膨胀,查询性能断崖式下跌。生产环境必须做冷热分离。
- 挂起(Suspend):冻结实例,任务无法推进。适用于单据作废但要保留现场。
- 终止(Delete) :物理删除运行时数据(
ACT_RU_*),在历史表里记个删除原因。
归档策略:
- 热数据(运行时 + 近 3 个月历史)留在 MySQL。
- 冷数据(3 个月前结束的):写个定时任务(比如 XXL-JOB),每天凌晨跑。查出
ACT_HI_PROCINST里END_TIME_早于 3 个月前的 ID。 - 把这些 ID 对应的所有历史表数据批量
INSERT到归档库(ClickHouse 或者单独的 MySQL 归档库)。 - 从主库
DELETE。
切记 :归档操作必须加分布式锁,且分批处理(每次 1000 条)。你要是敢一次性 delete 几十万条,数据库直接锁死给你看。
8. 性能调优与集群部署
8.1 性能调优
- 异步化 :ServiceTask 里如果有复杂逻辑或调第三方接口,必须在 BPMN 里勾选
Asynchronous Continuations。引擎会把它转成 Job 丢给后台线程池,不然大事务会把数据库连接池耗干。 - 历史级别 :Flowable 有
none,activity,audit,full。生产环境用audit(默认)就够了,记录实例、任务和活动。千万别手贱用full,连变量每次变更都记,性能直接拉胯。配置:flowable.history-level: audit。 - 连接池:用 HikariCP,连接数别配太大,流程引擎是 IO+CPU 混合密集,50-100 通常足够了。
8.2 集群部署
Flowable 原生支持集群,靠的是数据库级别的分布式锁 。
所有节点连同一个库,把 flowable.async-executor-activate 设为 true。Async Executor 会去抢 ACT_RU_JOB 等表里的记录,通过 SELECT ... FOR UPDATE 保证同一个 Job 只被一个节点执行。
如果并发高到数据库锁成了瓶颈,那就只能二次开发,把 Job 锁替换成 Redis (Redisson) 了。不过说实话,90% 的公司都遇不到这个瓶颈。
9. 权限与多租户:别用引擎自带的用户体系
再次强调:废弃 Flowable 的 IdentityService! 公司都有现成的 Spring Security + JWT 的 RBAC 模型,何必再去引擎里维护一套用户?
查待办任务时,直接拿 Spring Security 上下文里的用户 ID:
java
taskService.createTaskQuery()
.taskAssignee(SecurityUtils.getCurrentUserId())
.orderByTaskCreateTime().desc()
.list();
如果是 SaaS 多租户场景,Flowable 原生支持 tenantId。
部署时带上租户 ID,查询和推进时千万别忘了加 .processInstanceTenantId("tenant_001")。数据量大的话,建议用 MyBatis 拦截器在 SQL 层面强制追加租户条件,防越权。
10. 生产级避坑:事务、死锁与异常
这是区分"玩具"和"生产级"的分水岭。
10.1 事务边界冲突
Flowable 的 API 底层都是 Command 模式,包在它自己的事务拦截器里。如果你的 ServiceTask 里调了个慢 RPC,会导致 Flowable 的数据库事务长时间不提交。
解法:耗时操作必须异步化,或者在 ServiceTask 里只发个 MQ 消息,让消费者去处理。
10.2 死锁预防
如果在同一个 @Transactional 方法里,流程 A 触发流程 B,流程 B 又反过来更新流程 A 的变量,极大概率死锁。
解法 :别在同一个事务里嵌套调多个流程推进 API。用 Spring 的 @Async + @EventListener 或者 MQ 把动作解耦。
10.3 异常补偿
ServiceTask 调外部系统(比如扣库存)失败了,别傻乎乎地抛异常让流程回滚,外部系统可能已经部分成功了。
实战做法 :别太迷信 BPMN 的补偿事件,太复杂。直接在 JavaDelegate 里 try-catch,把错误码塞进流程变量(比如 errorCode = "INSUFFICIENT_BALANCE"),让流程自然走到下一个排他网关,根据错误码路由到"异常处理节点"或"人工干预节点"。简单粗暴,且极好排查。
工作流引擎这东西,入门容易,精通难。核心就一句话:把流程引擎当个纯粹的状态机来用。业务逻辑、权限、耗时操作全给它剥离出去。别想着让它包揽一切,它只是个引擎,不是业务系统本身。理清了这个边界,Flowable 在你手里才能发挥出真正的威力。
🎁 福利时间
如果你正在备战面试或者想要学习其他知识,给大家推荐一个宝藏知识库,作者整理了一些列 Java 程序员需要掌握的核心知识,有需要的自取不谢。
知识库地址:https://farerboy.com/
