ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flowable动态审批人实战:告别硬编码,实现可配置的审批流

Flowable动态审批人实战:告别硬编码,实现可配置的审批流 在项目里跑过审批流的兄弟应该都遇到过这种场景流程图画好了节点也配好了结果审批人是谁写死在代码里换个人审批就得发一次版。我见过最夸张的一次客户说“下周张总出差审批人改成李总”研发同学直接改代码、重新打包、重启服务前后折腾了一个多小时。这还算好的有的项目里审批人逻辑散落在十几个if-else里每次改完都提心吊胆生怕漏改一个分支。今天聊的就是这个问题用Flowable工作流引擎把审批人配置彻底从硬编码里解放出来。我会结合Spring Boot集成环境把动态审批人的几种主流玩法、底层原理、实操步骤和踩坑记录一次性讲清楚。无论你是刚接触Flowable还是已经在项目里用起来了但审批人写得很死这篇文章都能给你一套可以直接抄作业的方案。先说清楚这篇文章解决什么问题让“谁审批”这件事从代码里剥离出来变成可配置、可动态计算的能力。你要学会的不是背API而是理解Flowable里审批人解析的机制然后按业务需要选一套方案落地。1. 硬编码审批人的痛点和Flowable动态方案的解题思路1.1 写死审批人到底“死”在哪里很多团队第一版做审批流图省事直接把审批人写死。常见写法有以下几种我按“危害程度”从低到高排一下流程定义里写死直接在BPMN XML的flowable:assignee属性里写“zhangsan”流程图跟具体的人绑定在一起。这种做法的直接后果是同一个流程模板不同部门用不了人一变动流程就废了。Java代码里写死在监听器里task.setAssignee(zhangsan)虽然比写死在XML里灵活一点但换人还是要改代码。基于规则写死用if-else判断部门、金额看似智能实际每次加规则都要动代码而且测试回归成本极高。硬编码最坑的不是改代码本身而是审批人和流程定义耦合。流程是业务逻辑审批人是组织数据这俩本质上是两种变化频率完全不同的东西。业务逻辑几个月变一次组织架构可能每周都在动你把它们绑在一起就注定要频繁发版。1.2 Flowable解决这个问题的核心机制Flowable的流程引擎在设计上就考虑了这件事。它允许你在流程定义里不写死具体的审批人而是写一个解析规则等流程跑到那个节点时再通过规则去算出到底是谁来审批。这个机制的核心是DelegateExecution和表达式分发。简单说你可以在BPMN节点上配一个表达式比如${approvalHandler.getApprover(execution)}Flowable在创建任务时会自动调用这个表达式把当前流程实例的上下文传进去由你自己的代码决定返回值是谁。逻辑上就像这样以前你直接写“张三审批”现在是“谁该审批这个单子”由一个专门的处理器类告诉你答案。这个类可以查数据库、调接口、读配置中心想怎么算就怎么算。这里的关键认知是动态审批人不是Flowable的一个开关而是一种建模方式。你要在流程定义、代码结构两处同时做调整才能真正跑起来。1.3 动态化之后业务流程会发生什么变化把审批人动态化之后最直接的好处是流程定义基本不用动了。比如请假审批不管是一线员工还是部门经理走的是同一个流程模板只是跑到“部门负责人审批”节点时系统根据发起人的部门动态算出对应的负责人。第二个好处是响应快。组织调整、人事变动只需要维护好“岗位-人员映射表”或者人员接口流程下一次走到节点时自然拿到新的审批人不需要发版。第三个好处是每个节点的审批规则可以独立维护。部门审批、财务审批、总经理审批各自有各自的解析逻辑互不干扰。改财务规则不会影响部门规则这在代码层面也更好维护。画个重点动态审批人的本质是把“指派给谁”从“流程定义”里挪到“运行时计算”。理解这一点后面所有方案都是在围绕“怎么算、在哪算、算完放哪”做文章。2. 动态审批人的3种主流方案选型前必须先看懂网上讲Flowable动态审批人的文章不少但大多只讲一种做法读者看完还是不知道怎么选。我把实际项目中见过、用过的三种方案放在一起对比每种说清楚原理、适用场景和局限。2.1 方案一流程变量占位运行时用监听器赋值这是最基础的做法。流程定义里不写具体人而是写一个占位变量比如${assignee0}然后通过监听器或者流程发起时的变量设置把assignee0的值动态赋进去。实操上这个方案有两种触发时机流程发起时赋值在runtimeService.startProcessInstanceByKey时通过variables.put(assignee0, zhangsan)传入。节点创建时赋值在节点上挂flowable:taskListener监听create事件在监听器里动态设置task.setAssignee(zhangsan)。这个方案的好处是简单直观适合流程节点固定、审批人相对稳定的场景。比如只有一个审批环节的简单报销发起时就能确定审批人是谁。但缺点也很明显占位变量一多XML里全是${assignee0}、${assignee1}这种毫无语义的东西流程定义的可读性会变得很差。而且如果审批人需要依赖上一个节点的审批结果来决定这个方案就不好使了——因为谁审批这个信息在流程刚发起时往往还确定不了。2.2 方案二表达式调Bean方法运行时动态计算这就是前面提到的核心方案。在BPMN节点的flowable:assignee属性里不写人名而是写一个表达式${approvalHandler.getApprover(execution)}。approvalHandler是你在Spring容器里注册的一个Bean方法入参是DelegateExecution返回值是审批人的用户ID。Flowable在创建任务时会自动解析这个表达式从Spring容器里找到对应的Bean调用方法把返回值设为任务的assignee。这个方案最大的价值审批人的计算逻辑完全从流程定义里剥离出来收拢到一个独立的处理类里。流程定义只关心“这里有一个审批人需要确定”不关心“这个审批人到底是谁”。谁来做经理审批查哪个表是不是要往上找两级这些全在代码里说了算。同时因为方法是Java代码你能做的事情非常多调用其他服务、查数据库、按条件判断、取上级审批人、甚至对接外部通讯录。而且这些逻辑改动不需要动流程图只改代码测试范围可控。这个方案唯一的门槛是你要理解Flowable的表达式解析机制并掌握DelegateExecution能给你提供哪些上下文信息。这块后面会详细展开。2.3 方案三多实例会签场景下的动态审批人列表如果一个节点需要多人审批比如“部门所有副经理会签”、“金额大于10万需要三级审批”就需要用到Flowable的多实例Multi-Instance特性。多实例节点和普通节点的区别在于它会根据一个集合为集合里的每个元素生成一个子任务。在BPMN里你需要配置三个关键属性collection审批人集合表达式比如${approvalHandler.getApprovers(execution)}返回一个CollectionString里面是要参与审批的用户ID列表。elementVariable遍历集合时每个元素存入的变量名比如assigneeItem。完成条件默认为所有实例都完成才算节点完成也可以配成比例比如${nrOfCompletedInstances/nrOfInstances 0.5}实现“过半通过”。这个方案的适用场景很清晰一个节点需要多个人批可能是会签所有人都要批也可能是或签一个人批了就往下走。用多实例建模比把多人拼接成一个字符串再拆开解析要规范得多。我当时踩过的一个坑是在flowable:assignee里写${assigneeItem}结果发现任务根本没指派给任何人。原因是对多实例节点而言assignee要用flowable:assignee直接引用elementVariable声明的变量不能写复杂表达式。这个细节特别容易翻车后面我会专门说。2.4 方案对比怎么选最合适对比维度变量占位 监听器表达式调Bean方法多实例动态集合配置复杂度低中中高灵活性低发起时就得确定高运行时任意计算高适合多人审批代码侵入性中监听器逻辑高需写处理类高需写集合方法适合场景审批人固定、流程简单审批人依赖运行数据会签、或签、多人审批可维护性差变量无语义好逻辑集中好但需理解多实例机制我的建议是新项目直接上方案二把审批人解析统一收敛到独立的Handler类后续不管规则怎么变都只改这一个类。如果遇到多人审批节点在这个Handler里增加返回集合的方法配合多实例特性使用。3. Spring Boot集成Flowable的完整落地步骤方案选完了接下来就是实操。这一部分我会从零开始带你走一遍完整流程项目搭建、核心代码、流程定义文件设计、测试验证。每一步都给出可直接复制的代码和配置。3.1 项目初始化和依赖引入假设你用的是Spring Boot 2.7.x对应的Flowable版本建议用6.7.2或6.8.0这两个版本在Spring Boot 2.x下跑得很稳。如果用的是Spring Boot 3.x那Flowable版本就要升到7.0.0以上了数据库驱动也要对应升级。在pom.xml里引入核心依赖dependency groupIdorg.flowable/groupId artifactIdflowable-spring-boot-starter/artifactId version6.7.2/version /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope /dependency如果你只是学习或者本地快速尝鲜用H2内存数据库最省事启动就有表。生产环境该换MySQL还是换MySQL但开发阶段别让数据库配置拖慢你理解核心概念的速度。配套的application.yml配置如下spring: datasource: url: jdbc:h2:mem:flowable;DB_CLOSE_DELAY-1 driver-class-name: org.h2.Driver username: sa password: h2: console: enabled: true flowable: database-schema-update: true async-executor-activate: falsedatabase-schema-update: true的意思是引擎启动时自动检查并创建或更新表结构。开发环境开着方便生产环境建议改成false用专门的脚本去管理表结构变更不然哪天引擎升级表结构被自动改了你连审计都查不到。async-executor-activate: false也很重要。Flowable默认会启动异步执行器如果你的流程里没有异步延续、定时器等需求建议先关掉省得日志刷屏、线程空转。等真用到异步能力再打开不迟。3.2 核心代码审批人处理器Handler这是整个动态审批方案的心脏。我一般会建一个接口再加一个实现类这样如果未来需要对接不同的审批人来源自己系统的用户表、外部OA系统、企业微信通讯录扩展起来非常方便。先看接口定义public interface ApproverHandler { /** 获取单个审批人 */ String getApprover(DelegateExecution execution); /** 获取多个审批人会签/或签场景 */ ListString getApprovers(DelegateExecution execution); }再看一个具体实现模拟“根据发起人所在部门找部门经理审批”的场景Component(approvalHandler) public class DepartmentManagerApproverHandler implements ApproverHandler { Override public String getApprover(DelegateExecution execution) { String starter execution.getVariable(starter, String.class); if (starter null) { throw new FlowableException(流程发起人不能为空); } String department getDepartmentByUserId(starter); String managerId getManagerByDepartment(department); if (managerId null) { // 这里可以配置兜底方案比如默认给到某个固定角色 managerId admin; } return managerId; } Override public ListString getApprovers(DelegateExecution execution) { String starter execution.getVariable(starter, String.class); String department getDepartmentByUserId(starter); return listManagersByDepartment(department); } private String getDepartmentByUserId(String userId) { // 模拟查用户表或调用组织架构接口 return switch (userId) { case user001 - 研发部; case user002 - 财务部; default - 综合部; }; } private String getManagerByDepartment(String department) { // 模拟查组织架构表实际项目中这里是Mapper查询 return switch (department) { case 研发部 - manager_dev; case 财务部 - manager_fin; default - manager_admin; }; } private ListString listManagersByDepartment(String department) { return List.of(manager_a, manager_b); } }这里有几个细节需要特别强调Bean名称必须和表达式里的一致。我在这里用的Component(approvalHandler)BPMN表达式里写的是${approvalHandler.getApprover(execution)}。如果Bean名称对不上Flowable在运行时解析表达式会直接报错提示找不到这个Bean。方法签名一定要是DelegateExecution。有的同学会图省事写成不带参数的getApprover()这在Flowable表达式里也能调用但你拿不到流程上下文了。没有execution你怎么获取流程变量、怎么拿发起人、怎么读上一个节点的审批结果所以老老实实把DelegateExecution传进去。异常处理要慎重。如果审批人解析不出来我是直接抛FlowableException让流程报错。这样做的目的是让问题尽早暴露而不是让任务落入一个没有审批人的黑洞里。你有兜底逻辑没问题但兜底也要能追踪到——至少打一条error日志。3.3 BPMN流程定义文件设计流程定义文件是落地的关键。我用一个请假审批流程做示例员工发起申请先走部门经理审批然后走人事确认。BPMN文件的完整内容如下?xml version1.0 encodingUTF-8? definitions xmlnshttp://www.omg.org/spec/BPMN/20100524/MODEL xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlns:flowablehttp://flowable.org/bpmn targetNamespacehttp://flowable.org/bpmn process idleaveProcess name请假审批流程 isExecutabletrue startEvent idstartEvent name开始 flowable:initiatorstarter extensionElements flowable:executionListener eventstart expression${execution.setVariable(starter, starter)} / /extensionElements /startEvent sequenceFlow idflow1 sourceRefstartEvent targetRefmanagerApprove / userTask idmanagerApprove name部门经理审批 flowable:assignee${approvalHandler.getApprover(execution)} /userTask sequenceFlow idflow2 sourceRefmanagerApprove targetRefhrConfirm / userTask idhrConfirm name人事确认 flowable:assignee${approvalHandler.getHRApprover(execution)} /userTask sequenceFlow idflow3 sourceRefhrConfirm targetRefendEvent / endEvent idendEvent name结束 / /process /definitions注意startEvent上我挂了一个executionListener在流程启动时把starter发起人ID存入流程变量。flowable:initiatorstarter是Flowable的一个内置特性它会自动把发起人ID设置到starter变量里。但为了让starter变量在后续节点可见最好在启动事件里再显式set一次。在managerApprove节点上flowable:assignee直接写表达式${approvalHandler.getApprover(execution)}。流程跑到这个节点时Flowable会自己解析这个表达式把算出来的审批人设为任务的assignee。部署流程定义的方式很简单Autowired private RepositoryService repositoryService; public void deployProcess() { repositoryService.createDeployment() .addClasspathResource(processes/leave-process.bpmn20.xml) .name(请假审批流程) .deploy(); }3.4 启动流程并验证动态审批人流程定义部署好之后启动流程实例测试一下动态审批人是否生效Autowired private RuntimeService runtimeService; Autowired private TaskService taskService; public void testStartProcess() { MapString, Object variables new HashMap(); variables.put(starter, user001); ProcessInstance processInstance runtimeService .startProcessInstanceByKey(leaveProcess, variables); Task task taskService.createTaskQuery() .processInstanceId(processInstance.getId()) .singleResult(); System.out.println(当前审批人: task.getAssignee()); }如果一切正常控制台会输出当前审批人: manager_dev。因为user001属于研发部调用Handler时返回了研发部经理的ID。这个方案的精妙之处在于流程定义自始至终没有出现过真实人名。今天研发部经理是manager_dev下个月换成manager_dev_2都不用动流程图只要后台数据变了流程实例跑到这个节点时就拿得到新审批人。3.5 多实例会签场景的完整配置如果部门经理审批之后还要“研发部两位副经理会签”BPMN节点要改成这样userTask iddeptManagerApprove name研发部副经理会签 flowable:assignee${approverItem} multiInstanceLoopCharacteristics isSequentialfalse flowable:collection${approvalHandler.getApprovers(execution)} flowable:elementVariableapproverItem completionCondition${nrOfCompletedInstances/nrOfInstances 0.6}/completionCondition /multiInstanceLoopCharacteristics /userTask这里几个配置的作用isSequentialfalse并行会签所有副经理同时收到审批任务。如果要串行依次审批改成true。flowable:collection审批人集合的表达式返回一个ListString。flowable:elementVariable集合遍历时每个元素存入的变量名。它一定要和flowable:assignee里引用的名字一致比如这里都是approverItem。completionCondition完成条件。这里写的是已完成实例数/总实例数 0.6也就是3个副经理里至少2个同意就往下走。这里有个非常容易踩的坑collection表达式返回的集合如果你写的是ListUser而元素变量approverItem拿到的是User对象那flowable:assignee${approverItem}会直接报类型错误。所以集合的类型保持一致很重要最简单的做法就是返回用户ID字符串列表。4. 审批人无法确定时的兜底策略与设计经验前面讲的都是“能算出审批人”的情况但实际业务里总有算不出来的时候——部门经理岗位空缺、组织数据还没同步、或者流程发起人信息不完整。这些边缘情况如果不在设计时考虑进去上线后就是一个一个的生产事故。4.1 审批人兜底的几种常见方案我在项目里用过的兜底方案按优先级排大概是这么几种指定默认处理人。在Handler里配置一个兜底用户ID比如admin。任何解析不出来的情况最终都落到这个人身上。这个方案最保守但如果默认处理人没有及时关注任务审批就会卡住。按角色查询空闲用户。查某个角色下有哪些人按最近任务量排序把任务派给最“闲”的那个人。这种做法适合客服工单、城市经理审批这类负载均衡场景但它依赖角色用户数据的准确性。发送到角色而不是个人。这个方案和Flowable的flowable:candidateGroups结合非常紧密不在assignee里写具体人而是写候选组让组里的人自己认领。比起指定具体的人这个方案把选择题从“系统替人决定”变成了“人自己决定”灵活性最高。以请假审批为例如果算不出部门经理是谁就退化为任务候选组设置为“部门经理组”任何一个有该角色的人都可以认领。这是目前我见过最稳的兜底方式。4.2 用candidateGroups实现角色化审批BPMN节点配置如下userTask idmanagerApprove name部门经理审批 flowable:candidateGroups${approvalHandler.getApproverRoles(execution)} /userTaskHandler中增加方法public ListString getApproverRoles(DelegateExecution execution) { String starter execution.getVariable(starter, String.class); String department getDepartmentByUserId(starter); // 根据部门返回对应的角色编码 return List.of(ROLE_DEPT_MANAGER_ department); }配置好之后所有拥有ROLE_DEPT_MANAGER_研发部角色的用户都能在待办列表里看到这个任务谁先认领谁处理。用candidateGroups有两个明显的好处一是组织架构变动时只要角色用户的映射关系不变流程完全不需要调整二是审批人可以自己决定要不要处理遇到请假、出差等情况其他有角色的人可以顶上。代价就是查待办列表时如果你的查询条件不带candidateGroups这些任务不会出现在普通待办里。所以前端要么把候选组任务和指定任务分开两个列表展示要么在查询时同时带上assignee和candidateGroups两个条件。4.3 审批人自动跳过与多级审批的建模思路还有一个高频场景流程发起人本身就是要走审批的人比如员工请假3天以内只需要部门经理审批但这位员工自己就是部门经理那他需要自己审批自己吗业务上显然不合理。处理方式是在Handler里做判断如果计算出的审批人和发起人相同就自动跳过这个节点。实现上有两种思路一种是在Handler层处理把审批人替换成上一级审批人。一种是在流程设计层面做成条件分支审批人等于发起人时走跳过分支。前者适合审批层级固定、只跳过一级的场景后者适合审批层级多、要支持连续跳过的场景。以“连续跳过”为例BPMN大致是这样userTask idmanagerApprove name经理审批 flowable:assignee${approvalHandler.getNextApprover(execution)} extensionElements flowable:executionListener eventend expression${execution.setVariable(lastApprover, task.assignee)} / /extensionElements /userTask然后在Handler里判断如果算出来的审批人确实等于发起人就继续往上查直到找到不是发起人的人才返回。这种建模方式能把“谁能审”变成一个纯计算函数流程定义完全不用关心“跳过谁”。4.4 审批层级不固定时的动态模板思路有些业务流程更复杂审批层级是不固定的。同样的报销单普通员工要走三级审批总监自己提交只走一级。如果给每个层级都建一个流程模板那模板数量会膨胀到没法维护。这种场景我用的方案是流程定义里只放一个“动态审批”节点节点上配置一个循环或者一组并行子任务子任务的数量和审批层级由Handler计算得出。具体来说就是在Handler里根据发起人的职级、报销金额等参数算出一个审批链一级审批人是谁、二级是谁、三级是谁。然后用runtimeService.createChangeActivityStateBuilder去动态地驱动流程走下这些节点或者用子流程的方式把审批链嵌套进去。这种做法的门类比较深属于Flowable高级玩法了不建议刚上手就搞。先用表达式加Handler解决90%的问题剩下的10%等你把基础玩熟了再优化。5. 常见问题与排查技巧实录动态审批人如果配置不对流程跑起来会报各种错。我把实际项目里遇到的高频问题整理成速查表每一项都是真金白银踩过的坑。5.1 问题速查表问题现象根本原因解决方法任务没有assignee待办查不到表达式没生效或Handler返回null检查BPMN里assignee的表达式拼写检查Handler是否返回了空值报错无法解析表达式Bean名称不匹配或方法不存在确认Component注解的value和表达式里的Bean名一致报错Unknown property used in expression元素变量名拼写不一致检查elementVariable和assignee引用的变量名是否完全相同多实例节点只生成一个任务collection返回的不是集合类型确认Handler方法返回ListString而不是单个String流程启动报错Table not found数据库表结构没初始化确认database-schema-update: true配置生效部署流程定义时校验失败BPMN文件有语法问题用Flowable官方Modeler校验XML或者查看日志中的具体行号5.2 实战排查任务创建后审批人空白这是最常见的状况。用测试代码启动流程查task时发现task.getAssignee()返回null任务在待办列表里怎么都查不到。排查步骤我一般是这样走的第一步确认流程真的走到了对应节点ProcessInstance pi runtimeService.createProcessInstanceQuery() .processInstanceId(processInstanceId) .singleResult(); System.out.println(pi.getActivityId());如果打印的不是managerApprove说明流程压根还没到这一步优先查上一个节点有没有正常完成。第二步确认节点上的表达式配置。打开BPMN源码看userTask标签上有没有flowable:assignee属性属性值是不是${approvalHandler.getApprover(execution)}。第三步确认Spring容器里有这个Beancurl http://localhost:8080/actuator/beans | grep approvalHandler如果查不到这个Bean多半是Component注解没扫到或者类不在Spring Boot启动类所在包及其子包下。第四步确认Handler没有静默吞异常。有的同学喜欢在Handler里catch异常然后返回null这会让Flowable以为审批人就是null任务自然就没有assignee。我的建议是解析逻辑里不要大面积try-catch让异常直接抛出来流程报错好过任务静默丢失。5.3 实战排查流程变量取不到值execution.getVariable(starter)返回null也是高频问题。大多数情况下是因为流程启动时变量放的位置不对。Flowable的流程变量有两种执行实例变量execution variables和任务本地变量task local variables。查execution.getVariable()时它会从当前执行实例向上逐级找父级变量但如果变量是任务本地变量是查不到的。常见的错误写法是taskService.setVariableLocal(taskId, starter, user001);用setVariableLocal设置变量这个变量只挂在当前任务上后续节点通过execution.getVariable()是拿不到的。正确做法是用runtimeService.setVariable(processInstanceId, starter, user001)或者干脆在启动流程时通过variables参数传入。另外还有一种情况flowable:initiator虽然能自动设置发起人变量但它要求调用startProcessInstance时传入Authentication信息identityService.setAuthenticatedUserId(user001); runtimeService.startProcessInstanceByKey(leaveProcess, variables);如果没调用setAuthenticatedUserIdstarter变量就是空的。这一点文档里写得很隐晦很容易漏。5.4 候选组任务的待办查询问题改成candidateGroups模式后前端待办列表突然查不到数据了这也非常常见。原因很简单查询任务时条件没带对。指定审批人模式下查询条件是taskAssigneetaskService.createTaskQuery() .taskAssignee(user001) .list();候选组模式下需要按组查询taskService.createTaskQuery() .taskCandidateUser(user001) .list();taskCandidateUser会查出所有这个用户可以直接认领的任务福泽用户所属的所有组。如果两种类型的任务都要展示就需要按or条件查询taskService.createTaskQuery() .or() .taskAssignee(user001) .taskCandidateUser(user001) .endOr() .list();这里有个性能提醒or条件在任务量大时可能会拖慢查询速度如果并发不高可以忽略但如果待办任务量上了十万建议还是分两个查询然后在内存里合并。5.5 关于Flowable版本差异的一个重要提醒不同版本的Flowable在表达式解析、变量传递上是有细微差异的。我自己用过5.x、6.x和7.x最大的感受是6.x和5.x在API上有一些breaking change7.x又对Jakarta命名空间有了要求。如果你用的是Flowable 7.x配合Spring Boot 3.x一定要确认BPMN的XML命名空间还是不是http://flowable.org/bpmn以及javax开头的包是否都换成了jakarta。如果混用了启动时多半会报ClassNotFoundException排查起来非常费劲。所以建议新项目尽量用同一套主版本别混着来。Spring Boot 2.x用Flowable 6.7.2Spring Boot 3.x用Flowable 7.0.0。这个匹配关系在官方文档里有专门说明部署前一定先确认。6. 从“能跑”到“好用”审批人模块的生产级设计思路如果读到这你已经能把动态审批人跑起来了那值得再想深一层如何在生产环境把审批人模块做得更健壮、更好扩展。这个章节是我自己在多个项目中沉淀下来的设计思路不是教科书是经验谈。6.1 审批人解析逻辑的抽象与仓储化早期项目我把所有审批人规则都放在一个Handler里一开始还好后面规则越来越多一个类上千行改一处恨不得重新读一遍全文。后来我做了分层重构规则路由层入口Handler根据流程定义Key或节点ID路由到不同的规则处理器。具体规则层每个业务场景一个处理器比如ProjectApproveRule、PurchaseApproveRule实现统一的接口。数据访问层处理器不直接写SQL而是通过Mapper接口查询组织架构、岗位数据。重构之后加一个新场景的审批逻辑只需要新增一个规则类不动其他代码。而且规则之间天然隔离测试也方便每个规则类都可以单独写单元测试。这个分层思路不是Flowable特有的但在审批人场景下收益特别明显因为审批人规则的变更频率实在太高了没有隔离后面就是无休止的互相影响。6.2 审批人解析的缓存设计审批人解析往往要查组织架构数据如果在高并发下每次流程流转都实时查数据库压力还是不小的。尤其是那种“一级二级三级连续审批”的场景一个流程就要解析多次。我的做法是对组织架构这类变化频率低、实时性要求不高的数据加一层本地缓存。解析审批人时先查缓存缓存没有再查数据库并设置合理的过期时间。配置一个简单的CacheManagerConfiguration public class CacheConfig { Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager new CaffeineCacheManager(approverCache); cacheManager.setCaffeine(Caffeine.newBuilder() .maximumSize(500) .expireAfterWrite(Duration.ofMinutes(30))); return cacheManager; } }然后在Handler里使用Component(approvalHandler) public class DepartmentManagerApproverHandler implements ApproverHandler { Autowired private CacheManager cacheManager; Override public String getApprover(DelegateExecution execution) { String starter execution.getVariable(starter, String.class); Cache cache cacheManager.getCache(approverCache); String approverId cache.get(starter, this::loadApproverFromDb); return approverId; } private String loadApproverFromDb(String starter) { // 实际查询组织架构返回审批人ID return manager_dev; } }这里要注意审批人的缓存过期时间不要设置太长。组织架构如果确实发生变动了你希望的是半小时内生效而不是一个小时后。设置30分钟是一个相对稳妥的折中值既减少了数据库压力又不会让人员变动的影响滞后太久。6.3 审批链路可观测性动态审批人的另一个痛点是排障困难。以前写死审批人的时候任务审批人一目了然查数据库就知道谁审的。动态化了以后你看到的只是“任务到了经理节点”至于为什么解析出了张经理而不是李经理不看代码根本不知道。所以我强烈建议在审批人Handler里加审计日志。至少记录以下信息流程实例ID和流程定义Key当前节点ID入参发起人、业务表单关键字段审批人解析结果如果走了兜底逻辑也要记录兜底原因打日志用SLF4J就行关键是要记得打。我第一次做动态审批人时没打日志上线第三天用户反馈“某个单子审批人不对”我对着数据库查了一个下午最后实在没辙了才在Handler里补日志定位问题。从那以后所有审批人Handler一律先加日志再写逻辑。6.4 表单数据参与审批人决策的实现方式还有一种更复杂的场景审批人不仅和发起人有关还和表单内容有关。比如“报销金额大于5000需要财务总监审批小于5000财务经理审批就行”。这种场景表单数据必须存入流程变量Handler才能拿到。启动流程时把表单数据整个放入variablesMapString, Object variables new HashMap(); variables.put(starter, user001); variables.put(businessForm, formData); runtimeService.startProcessInstanceByKey(expenseProcess, variables);Handler里解析Override public String getApprover(DelegateExecution execution) { MapString, Object form (MapString, Object) execution.getVariable(businessForm); BigDecimal amount new BigDecimal(String.valueOf(form.get(amount))); if (amount.compareTo(new BigDecimal(5000)) 0) { return finance_director; } return finance_manager; }生产环境的表单数据一般比较复杂建议用一个专门的DTO对象封装而不是裸的Map。存入流程变量的对象需要实现Serializable接口否则Flowable在持久化变量时会报错。7. 写在最后动态审批人这件事代码量不大难的是思路转变。我见过太多团队一开始图省事写死审批人后来业务跑起来了每次调组织架构都要发版越往后越痛。其实Flowable这套表达式机制从第一天就摆在那只是很多人没注意到它的价值。从我个人的实践来看一个健壮的审批人模块核心不是怎么用Flowable的API而是怎么把“谁审批”这个业务问题抽象成一个可扩展的计算逻辑。流程定义只负责“流转”审批人解析全部收敛到一个内聚的模块里。做到了这一点不管未来组织架构怎么变流程审批人怎么调研发这边都能做到不慌不忙。最后再分享一个小技巧动态审批人方案上线后一定要做一次全流程回归测试不要只测“正常情况”。把“审批人解析失败”、“审批人等于发起人”、“审批人角色没有配置用户”这些异常场景都跑一遍确认兜底逻辑生效你才能在半夜接到业务电话的时候睡个安稳觉。
返回列表