☰
Flowable工作流引擎实战:从BPMN建模到审批流落地
2026/10/6 8:45:44 网站建设 项目流程

做后端开发久了,你会发现很多系统的核心痛点不是堆CRUD,而是“状态流转”。一个订单从创建到发货,一个审批从提交到归档,背后都是一条流程。项目里一旦出现“if走这里,else走那里,再套一个状态字段”的写法,前期很爽,后期就是灾难。Flowable这个开源工作流引擎,就是用来收拾这种局面的。它把流程定义、流程实例、任务、历史记录都抽象成标准模型,你可以用一套BPMN图描述业务规则,然后在Java代码里驱动它跑起来。这篇东西没有教科书腔,就是一个后端开发在项目里用Flowable从0到1把审批流程跑通后,沉淀下来的全流程经验。适合同样被审批流折磨过的Java开发者,也适合想评估工作流引擎选型的技术负责人。

1. 先弄清楚:Flowable到底解决什么问题

1.1 工作流的本质:状态不是简单的if else

很多人第一次接触工作流,第一反应是“我用状态机也能做”。确实,三五个状态、两三条转移路径,写个状态枚举加个Service判断就够了。但业务一旦复杂起来,状态会爆炸。比如一个报销流程,可能要经历部门经理审批、财务审核、总经理审批、出纳打款,中间还可能出现驳回、转办、撤回、会签。这些状态组合起来不是线性数组,而是一张有向图。如果还靠Status字段加if else,每一处流转都要改业务代码,测试用例几何级增长,产品改一个节点顺序,开发就要改半个项目。

Flowable解决的是这个层次的问题。它把流程建模、流程执行、流程监控三件事拆开。业务人员可以画BPMN流程图,开发人员只需要关注流程节点的执行逻辑,运维人员能看到正在跑的流程实例卡在哪里。流程定义和Java代码解耦,改流程顺序时不用动代码,重新部署一张流程图就行。这一点在真实的项目里价值极大,尤其是审批流这种需求变动频繁的场景。

如果你还理解不了,可以类比成快递系统。状态机像是你手动给每个包裹贴标签,改路线就要重新贴;Flowable则是一套中转站网络,包裹到了哪个站点,该走哪条干线,都由路由表决定。路由表可以随时调整,而包裹本身不需要重写。

1.2 为什么选Flowable而不是自己写状态机

自研工作流引擎最大的坑,不是写不出“流转”本身,而是写不出全套配套能力。从Flowable的功能清单来看,它基本覆盖了一个生产级工作流管理系统需要的全部维度:

  • 流程定义管理:BPMN 2.0标准建模,支持在线设计器,也支持XML文件导入。
  • 流程实例管理:启动、挂起、终止、删除,全程跟踪执行状态。
  • 任务管理:待办、已办、签收、委派、转办、驳回,候选人/候选组。
  • 行为控制:排他网关、并行网关、包容网关、事件网关,条件分支随意编排。
  • 多人协作:会签、或签、票签,多实例节点的完成条件可配置。
  • 监听体系:任务监听、执行监听、流程监听,方便和业务系统深度集成。
  • 历史数据:流程实例历史、任务历史、活动历史、变量历史。
  • 身份管理:用户、用户组,也可以对接自己的用户体系。

自己从零写这些东西,不是写不出来,而是成本太高。状态流转本身是业务规则的一部分,但历史归档、权限控制、流程版本升级、异常恢复、监控统计这些边缘能力,才是真正耗时间的地方。Flowable作为一款开源工作流引擎,已经把这些能力沉淀成了一个成熟的框架,你只需要学会怎么用,而不是重新造轮子。

2. 从零搭建:一个可运行的Flowable工程

2.1 环境准备与依赖引入

Flowable对Java 8的支持非常到位,这也是它在国内很多老项目中能落地的原因。如果你的项目还停留在Java 1.8,不用担心,直接引入Flowable 6.x版本的依赖就能跑。我用的是Spring Boot 2.x加Flowable 6.7.2,兼容性很稳。依赖只需要一个核心包,Flowable会自动把关联模块带进来。

