ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SpringBoot整合Flowable:从零实现请假审批工作流实战

2026/10/8 9:55:11 拓冰建站 浏览量
SpringBoot整合Flowable:从零实现请假审批工作流实战 前两天一个做Java后端的朋友跟我吐槽说他们公司OA系统里的请假审批要加一条规则请假超过三天需要部门总监加签开发一评估说要排两天。我听完一点都不意外因为我知道他们的审批逻辑是硬写在Service里的状态字段从0加到了7每个数字代表什么含义全靠一张Excel维护。这种玩法一开始很爽改到第三年就全是债了。我这两年经手的项目做审批、报销、合同会签这类功能统一用的是SpringBoot flowable这套组合从需求确认到流程跑通通常一两天就能完成。Flowable本身是Java生态里非常成熟的开源工作流引擎源自Activiti 5后来分叉独立演进它把审批链路描述成一份BPMN文件引擎负责状态流转、任务分配、历史归档业务代码只需要关心“发起”和“完成”这两件事。这篇文章我不打算讲太多理论就把我实际接入Flowable做请假审批的完整过程、关键代码、以及接入真实业务后躲不开的坑一次性说清楚。适合正在做OA、订单审核、简历筛选这类功能想快速引入工作流能力的后端同学。1. 为什么我把公司审批流从“硬编码状态机”换成了 Flowable先别急着写代码得先想清楚一个问题你现在的审批流到底难在哪里。我自己拆过不少“看似稳定”的自研审批模块表面看跑了好几年实际上早就快撑不住了。1.1 自研状态机的三个隐藏成本很多团队一开始做审批走的是“加字段”路线业务表上放一个leave_status或audit_status0 待提交、1 直属领导审批、2 总监审批、3 人事备案以此类推。再加一个状态流转Service方法里面全是if (status 1 result 2)这种分支判断。第一个隐藏成本是分支规模膨胀。审批链一旦出现会签、加签、驳回、撤回排列组合会迅速爆炸。我见过一张审批表的状态流转代码有四百多行一个状态值是“部门经理审批中”另一个值“部门经理审批不通过待重新提交”中间隔了好多层逻辑新来的同事看半天不敢动。第二个成本是审计缺失。自研状态机往往只记录“最终结果”过程记录靠业务日志表自己拼。一旦有人问“这个单子在谁手上卡了两天谁改了什么意见”你会发现数据根本对不上。第三个成本是流程复用基本为零。公司既要请假审批又要报销审批又要合同盖章审批。自研状态机写一套第二套还要复制改动三套之后维护量翻倍。1.2 Flowable真正帮你解决的问题以及不解决的问题Flowable最大的价值不是“省掉两个状态字段”而是把流程定义和业务代码解耦。流程长什么样画在BPMN文件里节点审批人怎么取写在流程变量里业务代码只发起流程、查询待办、完成审批不再去写状态流转。但有个边界必须搞清楚Flowable不帮你判断业务规则。比如“请假超过三天要总监加签”这个规则它体现在排他网关上条件还是要你自己用表达式写。另外消息通知、超时提醒、表单渲染这些能力Flowable有基础支持但通常不会直接满足你公司的交互要求还是要自己做。也别迷信工作流引擎。如果你们就是单表单、单节点、两个状态的“审批”比如一个“启用/停用”开关那我建议别上Flowable自己写个状态字段更轻。Flowable真正适合的是链路超过三级、存在驳回会签、需要完整历史追溯、以及审批流程经常变化的场景。我把选型依据整理成了一张表方便你按实际情况对号入座对比维度自研状态机Flowable轻量级脚本引擎如Drools流程可视化无靠文档BPMN文件本身可画图无审批链变更改代码测试回归改BPMN文件重新部署改规则脚本会签/多实例自研复杂原生支持并行会签不支持完整历史追踪需自建日志ACT_HI_*系列表完整记录不擅长学习成本低中等偏高中等适合场景状态极少、流程固定长链路、多变、需追溯规则判断密集型2. SpringBoot 工程接入 Flowable依赖、配置与先搞懂的核心概念确定要用Flowable之后先把工程搭起来。这步看起来简单实际上版本匹配是第一道坎。2.1 版本匹配是第一道坎Flowable的版本历史和SpringBoot版本强相关。我当前用的组合是SpringBoot 2.7.x Flowable 6.8.1这是目前最稳妥的组合之一。如果你项目已经上了SpringBoot 3.x那就得选Flowable 7.xAPI上差异不大但依赖坐标和自动配置类有不少改动。pom依赖就引入一个starterdependency groupIdorg.flowable/groupId artifactIdflowable-spring-boot-starter/artifactId version6.8.1/version /dependency这个starter会自动帮你配置好RepositoryService、RuntimeService、TaskService、HistoryService这些核心Bean不需要你再手动new。application.yml这边需要注意几个点spring: datasource: url: jdbc:mysql://localhost:3306/flowable_demo?nullCatalogMeansCurrenttrueuseUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: root password: root flowable: database-schema-update: true async-executor-activate: true process-definition-location-prefix: classpath:/processes/database-schema-update: true表示首次启动时自动创建Flowable所需的ACT_*系列表。注意MySQL 8的连接串里我加了nullCatalogMeansCurrenttrue这个参数不加Flowable在初始化时可能扫描到错误catalog导致表建到别的库去这是我踩过的坑后面排坑章节再细说。2.2 从一条请假审批来看核心对象很多新手看Flowable源码看得一头雾水主要是没搞懂这几个对象的关系。我用一条请假审批来串一下ProcessDefinition流程定义就是那张BPMN“图纸”。请假审批流程定义了一次一个公司所有请假人都用这张图纸。ProcessInstance流程实例某一次具体的请假。张三7月1号发起请假引擎就为这次请假创建一个流程实例。流程定义和流程实例的关系相当于“类”和“对象”。Execution执行实例一条流程实例内部可能有多条执行路径特别是遇到并行网关时。你日常用到的场景里把Execution理解成流程实例内部的执行指针就行。Task任务当前轮到谁处理了。比如“直属领导审批”这个节点被激活后引擎会创建一条Task记录处理人通过TaskService查到它并执行complete操作。用生活化的类比流程定义是施工图纸流程实例是正在盖的这栋楼Task是当前工位上贴着的待办单。2.3 你需要记住的五个ServiceFlowable提供的Service很多但日常开发高频用到的只有这几个Service核心作用常见操作RepositoryService部署流程定义、查询流程定义、暂停/激活createDeployment()、createProcessDefinitionQuery()RuntimeService发起流程实例、操作执行实例startProcessInstanceByKey()、createProcessInstanceQuery()TaskService查待办、完成审批、设置处理人createTaskQuery()、complete()、setAssignee()HistoryService查询历史流程实例、历史活动、历史任务createHistoricActivityInstanceQuery()、createHistoricProcessInstanceQuery()IdentityService用户和组管理引擎内置通常不用newUser()、saveUser()大多数项目里IdentityService基本用不上因为审批人一般直接用你们公司内部员工ID存在流程变量里就行不需要同步到Flowable的用户表。2.4 数据库表别害怕Flowable自动建出来的表看着铺天盖地实际上按前缀分成几类ACT_RE_*Repository流程定义和模型相关。比如ACT_RE_PROCDEF存放流程定义部署新版本会同一条key新增版本号。ACT_RU_*Runtime运行时数据。比如ACT_RU_TASK就是当前待办表ACT_RU_EXECUTION是执行实例表。ACT_HI_*History历史数据。比如ACT_HI_PROCINST是历史流程实例ACT_HI_ACTINST是每一个节点的执行历史ACT_HI_TASKINST是每一条任务的历史。ACT_ID_*Identity用户组表一般不用。ACT_GE_*通用数据比如ACT_GE_BYTEARRAY存BPMN文件二进制内容。你只需要重点理解ACT_RU_TASK和ACT_HI_ACTINST这两张日常业务百分之八十都在跟它们打交道。3. 用 BPMN 画一个请假审批流从建模到部署工程环境准备好之后核心任务就是建模。我建议你用一张最简单的请假审批流跑通全链路发起申请 → 直属领导审批 → 排他网关判断天数 → 三天以上加总监审批 → 人事备案 → 结束。3.1 两种建模方式Flowable支持手写BPMN XML也支持可视化建模。可视化工具有IDEA的Flowable BPMN插件也有flowable-ui应用。我的建议是学习阶段手写XML这样能逼自己搞懂每个节点的含义流程落地之后再用可视化工具维护方便给业务同事看流程图。手写XML最核心的就是process节点它定义一个流程。流程里每个节点有自己的id和nameid是引擎内部用的name是给人看的。3.2 请假审批BPMN核心片段我直接给一份简化但完整的请假审批BPMN文件把关键元素标出来?xml version1.0 encodingUTF-8? definitions xmlnshttp://www.omg.org/spec/BPMN/20100524/MODEL xmlns:flowablehttp://flowable.org/bpmn xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance targetNamespacehttp://flowable.org/bpmn process idleaveApproval name请假审批流程 isExecutabletrue startEvent idstartEvent name发起申请/ userTask idsubmitTask name提交申请 flowable:formKeyleaveForm extensionElements flowable:startFormDataFormKeyleaveForm/flowable:startFormDataFormKey /extensionElements /userTask userTask idleaderTask name直属领导审批 flowable:assignee${leaderId}/ exclusiveGateway iddaysGateway name请假天数判断/ userTask iddirectorTask name总监审批 flowable:assignee${directorId}/ userTask idhrTask name人事备案 flowable:assignee${hrUserId}/ endEvent idendEvent name结束/ sequenceFlow idflow_start_submit sourceRefstartEvent targetRefsubmitTask/ sequenceFlow idflow_submit_leader sourceRefsubmitTask targetRefleaderTask/ sequenceFlow idflow_leader_gateway sourceRefleaderTask targetRefdaysGateway/ sequenceFlow idflow_gateway_hr sourceRefdaysGateway targetRefhrTask conditionExpression xsi:typetFormalExpression ![CDATA[${days 3}]] /conditionExpression /sequenceFlow sequenceFlow idflow_director sourceRefdaysGateway targetRefdirectorTask conditionExpression xsi:typetFormalExpression ![CDATA[${days 3}]] /conditionExpression /sequenceFlow sequenceFlow idflow_director_hr sourceRefdirectorTask targetRefhrTask/ sequenceFlow idflow_hr_end sourceRefhrTask targetRefendEvent/ /process /definitions简单解释几个关键点flowable:assignee${leaderId}表示这个用户任务的处理人从流程变量leaderId里取。所以发起流程时你需要在流程变量里塞入leaderId、directorId、hrUserId这些值。exclusiveGateway是排他网关它的作用是从分支里选一条满足条件的路径走。上面我先判断days 3不满足就走另一个分支。注意${days 3}这个表达式里的days也是流程变量靠它判断。formKey字段并不是Flowable强制的它只是给外部表单一个标识。真实项目中你大概率还是用自建的前端表单这里的formKey可以当成关联业务表的标识来用。3.3 deploy让引擎认识这张图纸BPMN文件放哪、怎么加载有两种常用方式。第一种是手动部署适合动态上传流程的场景Autowired private RepositoryService repositoryService; public void deployProcess() { Deployment deployment repositoryService.createDeployment() .addClasspathResource(processes/leaveApproval.bpmn20.xml) .name(请假审批流程) .deploy(); System.out.println(部署ID: deployment.getId()); }第二种是自动部署。Flowable starter默认会扫描classpath*:/processes/目录下的BPMN文件只要你的文件放在src/main/resources/processes/下应用启动时就会自动部署。不需要写任何部署代码。部署成功后你可以查ACT_RE_PROCDEF表或者写查询接口确认repositoryService.createProcessDefinitionQuery() .processDefinitionKey(leaveApproval) .latestVersion() .singleResult();同一个key再次部署Flowable会自动递增版本号。这在实际项目中非常重要——流程改完部署上去老流程实例继续走老版本新流程用新版本互不干扰。4. 发起流程、查待办、做审批核心代码一次说清部署完了接下来就是业务开发里最常写的几段代码发起流程、查待办、完成审批、查历史。4.1 发起流程流程变量就是上下文先看发起流程的代码。假设前端提交了一个请假表单后端需要把请假人、天数、原因、审批人这些数据传进流程Autowired private RuntimeService runtimeService; public String startLeaveProcess(LeaveApplyRequest request) { // 确定各级审批人 String leaderId userService.findLeaderIdByUserId(request.getUserId()); String directorId userService.findDirectorIdByUserIdAndDept(request.getUserId()); String hrUserId hr001; // 流程变量就是整个流程运行期的上下文 MapString, Object variables new HashMap(); variables.put(applyUserId, request.getUserId()); variables.put(days, request.getDays()); variables.put(reason, request.getReason()); variables.put(leaderId, leaderId); variables.put(directorId, directorId); variables.put(hrUserId, hrUserId); variables.put(approved, false); // businessKey一般是业务单据号方便业务表关联 ProcessInstance processInstance runtimeService.startProcessInstanceByKey( leaveApproval, request.getBizNo(), variables ); return processInstance.getId(); }这里有几个设计点要理解。第一流程变量不是临时数据它跟着整个流程实例存活审批过程中每个节点的表达式都是从变量里取值。第二startProcessInstanceByKey用的是流程定义的key不是id这样部署新版本不影响发起入口。第三businessKey是业务单号建议和业务表主键对应后面查流程实例非常方便。流程发起后第一个用户任务submitTask提交申请会自动生成。很多项目会把审批表单填写和“提交”合在一起做也可以手动调用complete把这个任务处理掉。4.2 待办查询与审批完成查询某个用户当前有哪些待办任务是审批系统的核心操作Autowired private TaskService taskService; public ListTaskVO queryTodoList(String userId) { ListTask tasks taskService.createTaskQuery() .taskAssignee(userId) .orderByTaskCreateTime() .desc() .list(); return tasks.stream().map(task - { ProcessInstance processInstance runtimeService .createProcessInstanceQuery() .processInstanceId(task.getProcessInstanceId()) .singleResult(); TaskVO vo new TaskVO(); vo.setTaskId(task.getId()); vo.setTaskName(task.getName()); vo.setProcessInstanceId(task.getProcessInstanceId()); vo.setApplicationNo(processInstance.getBusinessKey()); return vo; }).collect(Collectors.toList()); }如果你的审批场景是“几个人抢同一张单子”也就是候选组模式可以在BPMN里用flowable:candidateGroups指定候选组然后查待办时用taskCandidateGroup(userId)。区别在于assignee是“已分配给我处理”candidate是“我候选但还没认领”认领用taskService.claim(taskId, userId)。完成审批的代码特别简单public void completeTask(String taskId, String comment, boolean approved) { MapString, Object variables new HashMap(); variables.put(comment, comment); variables.put(approved, approved); taskService.complete(taskId, variables); }看到没有审批动作本质上就是“给流程变量赋值 把当前任务标记完成”。approved、comment这些变量传入后后续节点和网关可以通过它们判断走向。4.3 驳回是怎么实现的驳回可能是新手最懵的地方。Flowable并没有一个专门的“驳回”API但实现思路很清晰画BPMN时在需要驳回的节点上画一条回到上文节点的连线用条件表达式控制。比如上面请假流程里如果直属领导审批不通过我希望流程回到“提交申请”节点让员工修改后再提交。那就需要在leaderTask和submitTask之间加一条sequenceFlow并在leaderTask完成后通过排他网关判断。换句话说“驳回”在BPMN里就是一条特殊的流转路径。不过实际项目里驳回往往需要跳到任意指定节点BPMN画线反而不灵活。Flowable 6.5 提供了ChangeActivityStateBuilder可以动态移动执行到指定节点Autowired private RuntimeService runtimeService; public void rejectToTask(String processInstanceId, String targetActivityId) { runtimeService.createChangeActivityStateBuilder() .processInstanceId(processInstanceId) .moveActivityIdTo(leaderTask, targetActivityId) .changeState(); }这个方法非常有用。审批不通过想退到哪个节点就退到哪个节点不用重新画流程图。但要注意移动节点后记得把相关流程变量重置否则旧的审批意见会影响下一次流转。4.4 用历史服务还原审批全链路审批系统还有一项硬需求给业务方展示“这个单子流转过哪些节点、谁在什么时间批的”。这个用HistoryService查Autowired private HistoryService historyService; public ListHistoricActivityInstance queryProcessHistory(String processInstanceId) { return historyService.createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .orderByHistoricActivityInstanceStartTime() .asc() .list(); }返回结果里就有每个节点activityId、activityName、assignee处理人、startTime、endTime。配合流程变量里存的comment一个完整的审批时间线就出来了。5. 接入真实业务后躲不开的三个硬问题业务关联、驳回会签、组织映射跑通请假审批之后你会面临三个“真实系统才有的问题”。这三点做不好生产环境一定出乱子。5.1 业务表与流程实例关联工作流引擎只管流程你的业务数据请假单明细、金额、附件仍然存在业务表里。两边怎么关联两种主流方案方案A业务表新增一列process_instance_id启动流程后写入返回的流程实例ID。查询时直接用业务单号查流程实例或者用流程实例ID查业务单号。方案B把业务表主键作为businessKey传入流程。需要的时候用runtimeService.createProcessInstanceQuery().processInstanceBusinessKey(bizNo).singleResult()反向查出流程实例。我在真实项目里通常是双管齐下businessKey存业务单号用于引擎侧检索业务表冗余process_instance_id用于快速查历史。冗余字段确实脏一点但换来的是查询代码简单直接不用每次都通过businessKey反查。5.2 驳回、撤回、会签怎么做驳回刚才用ChangeActivityStateBuilder说了撤回本质上也是移动节点只是目标是“上一个节点”。实现时可以在流程实例里维护一个“当前活动”的栈或者直接记录上一步节点ID撤回时移动回去。会签是另一个高频需求。比如合同审批需要多个部门负责人同时同意。Flowable解决方式是“多实例multi-instance”节点。我给出并行会签的BPMN片段userTask idmultiApproveTask name多部门会签 flowable:assignee${assignee} extensionElements flowable:formKeymultiApproveForm/flowable:formKey /extensionElements multiInstanceLoopCharacteristics isSequentialfalse flowable:collection${assigneeList} flowable:elementVariableassignee completionCondition${nrOfCompletedInstances nrOfInstances}/completionCondition /multiInstanceLoopCharacteristics /userTask关键点在flowable:collection${assigneeList}这个流程变量是一个List引擎会为列表里的每个元素生成一条子任务elementVariable指定每个子任务的处理人。isSequentialfalse表示并行审批completionCondition表示全部完成后才往下一步走。如果想实现“过半通过即可”可以调整completionCondition为${nrOfCompletedInstances 2}这种形式。我第一次用会签时犯过错以为assigneeList可以硬编码在XML里结果每次审批人不同必须通过流程变量传入。5.3 组织架构与多租户Flowable内置了IdentityService和用户组概念但大多数公司的组织架构维护在自研的权限系统里。我的建议是不要试图把公司人员全量同步进Flowable的用户表直接用业务员工ID作为assignee和流程变量即可。引擎只认字符串ID不关心你背后是哪个系统。多租户场景稍微复杂一点。如果你们是SaaS多个租户共用一套引擎那就要用tenantId隔离。部署流程时可以指定repositoryService.createDeployment() .addClasspathResource(processes/leaveApproval.bpmn20.xml) .tenantId(tenant_001) .deploy();发起流程时也带上租户上下文。查询待办时引擎会自动按租户过滤。这一块不复杂但要在项目第一天就设计好后面补会非常痛苦。6. 实战排坑自动建表、事务边界、性能与长流程的取舍最后这章全是血泪经验。任何一个坑在测试环境都看不出来上生产必炸。6.1 自动建表与MySQL字符集坑先说nullCatalogMeansCurrenttrue。Flowable初始化时要扫描数据库catalog来创建ACT_*表如果你MySQL连接串没加这个参数它可能扫描出多个catalog导致表建到其他库去或者在已存在的库里报“表已存在”的错。这个参数的含义是告诉JDBC驱动“返回的catalog只用当前连接指定的库”我加上之后问题立刻消失。字符集也要注意。审批意见、驳回理由这种字段必然是中文MySQL建库时务必用utf8mb4连接串上加characterEncodingutf8mb4。别用默认的latin1否则等你上线后写入中文变问号再改字符集就是一场灾难。6.2 事务边界别把HTTP回调放在事务里Flowable默认和Spring事务是打通的。也就是说你在一个Transactional方法里调用taskService.complete()事务提交时流程状态变更一起提交事务回滚时流程状态也一起回滚。这一点非常好但也容易让人踩坑。常见错误是在complete之后马上发MQ、调外部HTTP接口通知审批人。如果外部接口超时或者MQ不可用整个事务会被拖着流程推进变慢严重时数据库连接耗尽。我的做法是事务方法里只改流程和业务数据外部通知通过Spring的事件机制发布出去事务提交后再异步执行或者直接把通知写入本地消息表用定时任务/消息中间件慢慢消费。6.3 性能运行时表一定要“瘦”Flowable性能大头不在引擎本身而在数据表设计。你要记住一个原则*ACT_RU_系列是运行时表流程结束后里面的数据必须清空。如果你发现ACT_RU_TASK表数据量涨到几十万那说明大量流程没有走到正常结束节点是流程设计或业务逻辑有问题。正常结束后由运行库搬进历史库Flowable自动完成。但历史库会无限涨所以需要定期归档。我的做法是用定时任务把ACT_HI_PROCINST按结束时间搬进历史归档表保留近一年热数据。查询历史页时先走Flowable原生HistoryService数据量大了之后建议按业务单号时间范围分页不要list()一把梭。6.4 长流程的模型设计别把所有节点画在一条线上有些人上手之后会画出一条三十个节点的“超级流程”上下游串联看着很完整实际上运维噩梦。但凡中间某个节点返回数据格式变了排查需要翻遍整张流程图。我的经验是超过七八个节点的流程优先拆成子流程。Flowable支持callActivity调用子流程也支持事件子流程、定时边界事件。比如审批通过后需要自动通知多个系统的场景就用异步子流程让主审批流快速结束后续动作慢慢跑。定时提醒用边界定时事件做。比如“财务审批超时2小时自动提醒”可以在财务审批节点上加一个定时边界事件触发后给指定人发提醒同时流程继续等。这种需求用自研状态机实现很痛苦但在BPMN里就是一个节点参数。6.5 我现在的验收清单每次上线新流程我都会拿着下面这个清单过一遍流程定义是否有版本管理能否回滚没有版本管理的部署迟早出事。待办查询是否走索引ACT_RU_TASK按 assignee 查询要建组合索引。历史查询有没有分页生产环境list()查询必炸。驳回和撤回路径是否都测试过特别是驳回后变量重置。是否有监控可以定时扫ACT_RU_TASK表找出超过N天未处理的积压任务并告警。通知是否异步有没有可能把外部依赖拖进流程事务里。说个最让我印象深刻的生产事故某次午夜同事发版本改动了一个流程定义。由于没有指定版本老流程实例全部被新版本“吸走”了正在审批的人发现界面上的流程图变了历史节点显示错乱。从那之后我要求所有流程部署必须打version标签禁止裸部署。工作流引擎这东西用起来不难难的是把边界理解清楚。Flowable替你扛住了状态流转、历史归档、并发控制这些底层脏活但业务规则、通知、权限、数据一致性仍然是你自己的责任。你只需要画好流程图、管理好流程变量、设计好业务关联就能把审批类功能做得又稳又灵活。至少现在朋友再跟我说“审批要加签”我第一反应不会再是“排期两天”而是“改一下BPMN半小时搞定”。