Flowable请假流程实战:从BPMN建模到审批流转与踩坑避坑指南
简介这是一份基于Struts2与jBPM的请假审批工作流实例主要面向Java Web初学者和希望掌握工作流落地方法的开发者。压缩包内共42个文件整体仅51KB包含12个XML配置文件、8个JSP页面、3个Java源码与3个class文件以及properties配置、项目元数据和示意图覆盖了struts.xml、jbpm.jpdl.xml等关键配置文件导入Eclipse即可查看完整工程结构。已有252人学习下载。实例完整呈现员工提交、主管审批、老板复核的请假链路通过Action类处理业务请求结合jBPM流程定义与部署并详细演示了Struts2中过滤中文乱码的代码级解决方案同时附带数据库脚本和测试用例便于理解各层协作。对想用工作流改造管理系统的开发者来说这是一个紧凑型参考样例既能快速跑通流程又能作为二次改造的起点。读者还可以借此理清工作流引擎与MVC框架的整合思路掌握从流程设计、Action编写到持久化配置的完整步骤适合作为课程设计或入职预热的实战素材。1. 工作流请假实例看似最简单的流程为什么总在真实项目里翻车几乎每个团队第一次接触工作流引擎都是从工作流请假实例开始的。它看起来足够简单发起、审批、结束三张表单就能说清楚。但真正落地时网关条件、流程变量、会签驳回、多人审批全藏在“审批”两个字后面。我见过不少项目把请假实例跑通后上线不到两周就被各种边界情况缠住审批人看不到待办、请假两天走了八天的审批链、同一人身兼两级职位时出现重复待办。这些不是引擎的玄学而是流程实例的运行机制决定的。这篇文章用一个可复现的 Flowable 请假流程把建模、部署、启动、审批到踩坑的完整链路梳理一遍。你会看到请假三天和请假八天在流程图里并不是一条直线也会看到改完流程定义后运行中的老实例并不会自动升级。适合正在做选型、或者已经接入了工作流引擎但被流程细节反复折磨的开发者。读完你不仅能跑通一个请假实例还能避开最常见的几个翻车点。2. 先把流程画出来Flowable 选型与请假流程的 BPMN 建模2.1 三个引擎怎么选Flowable、Activiti 与 Camunda 的取舍请假流程本身不复杂但选错引擎后面每一步都在还债。国内项目里最常见的三个开源工作流引擎是 Flowable、Activiti 和 Camunda它们都支持 BPMN 2.0 标准流程定义文件可以互相迁移但在集成方式和维护节奏上有明显差异。引擎血缘社区状态适合场景Flowable由 Activiti 5/6 分支演化而来发版稳定Spring Boot 集成资料最多大多数企业系统的默认选择Activiti早期开源流程引擎7.x 转向云原生路线社区版更新放缓存量项目维护为主Camunda核心成员另起炉灶商业产品线完善社区版功能也开放重视流程可视化和运维治理的团队我的习惯是没有特殊诉求就直接用 Flowable。原因不是它比另外两个强多少而是遇到问题能搜到的案例最多团队里随便一个后端都能上手。流程引擎这种基础组件选最主流的那条路本身就是降低风险。Camunda 的可视化确实好但商用版授权成本需要认真评估Activiti 老项目里存量很多新项目没必要赌它的更新节奏。2.2 请假流程的 BPMN 定义请假三天和请假八天为什么走不同的节点先定业务规则再画流程图。这里我给出一个典型的规则组合请假天数小于等于 3 天只需要主管审批大于 3 天需要主管审批加部门经理审批大于 7 天再加一道 HR 备案。流程文件名字叫leave-process.bpmn20.xml放在src/main/resources/processes/下。?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://example.com/leave process idleaveProcess name员工请假流程 isExecutabletrue startEvent idstartEvent name发起请假/ userTask idapplyTask name填写请假申请 flowable:assignee${applyUser}/ userTask idmanagerApprove name主管审批 flowable:assignee${manager}/ exclusiveGateway idgwDept name是否需要部门经理审批/ userTask iddeptApprove name部门经理审批 flowable:assignee${deptManager}/ exclusiveGateway idgwHr name是否需要HR备案/ userTask idhrRecord nameHR备案 flowable:assignee${hrUser}/ endEvent idendEvent name结束/ sequenceFlow idsf1 sourceRefstartEvent targetRefapplyTask/ sequenceFlow idsf2 sourceRefapplyTask targetRefmanagerApprove/ sequenceFlow idsf3 sourceRefmanagerApprove targetRefgwDept/ sequenceFlow idsf4 sourceRefgwDept targetRefendEvent conditionExpression xsi:typetFormalExpression${leaveDays lt; 3}/conditionExpression /sequenceFlow sequenceFlow idsf5 sourceRefgwDept targetRefdeptApprove conditionExpression xsi:typetFormalExpression${leaveDays gt; 3}/conditionExpression /sequenceFlow sequenceFlow idsf6 sourceRefdeptApprove targetRefgwHr/ sequenceFlow idsf7 sourceRefgwHr targetRefendEvent conditionExpression xsi:typetFormalExpression${leaveDays lt; 7}/conditionExpression /sequenceFlow sequenceFlow idsf8 sourceRefgwHr targetRefhrRecord conditionExpression xsi:typetFormalExpression${leaveDays gt; 7}/conditionExpression /sequenceFlow sequenceFlow idsf9 sourceRefhrRecord targetRefendEvent/ /process /definitions这个 BPMN 文件里有几个关键点。process标签的idleaveProcess就是后面启动流程用的 key一旦发布就不能轻易改因为代码里到处引用它。userTask上的flowable:assignee用的是表达式语法比如${manager}意思是这个任务由流程变量manager指定的用户来处理。exclusiveGateway叫排他网关它根据出口条件决定走哪条线注意条件表达式里的小于号和大于号必须写成lt;和gt;否则 XML 解析直接报错。这条链路走下来2 天假期只经历“填写申请 → 主管审批 → 结束”5 天假期会经历“主管审批 → 部门经理审批 → 结束”8 天假期则会在部门经理审批后继续走到 HR 备案。排他网关在这里做的事情就是把同一个流程实例按业务规则分流到不同的审批链上。2.3 最小可跑通的部署Spring Boot 集成与首个部署命令流程定义文件写好了接下来要把它部署到引擎里。Flowable 与 Spring Boot 集成的第一步是引入依赖并配置数据源。引擎启动后会自动创建ACT_前缀的一堆表流程定义、运行中的任务、历史数据都会落到这些表里。SpringBootTest class LeaveProcessDeployTests { Autowired private RepositoryService repositoryService; Test void deployLeaveProcess() { Deployment deployment repositoryService.createDeployment() .addClasspathResource(processes/leave-process.bpmn20.xml) .name(员工请假流程-v1) .enableDuplicateFiltering() .deploy(); // 部署完成后流程定义会落库到 ACT_RE_PROCDEF 表 System.out.println(部署ID: deployment.getId()); } }这段代码把 classpath 下的 BPMN 文件部署进引擎调用一次就生成一条流程定义记录。addClasspathResource的路径必须和文件实际位置一致name是给人看的部署名称建议带上版本号enableDuplicateFiltering()表示相同内容的资源不重复部署但它基于资源内容做判断不是基于流程 key这一点要留意。注意哪怕只改了 BPMN 文件里的一个节点坐标重新部署也会生成新版本的流程定义。旧版本不会消失只是新流程实例默认走最新版本。部署完成后可以在ACT_RE_PROCDEF表里查到流程定义KEY_字段是leaveProcessVERSION_字段是 1。整个部署动作不涉及业务表引擎只认识你给它的流程定义和变量这一点在后面排查问题时会反复用到。3. 发起请假与审批流转从启动流程到完成任务的核心代码3.1 启动流程实例请假表单数据如何放进流程变量流程定义是模板每次请假申请都会创建一个流程实例。启动实例时需要把表单数据放到流程变量里因为这些变量会驱动网关条件也会决定每个审批节点的处理人。// 启动请假流程 MapString, Object variables new HashMap(); variables.put(applyUser, currentUserId); // 当前登录人申请节点处理人 variables.put(leaveDays, 5); // 请假天数必须是 Integer variables.put(leaveReason, 回老家参加婚礼); variables.put(manager, managerId); // 主管用户ID variables.put(deptManager, deptManagerId); // 部门经理用户ID variables.put(hrUser, hrUserId); // HR 用户ID ProcessInstance instance runtimeService.startProcessInstanceByKey( leaveProcess, businessKey, variables );startProcessInstanceByKey的三个参数分别是流程定义 key、业务主键和流程变量。businessKey通常放业务表主键比如请假单 ID这样以后通过runtimeService.createProcessInstanceQuery().processInstanceBusinessKey()就能直接反查流程实例。leaveDays这里强制用Integer是因为网关条件里要做数值比较如果传成字符串条件判断会得到完全相反的结果。审批人变量是启动时从组织架构接口查出来的。这里有一个容易被忽略的点manager、deptManager、hrUser必须和用户表中的用户 ID 完全一致。很多系统的登录用户 ID 是数字主键但流程表里存的是工号两边一旦对不上任务就会变成无人认领的孤儿任务。3.2 查询待办与完成任务审批人视角的接口设计审批人登录系统后看到的待办列表本质上是查引擎的运行任务表。Flowable 提供了TaskQuery按当前用户 ID 过滤出所有分配给这个人的任务。// 查询当前用户的所有待办任务 ListTask tasks taskService.createTaskQuery() .processDefinitionKey(leaveProcess) .taskAssignee(currentUserId) .orderByTaskCreateTime() .desc() .list(); // 组装待办卡片时需要拿到对应的流程变量 for (Task task : tasks) { MapString, Object vars runtimeService.getVariables(task.getProcessInstanceId()); int leaveDays (Integer) vars.get(leaveDays); String reason (String) vars.get(leaveReason); // 把 reason、leaveDays、task.id 返回给前端渲染 }这里要小心一个性能问题getVariables每次都查一次数据库如果待办列表有几十条就会产生几十次查询。数据量小的时候无所谓但到了生产环境建议一次性查出所有流程实例的变量或者干脆在业务表里冗余一份请假表单数据只把processInstanceId留给引擎。审批人点击“同意”时调用的核心方法只有一行MapString, Object completeVars new HashMap(); completeVars.put(approveResult, agree); completeVars.put(approveComment, 同意注意返程安全); taskService.complete(task.getId(), completeVars);complete方法会结束当前任务然后按照 BPMN 的连线规则推动流程继续往下走。completeVars里携带的审批结果会写入流程变量后续节点和网关条件都可以读取。如果审批人点“不同意”把approveResult改成reject即可至于这个变量怎么驱动流程走向取决于流程图里有没有对应的排他网关分支。3.3 流程变量的边界哪些数据该进流程哪些留在业务表流程变量是引擎里的全局状态一旦写入在整个流程实例生命周期内都可以读取。但这不意味着什么数据都往里塞。我见过有人把整个请假表单对象、职级职等 JSON、甚至附件二进制放进去结果历史变量表迅速膨胀查一次审批记录慢到无法接受。我的建议是流程变量只放驱动流程所需的数据也就是流程分支条件、节点处理人、业务主键。具体到这个请假实例核心变量就是leaveDays、applyUser、manager、deptManager、hrUser、approveResult。请假理由、附件、抄送人等业务数据统一存业务表通过businessKey关联。这样引擎表保持干净出问题排查也容易。4. 会签与多级审批真实组织规则在请假流程里的落地4.1 部门会签的多实例建模一个节点让所有人同时审批请假不一定只有一个人审批。有些公司规定超过 10 天的长假需要部门全员会签。如果每个审批人画一个用户任务节点流程图会变得又长又蠢。BPMN 里的多实例multi-instance机制就是为这种场景设计的一个节点同时为集合里的每个元素生成一个子任务。userTask iddeptCountersign name部门会签 flowable:assignee${assignee} multiInstanceLoopCharacteristics isSequentialfalse flowable:collection${assigneeList} flowable:elementVariableassignee completionCondition${nrOfCompletedInstances nrOfInstances}/completionCondition /multiInstanceLoopCharacteristics /userTask这段 XML 的含义是流程实例到达deptCountersign节点时引擎读取流程变量assigneeList它是一个用户 ID 的 List比如[u001, u002, u003]。引擎会为每个用户创建一个独立的任务每个任务的处理人分别是u001、u002、u003。completionCondition里的nrOfCompletedInstances表示已完成的任务数nrOfInstances表示总任务数当前条件“已完成数大于等于总数”就是所有人都批完节点才继续往下走。多实例的参数里isSequentialfalse表示并行会签所有人同时收到待办改成true就是串行会签第一个人批完第二个人才能看到。flowable:collection指向的集合必须在节点创建之前就存在因为它是在进入节点时一次性快照的流程跑了一半再往assigneeList里加人不会对当前实例生效。4.2 多级审批怎么串规则固化在 BPMN 还是放在外部多级审批在请假流程里的典型形态是主管审批完部门经理审批再往上可能是总经理。把审批链直接画在 BPMN 里优点是一目了然、审计方便、每个节点的处理人和规则都固化在流程定义里。缺点也同样明显公司组织架构一调整就要改 BPMN 重新部署。我处理这类问题的习惯是分层稳定的审批链画进 BPMN容易变化的审批链放到业务代码里计算。比如“请 3 天以内主管审批3 到 7 天加部门经理”这种规则相对稳定就画成排他网关而“某些部门需要财务会签”这种临时规则在启动流程前由 service 层动态决定要经过哪些节点通过设置跳过变量来控制网关走向。如果你发现审批链每天变那说明流程本身还没有稳定下来这时候应该先和业务方对齐规则而不是急着改流程定义。流程引擎最怕的不是复杂而是规则一直动每次改动都是一次发布出错的风险会成倍累积。4.3 或签与一票否决不同会签完成条件对应的表达式会签的完成条件不只有“全部通过”这一种。常见的还有两种或签也就是一个人同意就继续一票否决也就是只要有一个人拒绝整个流程就终止或驳回。或签的完成条件改成下面这样completionCondition${nrOfCompletedInstances 1}/completionCondition一票否决需要结合每个子任务的审批结果变量来判断completionCondition${nrOfCompletedInstances nrOfInstances || hasReject}/completionCondition这里的hasReject是流程变量需要在每个子任务完成时检测approveResult并动态更新。实现方式通常是在任务监听器里写逻辑如果审批结果是拒绝就把hasReject置为true。这样其他还在等待审批的人可能正打开审批页流程却已经结束了前端需要做好任务已失效的提示。注意多实例节点的完成条件表达式不能写得太复杂引擎会频繁触发计算。宁可监听器等业务代码多写几行也别在 XML 里写一长串嵌套条件。5. 请假流程避坑指南五个高频翻车点与排查方法流程引擎的报错往往不会直接告诉你“哪行配置错了”而是表现为流程静默卡住、任务凭空消失、分支走错。这一章写五个我在请假实例上踩过或者帮别人排查过的坑每个都按现象、原因、解决的顺序说清楚。5.1 审批人看不到待办先查 ACT_RU_TASK 的 ASSIGNEE_现象流程实例明明启动了申请提交成功但审批人说待办列表是空的流程像卡住了一样。原因最常见的是flowable:assignee表达式里的变量没传或者传的 key 和 XML 里不一致。比如 XML 里写的是${manager}启动流程时变量名却写成了managerId引擎解析表达式失败后任务会进入未分配状态自然不会有任何人的待办里出现它。还有一种情况是用户 ID 体系不一致流程表里的ASSIGNEE_存的是工号而前端传过来的是数据库主键。解决直接查运行任务表。用数据库客户端执行SELECT * FROM ACT_RU_TASK WHERE PROC_INST_ID_ 你的流程实例ID重点看ASSIGNEE_和TASK_DEF_KEY_两个字段。如果ASSIGNEE_是空说明表达式没解析成功再去ACT_RU_VARIABLE表查对应流程实例的变量列表核对 key。这个排查路径能覆盖绝大多数“看不到待办”的情况。5.2 网关“静默走错分支”类型陷阱与缺少 default 流向现象请假 2 天流程却一路走到了 HR 备案节点审批链完全错乱。原因这个坑有两个常见来源。第一leaveDays在启动时传的是字符串2网关条件${leaveDays 3}在做比较时数值和字符串的运算结果不符合预期条件判断返回 false但另一个出口条件偏偏返回 true。第二排他网关的所有出口条件都不满足时如果配了default流引擎就会静默走默认出口不会报错表现出来就是“走错分支”。解决启动流程时把leaveDays强制转成Integer并且在 service 层入口做类型校验。网关条件写完以后用单元测试覆盖边界值1 天、3 天、4 天、7 天、8 天分别验证走到哪个节点。没有 default 流的话条件全 false 时引擎会直接抛异常这样问题反而更容易暴露不至于在生产环境悄悄跑偏。5.3 流程变量放太大一张表被撑爆的经过现象系统上线一个月ACT_HI_VARINST表涨到几百万行查审批历史接口经常超时。原因有人把请假表单的完整对象、附件下载地址列表、甚至前端传来的整段 HTML 都塞进了流程变量。流程变量的设计本意是存储流程运行状态不是业务数据仓库。每完成一个节点Flowable 都会把相关变量写入历史表变量越大、实例越多表膨胀越快。解决流程变量只留业务主键比如businessKey201234表单详情全部在业务表里查。附件上传后拿到存储 URL存业务表不碰流程变量。这个原则在任何工作流项目里都适用别等到表被撑爆了再回头清理历史数据的清理可比防止写入麻烦得多。5.4 同一人身兼两级审批重复待办与跳步问题现象某主管同时也是部门经理他请 5 天假按理说主管审批和部门经理审批都是他自己。结果系统生成了两个待办任务他批完主管节点部门经理节点还在待办列表里流程看起来像卡住了。原因Flowable 对每个 userTask 都是独立创建任务的它不会因为两个节点的 assignee 是同一个用户就把任务合并成一个。这是引擎的正常行为不是 bug但确实不符合很多公司的实际预期。解决在启动流程的 service 层做一次判断如果manager和deptManager是同一个用户 ID就在流程变量里设置skipDepttrue然后在deptApprove节点前面加一个排他网关条件成立时直接跳过部门经理审批。这样既保留了流程图的完整性又避免重复待办。5.5 改了流程定义老实例为什么没跟着变现象上线后根据新制度改了一版流程定义重新部署下去以为所有运行中的请假单都会按新规则走结果老单子的审批链完全没变甚至出现了节点找不到的报错。原因Flowable 的版本机制是新启动的流程实例使用最新版本的流程定义已经在运行中的实例继续使用它启动那一刻的版本。这是刻意的设计保证流程执行的确定性。但有人直接在数据库里删了旧版本流程定义导致运行中的实例引用了不存在的定义流程直接中断。解决发布新流程定义时永远不要删除旧版本让版本号自然叠加。老实例如果没必要迁移就让它按旧规则跑完。如果确实需要迁移去看看引擎提供的流程实例迁移接口但迁移前必须把新旧版本的节点映射关系梳理清楚这个操作远比重新部署一次流程定义要复杂建议单独评估。6. 进阶三招驳回到发起人、转办与超时自动提醒6.1 驳回到发起人重新提交一张网关解决“打回修改”真实请假流程里审批人不只是同意或不同意还经常“打回修改”。比如请假理由写得不充分主管驳回让申请人补充。实现方式最稳妥的是在审批节点后面加一个排他网关approveResult为reject时流程回到applyTask节点发起人重新填写并再次提交流转。流程变量需要保留businessKey这样第二次提交时不会产生新的业务单而是继续走同一个流程实例。这个方案在 BPMN 里就完成了闭环不需要写自由跳转代码。6.2 转办与委派审批人不在时任务怎么处理审批人出差或请假待办任务不能一直压在头上。最简单的转办一行代码taskService.setAssignee(task.getId(), delegateUserId);setAssignee把任务的处理人改掉原审批人不再看到这条待办。如果需要保留审计轨迹建议在转办前把操作记录写到业务表的审批日志里流程引擎本身不会记录这次转办的业务原因。6.3 48 小时未审批自动提醒边界定时事件与 JobExecutor超时提醒是请假流程里最常见的运维需求。可以用 BPMN 边界定时事件实现给主管审批节点挂一个定时器48 小时未完成就触发提醒。关键配置是cancelActivityfalse表示定时器触发后不终止原任务只是额外走一条提醒分支。要注意 Flowable 的定时器依赖 JobExecutor 组件测试环境如果没启动这个组件定时事件永远不会触发。我遇到过多次代码没问题、但提醒一直不发的案例最后都是这个原因。我做第一个请假流程实例时把全部精力都放在画流程图上以为 BPMN 对了就万事大吉结果上线后被版本管理、变量设计、组织架构一致性这些“流程图外”的事情折腾得够呛。后来养成的习惯是先写单测覆盖网关边界再部署到公共环境联调最后才敢发布。流程引擎本身是可靠的绝大多数翻车都来自使用姿势。希望帮到你。本文还有配套的精品资源点击获取