<dependency> <groupId>org.flowable</groupId> <artifactId>flowable-spring-boot-starter</artifactId> <version>6.7.2</version> </dependency>

启动类上加@EnableFlowable不是必须的,starter会自动配置。数据库方面,Flowable支持H2、MySQL、Oracle、PostgreSQL,生产环境我建议用MySQL或PostgreSQL,H2只适合本地演示。引入依赖后,项目启动时会自动创建ACT_开头的表。第一次启动时注意数据库账号尽量有建表权限,否则启动会报错。

这里要提一个新手很容易忽略的点:Flowable默认使用flowable这个数据库Schema下的表,如果你在同一个库里有其他业务表,建议通过配置把表前缀或者表空间区分开。尤其不能把Flowable的表和业务表混在一个迁移工具里管理,否则后面升级引擎版本时会很痛苦。

2.2 Flowable的数据库表结构怎么看

Flowable启动后会自动生成几十张表,刚接触的人看着头大。其实这些表的命名规则已经说明了它们的职责:

表前缀属于哪类数据干什么用
ACT_RE_流程定义与流程模型部署的流程定义、模型,静态数据
ACT_RU_运行时数据流程实例、任务、变量、作业,流程跑动时的动态数据
ACT_HI_历史数据流程实例历史、任务历史、活动历史、变量历史
ACT_ID_身份数据用户、用户组、关系,可以关闭不用
ACT_GE_通用数据字节数组、属性等

开发时最常打交道的表是ACT_RU_TASK,它是当前待办任务表,业务系统做待办查询时基本都要关联它。ACT_HI_PROCINST记录每个流程实例的开始时间、结束时间、结束状态,做统计报表很实用。ACT_RE_PROCDEF是流程定义表,一个流程对应一条记录。

有个经验分享:生产环境下ACT_RU_表必须保持小而快,因为它是实时数据,查询频繁,不能让它无限增长。流程一旦结束,运行时数据就会被清理或归档,不需要手动删。如果你发现ACT_RU_TASK里面数据越来越多,多半是流程没走完,需要排查是否有流程实例卡住了。

2.3 第一个BPMN流程:从XML到部署

Flowable中最核心的建模文件是BPMN 2.0 XML,它描述了一个流程从开始事件到结束事件的完整路径。在Flowable的图形设计器里画图,最终生成的也是这个XML。刚入门时,我建议直接手写一个简单XML,这样能加深对节点、连线、属性之间关系的理解。

下面是一个最简单的“发起请假申请”流程,只有发起、审批、结束三个环节。

<?xml version="1.0" encoding="UTF-8"?> <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:flowable="http://flowable.org/bpmn" targetNamespace="http://flowable.org/bpmn"> <process id="leaveProcess" name="请假审批流程" isExecutable="true"> <startEvent id="startEvent" name="开始"/> <userTask id="applyTask" name="发起请假" flowable:assignee="${applyUser}"/> <userTask id="managerTask" name="经理审批" flowable:assignee="${managerUser}"/> <endEvent id="endEvent" name="结束"/> <sequenceFlow id="flow1" sourceRef="startEvent" targetRef="applyTask"/> <sequenceFlow id="flow2" sourceRef="applyTask" targetRef="managerTask"/> <sequenceFlow id="flow3" sourceRef="managerTask" targetRef="endEvent"/> </process> </definitions>

这里最重要的属性是process id,它是流程定义的Key。启动流程时用leaveProcess这个Key,引擎会根据Key找到最新的流程定义版本,然后创建一个流程实例。flowable:assignee表示这个任务由谁处理,值可以是一个写死的用户名,也可以是一个流程变量。实际项目中基本都是变量,不同人发起流程,审批人不同。

XML写好后,放到src/main/resources/processes目录下。接着用RepositoryService部署它:

@Autowired private RepositoryService repositoryService; public void deployLeaveProcess() { Deployment deployment = repositoryService.createDeployment() .addClasspathResource("processes/leave.bpmn20.xml") .name("请假审批流程") .deploy(); System.out.println("部署ID: " + deployment.getId()); }

