做后端开发久了你会发现很多系统的核心痛点不是堆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 groupIdorg.flowable/groupId artifactIdflowable-spring-boot-starter/artifactId version6.7.2/version /dependency启动类上加EnableFlowable不是必须的starter会自动配置。数据库方面Flowable支持H2、MySQL、Oracle、PostgreSQL生产环境我建议用MySQL或PostgreSQLH2只适合本地演示。引入依赖后项目启动时会自动创建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 version1.0 encodingUTF-8? definitions xmlnshttp://www.omg.org/spec/BPMN/20100524/MODEL xmlns:flowablehttp://flowable.org/bpmn targetNamespacehttp://flowable.org/bpmn process idleaveProcess name请假审批流程 isExecutabletrue startEvent idstartEvent name开始/ userTask idapplyTask name发起请假 flowable:assignee${applyUser}/ userTask idmanagerTask name经理审批 flowable:assignee${managerUser}/ endEvent idendEvent name结束/ sequenceFlow idflow1 sourceRefstartEvent targetRefapplyTask/ sequenceFlow idflow2 sourceRefapplyTask targetRefmanagerTask/ sequenceFlow idflow3 sourceRefmanagerTask targetRefendEvent/ /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() { MapString, 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 idgateway1 name请假天数判断/ sequenceFlow idflow1 sourceRefgateway1 targetRefmanagerTask conditionExpression xsi:typetFormalExpression${days 3}/conditionExpression /sequenceFlow sequenceFlow idflow2 sourceRefgateway1 targetRefbossTask conditionExpression xsi:typetFormalExpression${days 3}/conditionExpression /sequenceFlow对应Java代码里启动流程时需要传入days变量variables.put(days, 5);条件表达式使用的是UELUnified Expression Language核心就是变量名的比较运算。注意表达式里的变量必须是流程变量或者通过RuntimeService设置到流程里的数据。如果表达式引用了不存在的变量引擎会按False处理流程就会卡在网关处出现“找不到出口”的错误。关于网关还有一个常见误区排他网关虽然叫排他但如果你写了多个条件同时满足的出口并且Flowable的默认行为又改为“都走一遍”就会产生多个分支。为了避免这种问题我习惯在条件里写互斥条件比如一个用 3另一个就用 3不要出现重叠区间。3.2 会签、或签、票签的配置审批流里经常出现“需要多个人共同审批”的场景。比如采购合同要项目经理、财务、法务三个人都通过才允许通过或者三个人中只要一个人同意就往下走。这就是会签和或签Flowable把它们统一抽象成“多实例”节点。多实例节点的核心是在用户任务上配置multiInstanceLoopCharacteristics也就是一个循环。它会根据一个集合变量为集合里的每个元素生成一个子任务。一个“三人会签”的用户任务配置大致如下userTask idcountersignTask name会签审批 multiInstanceLoopCharacteristics isSequentialfalse flowable:collectionassigneeList flowable:elementVariableassignee completionCondition${nrOfCompletedInstances nrOfInstances}/completionCondition /multiInstanceLoopCharacteristics /userTask这里的assigneeList是流程变量里面是审批人列表。completionCondition里的nrOfCompletedInstances表示已完成实例数量nrOfInstances表示总实例数量当两者相等时表示所有人都完成了流程继续往下走。如果设置成${nrOfCompletedInstances 1}那就变成或签只要一个人完成就放行。多实例处理起来有个细节要注意taskService.complete(taskId)时只会完成当前这一个子任务。全部子任务完成之前流程不会离开多实例节点。而且如果某个人已经完成了任务你不能让他重复办理否则会报“任务不存在”。这里配合业务系统做待办展示时要按任务ID去重因为同一个流程实例的会签节点会生成多条待办分别属于不同处理人。3.3 驳回、撤回、转办与委派Flowable本身没有内置一个叫“驳回”的按钮所以很多初学者会一脸懵。实际上驳回是一种业务语义通过流程设计实现。最常用的一种做法是在审批节点旁边加一个排他网关让审批任务有两种出口一个是“同意”往下走一个是“驳回”回到发起节点。具体实现时通常会在完成任务时传一个approved变量MapString, Object vars new HashMap(); vars.put(approved, false); vars.put(BACK_TO_START, true); taskService.complete(taskId, vars);在流程XML中审批任务对应的网关出口条件就可以写成sequenceFlow sourceRefgatewayApprove targetRefapplyTask conditionExpression xsi:typetFormalExpression${approved false}/conditionExpression /sequenceFlow注意驳回回到发起节点时发起节点还是一条用户任务重新生成一条待办。为了避免原审批人再次处理到同一个任务通常会把approved变量保存到历史变量中让发起人能看到上次的审批意见。这里没有银弹只能根据业务去设计。转办和委派则是Flowable原生支持的。转办是把这个任务完全转交给另一个人原处理人不再处理委派是暂时移交给别人处理处理完还会回到原任务人手里。接口也很简单// 转办 taskService.setAssignee(taskId, newUser); // 委派 taskService.delegateTask(taskId, delegateUser);我实际项目里转办用得最多。比如经理休假把他的审批任务转给副经理这时候用setAssignee最直接。委派更适合需要别人替你提交材料但最终还得你确认的场景。这两种能力是审批流系统的刚需Flowable的API覆盖得很完整。3.4 待办列表与流程状态查询待办列表是工作流管理系统最基础也最关键的功能。写法上核心是TaskQuery可以按办理人、流程定义Key、任务状态、创建时间等条件组合过滤。ListTask tasks taskService.createTaskQuery() .taskCandidateOrAssigned(zhangsan) .processDefinitionKey(leaveProcess) .orderByTaskCreateTime().desc() .list();这里面有个小坑taskCandidateOrAssigned匹配的是候选人或者指定办理人的任务如果任务设置了候选组这里的处理方式会不一样。更常见的是直接查两个条件一个是taskAssignee已认领任务一个是taskCandidateUser候选任务。如果业务上只需要显示“我的待办”建议用taskAssignee因为任务被认领后就会带上明确的处理人。流程状态查询则依赖HistoryService。流程跑完了RuntimeService查不到数据需要用历史接口查。比如查用户“zhangsan”参与过的所有已结束流程ListHistoricProcessInstance 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.enabledfalse flowable.database-schema-updatetrue这样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。多花十分钟建模能省下后面好几个小时的调试时间。流程引擎不是银弹但用好了确实能让“状态流转”这件事变得可控、可追、可优化。