Spring Boot 整合 Flowable 的企业级落地指南

做中后台系统,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 的版本控制核心就两个字:KEYVERSION

你可以把 .bpmn20.xml 扔在 classpath:processes/ 下让它启动时自动部署,也可以用 RepositoryService 通过 API 动态传 XML 部署。

当部署一个流程时,引擎会查数据库有没有同 KEY 的流程。有,VERSION 就 +1;没有,VERSION 就是 1。

这里有个极易踩坑的地方 :部署新版本后,新启动的实例会走新流程,但已经跑在路上的老实例,依然会死死咬住它启动时的老版本流程定义

如果业务方非逼着老实例也走新流程,Flowable 提供了 ProcessInstanceMigrationValidator 做强制迁移。但我劝你三思,线上跑着的数据给你迁移崩了,背锅的可是你。通常的做法是:老实例老办法走完,新实例走新办法。


4. 动态表单与变量:坚决弃用自带表单引擎

企业级避坑第一法则:千万别用 Flowable 自带的 Form Engine! 那玩意儿简陋得令人发指,根本应付不了企业级复杂的动态表单。

标准做法是:业务表单与流程引擎彻底解耦

  1. 表单数据老老实实存在你的业务表(比如 leave_request)里。
  2. 启动流程时,只把"业务主键 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 :绑在连线、事件、任务上。触发 startendtake。用来记流转日志。
  • Task Listener只能 绑在 UserTask 上。触发 createassignmentcompletedelete。用来动态算审批人。

血泪教训 :千万别在 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_*),在历史表里记个删除原因。

归档策略

  1. 热数据(运行时 + 近 3 个月历史)留在 MySQL。
  2. 冷数据(3 个月前结束的):写个定时任务(比如 XXL-JOB),每天凌晨跑。查出 ACT_HI_PROCINSTEND_TIME_ 早于 3 个月前的 ID。
  3. 把这些 ID 对应的所有历史表数据批量 INSERT 到归档库(ClickHouse 或者单独的 MySQL 归档库)。
  4. 从主库 DELETE

切记 :归档操作必须加分布式锁,且分批处理(每次 1000 条)。你要是敢一次性 delete 几十万条,数据库直接锁死给你看。


8. 性能调优与集群部署

8.1 性能调优

  1. 异步化 :ServiceTask 里如果有复杂逻辑或调第三方接口,必须在 BPMN 里勾选 Asynchronous Continuations。引擎会把它转成 Job 丢给后台线程池,不然大事务会把数据库连接池耗干。
  2. 历史级别 :Flowable 有 none, activity, audit, full。生产环境用 audit(默认)就够了,记录实例、任务和活动。千万别手贱用 full,连变量每次变更都记,性能直接拉胯。配置:flowable.history-level: audit
  3. 连接池:用 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/


相关推荐
苏三说技术2 小时前
Kafka已正式接入AI
后端
企业数字化笔记2 小时前
AI写的系统出现504怎么办?接口超时和数据库慢查询排查
数据库·后端
IT_陈寒2 小时前
Redis的Set操作居然能把我的服务整挂了?
前端·人工智能·后端
momo061173 小时前
Redis新手入门 -- 学习笔记
redis·后端
井云AI3 小时前
vendor 资源包是什么:井云 DSH 客户端打包为什么离不开它
后端·智能体小程序
2601_962218613 小时前
万象生鲜系统订单全生命周期状态同步技术实现业务可视
大数据·数据库·人工智能·python·算法
重生之我是Java开发战士3 小时前
【Spring Cloud】Nacos注册中心
后端·spring·spring cloud
Wang's Blog3 小时前
Java框架快速入门:Spring Security+OAuth2之用户注册与唯一性校验实现
java·数据库·spring
程序员20073 小时前
AI 编程工具链碎片化?基于多端兼容的统一 Agent Rules 架构实践
后端