部署动作会将BPMN文件解析成流程定义,存入ACT_RE_PROCDEF。这里要提醒一个坑:同一份流程Key重复部署,不会覆盖旧版本,而是生成新版本。流程实例启动时默认使用最新版本。如果没有显式指定版本号,老流程实例会继续按它启动时的旧版本走完,新流程实例则走新版本。这样保证了运行中的流程不会被部署操作打断,但也意味着你不能靠“重新部署”来修复一个已经在跑的错误流程。

2.4 启动流程实例与完成任务

流程定义部署好之后,启动实例就是一行代码的事。启动时可以向流程中传递变量,这些变量会影响后续节点的走向。

@Autowired private RuntimeService runtimeService; @Autowired private TaskService taskService; public void startAndComplete() { Map<String, Object> variables = new HashMap<>(); variables.put("applyUser", "zhangsan"); variables.put("managerUser", "lisi"); ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("leaveProcess", variables); System.out.println("流程实例ID: " + processInstance.getId()); Task task = taskService.createTaskQuery() .processInstanceId(processInstance.getId()) .singleResult(); System.out.println("当前任务: " + task.getName()); taskService.complete(task.getId()); }

启动流程实例时,引擎会从开始事件沿连线往下走,遇到用户任务就停下来,创建一个待办任务。RuntimeService负责推动流程,TaskService负责处理任务。很多新手搞不懂这两个Service的区别,其实一句话就能说清:RuntimeService管流程实例,TaskService管人工任务。

complete(task.getId())表示当前任务完成,引擎会继续沿流程往下走。如果后面还有下一个用户任务,就会生成新任务;如果碰到结束事件,流程实例就正常结束。整个过程中,流程变量会被保存在ACT_RU_VARIABLE表里,等流程结束后转存到历史表。

我还建议你第一次跑通后,立刻去查一下ACT_HI_PROCINST表,看看流程实例的START_TIME_和END_TIME_字段,理解一下历史和运行时数据的区别。这一步想明白了,后面做流程报表和监控会顺手很多。

3. 核心业务场景实现:审批、会签、驳回

3.1 审批流与条件网关

简单线性流程只适合演示,真实业务中一定会遇到分支。比如请假天数小于等于3天,部门经理审批就行;大于3天,还需要总经理审批。这种逻辑在Flowable里通过排他网关实现。排他网关会根据出口连线上配置的条件表达式,选择第一个满足条件的路径继续执行。

在BPMN XML中,网关节点长这样:

<exclusiveGateway id="gateway1" name="请假天数判断"/> <sequenceFlow id="flow1" sourceRef="gateway1" targetRef="managerTask"> <conditionExpression xsi:type="tFormalExpression">${days <= 3}</conditionExpression> </sequenceFlow> <sequenceFlow id="flow2" sourceRef="gateway1" targetRef="bossTask"> <conditionExpression xsi:type="tFormalExpression">${days > 3}</conditionExpression> </sequenceFlow>

对应Java代码里,启动流程时需要传入days变量:

variables.put("days", 5);

条件表达式使用的是UEL(Unified Expression Language),核心就是变量名的比较运算。注意表达式里的变量必须是流程变量,或者通过RuntimeService设置到流程里的数据。如果表达式引用了不存在的变量,引擎会按False处理,流程就会卡在网关处,出现“找不到出口”的错误。

关于网关还有一个常见误区:排他网关虽然叫排他,但如果你写了多个条件同时满足的出口,并且Flowable的默认行为又改为“都走一遍”,就会产生多个分支。为了避免这种问题,我习惯在条件里写互斥条件,比如一个用<= 3,另一个就用> 3,不要出现重叠区间。

3.2 会签、或签、票签的配置

审批流里经常出现“需要多个人共同审批”的场景。比如采购合同要项目经理、财务、法务三个人都通过,才允许通过;或者三个人中只要一个人同意就往下走。这就是会签和或签,Flowable把它们统一抽象成“多实例”节点。

多实例节点的核心是在用户任务上配置multiInstanceLoopCharacteristics,也就是一个循环。它会根据一个集合变量,为集合里的每个元素生成一个子任务。

一个“三人会签”的用户任务配置大致如下:

<userTask id="countersignTask" name="会签审批"> <multiInstanceLoopCharacteristics isSequential="false" flowable:collection="assigneeList" flowable:elementVariable="assignee"> <completionCondition>${nrOfCompletedInstances == nrOfInstances}</completionCondition> </multiInstanceLoopCharacteristics> </userTask>

这里的assigneeList是流程变量,里面是审批人列表。completionCondition里的nrOfCompletedInstances表示已完成实例数量,nrOfInstances表示总实例数量,当两者相等时表示所有人都完成了,流程继续往下走。如果设置成${nrOfCompletedInstances >= 1},那就变成或签,只要一个人完成就放行。

多实例处理起来有个细节要注意:taskService.complete(taskId)时,只会完成当前这一个子任务。全部子任务完成之前,流程不会离开多实例节点。而且如果某个人已经完成了任务,你不能让他重复办理,否则会报“任务不存在”。这里配合业务系统做待办展示时,要按任务ID去重,因为同一个流程实例的会签节点会生成多条待办,分别属于不同处理人。

3.3 驳回、撤回、转办与委派

Flowable本身没有内置一个叫“驳回”的按钮,所以很多初学者会一脸懵。实际上驳回是一种业务语义,通过流程设计实现。最常用的一种做法是在审批节点旁边加一个排他网关,让审批任务有两种出口:一个是“同意”往下走,一个是“驳回”回到发起节点。

具体实现时,通常会在完成任务时传一个approved变量:

Map<String, Object> vars = new HashMap<>(); vars.put("approved", false); vars.put("BACK_TO_START", true); taskService.complete(taskId, vars);

在流程XML中,审批任务对应的网关出口条件就可以写成:

<sequenceFlow sourceRef="gatewayApprove" targetRef="applyTask"> <conditionExpression xsi:type="tFormalExpression">${approved == false}</conditionExpression> </sequenceFlow>

注意,驳回回到发起节点时,发起节点还是一条用户任务,重新生成一条待办。为了避免原审批人再次处理到同一个任务,通常会把approved变量保存到历史变量中,让发起人能看到上次的审批意见。这里没有银弹,只能根据业务去设计。

转办和委派则是Flowable原生支持的。转办是把这个任务完全转交给另一个人,原处理人不再处理;委派是暂时移交给别人处理,处理完还会回到原任务人手里。接口也很简单:

// 转办 taskService.setAssignee(taskId, "newUser"); // 委派 taskService.delegateTask(taskId, "delegateUser");

我实际项目里,转办用得最多。比如经理休假,把他的审批任务转给副经理,这时候用setAssignee最直接。委派更适合需要别人替你提交材料,但最终还得你确认的场景。这两种能力是审批流系统的刚需,Flowable的API覆盖得很完整。

3.4 待办列表与流程状态查询

待办列表是工作流管理系统最基础也最关键的功能。写法上,核心是TaskQuery,可以按办理人、流程定义Key、任务状态、创建时间等条件组合过滤。

List<Task> tasks = taskService.createTaskQuery() .taskCandidateOrAssigned("zhangsan") .processDefinitionKey("leaveProcess") .orderByTaskCreateTime().desc() .list();

这里面有个小坑:taskCandidateOrAssigned匹配的是候选人或者指定办理人的任务,如果任务设置了候选组,这里的处理方式会不一样。更常见的是直接查两个条件,一个是taskAssignee(已认领任务),一个是taskCandidateUser(候选任务)。如果业务上只需要显示“我的待办”,建议用taskAssignee,因为任务被认领后就会带上明确的处理人。

流程状态查询则依赖HistoryService。流程跑完了,RuntimeService查不到数据,需要用历史接口查。比如查用户“zhangsan”参与过的所有已结束流程:

List<HistoricProcessInstance> list = historyService.createHistoricProcessInstanceQuery() .startedBy("zhangsan") .finished() .list();

很多报表功能都是基于历史数据做的。工程上我建议给历史表加索引,尤其是ACT_HI_PROCINST的PROC_DEF_ID_、START_USER_ID_、END_TIME_这几个字段。不加索引的情况下,数据量到几十万条,分页查询就会明显变慢。

4. 项目落地绕不开的坑

4.1 事务、锁与死锁

Flowable与Spring Boot集成后,默认会使用Spring事务管理器。一个流程操作最好包在一个事务里,比如启动流程、设置变量、发送业务通知,应该是一个整体。如果中途出现异常,整个事务回滚,流程实例也不会残留。

但这里有个隐藏的坑:Flowable操作数据库时会锁行,尤其是更新ACT_RU_EXECUTION和ACT_RU_TASK的行。如果多个请求同时针对同一个流程实例做操作,就可能产生死锁。比如用户A在办理任务的同时,管理员在挂起同一个流程实例。我实际遇到过几次MySQL死锁报错,排查后都是并发操作同一个流程实例引起的。

规避方案有三条:一是流程操作接口尽量放到独立的Service方法里,缩短事务时间;二是对同一个流程实例的操作,在业务层做串行化处理,比如加分布式锁;三是Flowable配置里开启asyncExecutorActivate,把耗时操作异步化,降低锁的持有时间。死锁问题不是Flowable独有的,只要是数据库并发写,都会碰到,关键是要理解锁的边界。

4.2 流程部署与版本升级

流程部署版本是工作中容易踩雷的点。很多人以为重新上传一个BPMN就能修复流程,结果发现已经启动的流程实例还是走旧逻辑。这是因为Flowable的版本管理策略是“运行中的实例不受新版本影响”。如果线上的流程还在跑,你重新部署了一个修复版,已经创建的老流程实例会继续走旧版本。只有新发起的流程才会使用新版本。

这就要求你在做流程变更时,先评估是否有正在运行的流程实例。如果存在,要么等它们跑完,要么手动干预,把这些实例迁移到新版本。Flowable本身没有提供一键迁移所有运行实例的功能,只能通过业务流程或脚本维护。我的经验是:流程上线前尽可能做充分测试,因为运行中的实例很难“改道”。

另外,不要在部署时随便改流程定义ID。process id一旦定下来,就是一个稳定的业务标识,换一个ID相当于换了一个新流程。后续的流程统计、权限配置都会对不上。

4.3 性能优化与历史数据清理

Flowable的性能瓶颈主要集中在运行时数据表和高频查询上。流程启动和任务完成本身是轻量操作,但如果业务系统把待办列表当成主查询,每次打开首页都要关联ACT_RU_TASK,数据量一大就有压力。

我常用的优化手段有几个:

  • 给ACT_RU_TASK的ASSIGNEE_字段加索引。
  • 待办列表只查询当前运行流程的数据,绝不查历史表。
  • 用processDefinitionKey做过滤,减少扫描范围。
  • 配置异步执行器,把定时器事件、异步延续放到单独线程池执行。
  • 定期清理历史数据,或者把历史表迁移到归档库。

历史数据清理是生产环境维护的重点。Flowable提供了HistoryService删除历史流程实例的接口,但一次性删除大量数据容易锁表,最好分批次删除。比如每次按时间范围删1000条。如果不想写脚本,也可以直接用MySQL的计划任务,把ACT_HI_表按时间分区,旧分区定时drop。分区表配合Flowable确实没问题,我在一个数据量较大的项目里就是这么做的,效果很明显。

4.4 流程管理系统集成经验

很多项目并不是只接一个Flowable引擎就完事,而是要做一套流程管理系统,包含流程模型管理、表单管理、待办中心、流程监控、流程分析。集成时最容易犯的错误,是把Flowable表直接暴露给前端,前端查询直接操作引擎表。这样做开发快,但后续引擎升级、表结构调整就会崩。

更合理的做法是:在Flowable和业务系统之间加一层封装,所有操作都通过Service接口,返回业务对象。比如待办列表不是直接查ACT_RU_TASK,而是查你自己的一张TaskExt表或Redis缓存,再定时同步。当然,前期可以简单一点,直接用Flowable的API查询,但接口封装一定要做。这样哪怕Flowable底层怎么变,业务系统也不会受影响。

表单集成是另一个难点。Flowable本身不自带业务表单,常见做法是流程节点上关联一个formKey,业务系统根据formKey动态渲染表单。你可以用动态表单引擎,也可以自己写一套表单配置和引擎,只需要在用户任务完成时把表单数据作为流程变量提交即可。表单和流程是两套体系,初期最好分开设计,不要强行耦合。

5. Flowable功能清单与选型建议

5.1 核心功能能力清单

如果你正在做技术选型,可以参考我整理的这个清单。Flowable的主要能力可以分为以下几类:

能力域具体功能应用场景
流程建模BPMN 2.0、流程设计器、模型导入导出业务人员画图、开发人员精调
流程执行串行/并行/条件分支、子流程、事件子流程复杂业务编排
人工任务待办、候选人、候选组、转办、委派审批中心
多实例会签、或签、票签,动态集合多级联合审批
定时与消息定时器事件、消息事件、信号事件超时提醒、系统间触发
监听器任务监听、执行监听、流程监听业务系统集成
历史报表流程实例历史、任务历史、活动历史统计分析、审计
身份管理用户、用户组、关系映射与业务用户体系挂钩

Flowable还有商业版产品Flowable Work,提供了更完善的工作流管理系统界面。但是开源版本做后端引擎已经足够,前端审批界面完全可以用自己现成的低代码平台或者Vue页面去对接。

5.2 轻量级使用与Java 8兼容性

Flowable经常被质疑“太重”,其实它也可以很轻。如果你不需要完整的身份管理模块,可以关闭Flowable自带的用户和组功能,完全依赖业务系统的用户体系。此时只需要配置:

flowable.idm.enabled=false flowable.database-schema-update=true

这样ACT_ID_几张表就不会被使用,部署和运行时会省去一部分维护成本。另外,Flowable的包体积和依赖数量在同类引擎中算是中等,配合Spring Boot使用基本不需要额外的复杂配置。Java 8兼容性问题完全不用担心,国内很多老项目就是用Java 8跑Flowable 6.x,稳定跑了好多年。

还有一点,很多人把Flowable和Activiti搞混。Flowable是从Activiti分支出来的,API风格非常接近,但如果你的团队以前用过Activiti,迁移到Flowable时要特别注意包名变化,比如org.activiti换成org.flowable。换包名看着简单,实际涉及很多import修改。

5.3 什么场景不该用Flowable

技术选型不能只看功能,还得看场景。Flowable并不适合所有项目,我心里有几个反面场景:

  • 如果只是简单的两级审批,没有分支、没有会签、没有驳回,一个表加一个状态字段就够了,引入Flowable反而增加维护成本。
  • 如果团队没有一个人熟悉BPMN,不愿意画流程图,也没有能力维护流程XML,那强行上Flowable只会让项目失控。
  • 如果业务流程极不稳定,每天都在改,建议先用代码和配置顶住,等流程模式相对固定后再上引擎。

另外,Flowable对事务较重的业务处理得不错,但如果你的业务需要分布式事务、跨多个微服务编排,Flowable不一定是首选。它更适合流程相对集中、以人工审批为主、数据量可控的业务系统。

在我现在的项目里,Flowable的定位就是“审批流基础设施”。它不参与具体业务判断,业务判断全部放在Java代码的Delegate里。这样做的好处是,流程无论怎么改,业务逻辑保持不变;业务逻辑调整时,也不需要动流程图。两者边界清晰,团队协作顺畅。这套全流程设计思路,比具体API更值得参考。

最后分享一个小技巧:用Flowable开发时,一定不要跳过流程建模这一步。哪怕是一个很简单的审批,也先在纸上画出节点和连线,再转换成BPMN。多花十分钟建模,能省下后面好几个小时的调试时间。流程引擎不是银弹,但用好了,确实能让“状态流转”这件事变得可控、可追、可优化。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询