Flowable请假流程实战:BPMN建模到Spring Boot集成

发布时间:2026/9/7 6:09:41
Flowable请假流程实战:BPMN建模到Spring Boot集成 简介工作流请假实例是一份面向工作流初学者与Java Web开发者的实战案例以员工请假申请与审批为业务场景展示Struts2框架下从任务提交、主管审批到人力归档的多级流程并重点解决中文编码、拦截器权限检查等常见问题。压缩包共42个文件主要包含Java源码、JSP视图页面、struts.xml与jbpm相关配置文件、properties设置、class文件以及数据库SQL脚本与测试用例整体仅51KB便于导入开发环境快速运行和改动。目前已有250人学习下载适合作为工作流入门练习、毕业设计参考或课堂演示项目。通过完整代码可掌握Action类与拦截器使用、理解Service/DAO分层与数据库脚本设计还能解决Struts2中文乱码问题并借助其JSP页面与流程定义文件搭建自己的请假审批模块。这个过程能帮助初学者建立从请求处理到业务持久化的完整工作流开发思维积累项目排错经验。1. 为什么请假能成为所有工作流系统的标准考题搜过工作流资料的人应该都有同感现在工作流这三个字被用得太宽泛了。Dify有工作流Coze有工作流n8n有工作流ComfyUI也有工作流连简历筛选和漫剧生成都叫工作流。但真正到了企业级系统设计大家默认的工作流请假实例指的还是那条经典的审批链路——员工提交请假申请主管审批人事归档考勤同步。别小看这条链路它几乎涵盖了BPM引擎的所有核心概念状态流转、任务分配、网关分支、会签或签、驳回退回。我接过的不少项目里第一次引入工作流引擎的团队几乎都拿请假来做试点。原因很实际请假流程足够简单业务方讲得清楚规则开发能快速跑通全链路但它又足够复杂涉及到多级审批、条件分支、超时提醒这些典型需求。一个请假实例能跑顺后面的采购审批、合同会签、离职交接就都有了参照模板。不过这里要先厘清一个容易踩的坑Flowable、Camunda这类BPM引擎和Dify、n8n这类自动化编排工具虽然都叫工作流但解决的问题完全不同。后者更接近任务管道面向的是API调用、数据转换、AI节点编排前者则包含完整的BPMN 2.0规范支持、流程实例持久化、任务分配策略、历史审计。请假审批需要的是后者这类带状态管理和审批语义的引擎如果你拿n8n去搭审批流做到驳回、会签的时候会非常痛苦。这个选择题做对了后面才能省心。2. 开工前先把需求说清楚请假实例的流程规则建模项目正文是空的但只要接过这类需求就知道请假两个字的背后业务方大概率会给你一长串规则。我习惯先画一张规则清单把所有可能的分支、异常、约束列全再去碰代码。下面是我在多个项目里沉淀下来的请假流程规则模板你可以直接拿去和业务方对齐。2.1 参与者、天数分界与审批链设计先确认三方角色员工发起申请、部门主管一级审批、人事备案与考勤同步。这里最容易漏掉的是部门主管请假怎么办以及主管和员工同一天请假谁来审批。简单场景可以先规定主管请假走上级审批但更优雅的做法是引入动态任务分配根据发起人所在部门查询主管如果主管就是发起人自动跳过该节点。天数分界是第二个必须掰扯清楚的规则。我见过最简单的规则是3天以内主管审批3天以上主管审批后转人事二次审批。注意边界值3天整算哪一类补一句含3天就少很多扯皮。如果公司还有年假、事假、病假的不同策略这里就要加请假类型维度不同请假类型走不同分支或者不同时长上限。2.2 从需求到流程图的翻译方法需求对齐之后下一步是把文字规则翻译成流程图。这一步不需要急着打开建模工具先在白板上把节点和连线画出来。以3天内主管审批、3天以上加人事审批为例流程是这样的员工提交申请填写开始时间、结束时间、请假类型、事由系统计算请假天数进入排他网关判断天数3只转主管审批天数3主管审批通过后转人事审批任一节点驳回流程直接结束状态变为已驳回全部通过流程结束状态变为已通过同步考勤系统这个看似简单的流程用BPMN画出来就是一个开始事件、一个用户任务提交申请、一个排他网关、两个用户任务主管审批、人事审批、一个结束事件。提交申请其实也是用户任务这一点很多新手会忽略——先创建流程实例再自动完成提交申请这个任务的做法虽然也能跑但会让待办列表和操作记录变得不自然。正确的做法是流程实例创建后第一个待办就是发起人自己的提交申请任务发起人填写表单并点击提交流程才真正往前走。3. 选型不是越重越好Flowable、Camunda与自研状态机的边界相关热词里同时出现了flowable、camunda、temporal、轻量级工作流。我猜有不少人正卡在选型这一步。先把结论放在前面请假审批这类场景Flowable和Camunda都合适选哪个更多取决于团队技术栈和历史包袱Temporal更适合长时运行的业务编排用在审批流上是杀鸡用牛刀自研状态机只适合规则极其简单、且确定未来不会增加复杂审批规则的小团队。3.1 Flowable与Camunda的对比维度FlowableCamundaBPMN 2.0支持完整支持完整支持且引擎对BPMN规范的实现更严格licenseApache 2.0Apache 2.0核心引擎与Spring Boot集成官方starter集成度高官方starter集成度高流程设计器Flowable UI较老但稳定Camunda Modeler体验更好社区活跃度高高但近年重心偏向SaaS平台学习曲线中等中文资料多中等偏陡但官方文档质量高典型用户国内企业项目常见国内外中大型项目常见从我实操的角度讲Flowable在国内社区沉淀的中文案例和踩坑记录更多遇到问题更容易搜到答案适合团队第一次引入工作流引擎。Camunda的Modeler确实比Flowable自带的UI好用但对于一个请假实例来说设计器差异没有想象中重要因为流程画完之后大部分时间还是写代码。3.2 到底什么时候才需要引入BPM引擎这里多说一句选型的判断标准。如果你的审批流程固定不变节点不超过两三个且不需要流程版本管理、历史追溯、动态驳回那Spring StateMachine甚至手写一个状态表就够了。但一旦出现下面任何一个信号就值得引入BPM引擎业务方明确说审批流程可能经常调整最好能可视化改需要并行审批、会签、或签、条件跳转这类复杂网关逻辑需要记录完整的审批历史包括每个节点的处理人、处理时间、处理意见同一套流程要服务多个业务单据请假、报销、用章申请请假实例恰好把这些信号全部踩中了。所以我最终建议技术栈选型为Spring Boot Flowable MySQL Redis可选用于待办计数缓存。这套组合在中小团队里是性价比最高的跑一个请假实例绰绰有余后续扩展到其他审批流也不用换引擎。4. 完整落地过程从建表到第一条请假单跑通选型定下来之后就是动手了。以下是我在一个实际项目中跑通请假实例的完整过程每一步都是验证过的。4.1 项目依赖与基础配置以Spring Boot 2.7为例引入Flowable依赖dependency groupIdorg.flowable/groupId artifactIdflowable-spring-boot-starter/artifactId version6.8.0/version /dependency引入依赖之后Flowable会自动创建它需要的业务表ACT_开头的那些表。这里注意一个坑Flowable 6.x版本默认会同时创建历史表和通用数据表共60多张如果你的数据库账号权限不足启动会直接报错。我在一个项目里就遇到过开发库账号只有DML权限、没有DDL权限的情况解决方法是提前用有权限的账号初始化好表结构或者让DBA先执行建表脚本。spring: datasource: url: jdbc:mysql://localhost:3306/workflow_demo?useUnicodetruecharacterEncodingutf8nullCatalogMeansCurrenttrue username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver flowable: database-schema-update: true async-executor-activate: falsedatabase-schema-update: true表示启动时自动升级表结构开发环境建议开启。async-executor-activate: false关掉异步执行器请假这种轻量流程不需要异步开着反而多占用线程资源。4.2 定义请假流程的BPMN模型用Flowable Modeler画完图之后最终落地的还是XML文件。我这里给出一个可以直接用的BPMN XML对应3天内主管审批、3天以上加人事审批的规则?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:initiatorinitiator/ userTask idsubmitTask name提交申请 flowable:assignee${initiator} extensionElements modeler:initiator-can-complete xmlns:modelerhttp://flowable.org/modeler flowable:initiatortrue/ /extensionElements /userTask exclusiveGateway iddaysGateway name天数判断/ userTask idmanagerTask name主管审批 flowable:assignee${manager}/ userTask idhrTask name人事审批 flowable:assignee${hrUser}/ endEvent idendApproved name通过结束/ endEvent idendRejected name驳回结束/ sequenceFlow idflow1 sourceRefstartEvent targetRefsubmitTask/ sequenceFlow idflow2 sourceRefsubmitTask targetRefdaysGateway/ sequenceFlow idflow3 sourceRefdaysGateway targetRefmanagerTask conditionExpression xsi:typetFormalExpression ![CDATA[${leaveDays 3}]] /conditionExpression /sequenceFlow sequenceFlow idflow4 sourceRefdaysGateway targetRefmanagerTask conditionExpression xsi:typetFormalExpression ![CDATA[${leaveDays 3}]] /conditionExpression /sequenceFlow sequenceFlow idflow5 sourceRefmanagerTask targetRefendApproved/ sequenceFlow idflow6 sourceRefmanagerTask targetRefendRejected conditionExpression xsi:typetFormalExpression ![CDATA[${approved false}]] /conditionExpression /sequenceFlow /process /definitions等等上面的XML我故意留了一个逻辑错误flow3和flow4都指向managerTask这会让3天以上的流程没法走到人事审批。实际流程里flow4应该指向hrTask然后hrTask再连到结束事件。我在写这种条件分支时经常犯这种错误所以提醒大家画图或者写XML时务必把每条分支走一遍。修正后的核心片段sequenceFlow idflow3 sourceRefdaysGateway targetRefmanagerTask conditionExpression xsi:typetFormalExpression ![CDATA[${leaveDays 3}]] /conditionExpression /sequenceFlow sequenceFlow idflow4 sourceRefdaysGateway targetRefhrTask conditionExpression xsi:typetFormalExpression ![CDATA[${leaveDays 3}]] /conditionExpression /sequenceFlow sequenceFlow idflow5 sourceRefmanagerTask targetRefendApproved/ sequenceFlow idflow7 sourceRefhrTask targetRefendApproved/把流程定义文件放在src/main/resources/processes/leave.bpmn20.xmlFlowable启动时会自动部署。如果你想手动部署并指定版本号可以通过RepositoryServiceAutowired private RepositoryService repositoryService; public void deploy() { Deployment deployment repositoryService.createDeployment() .addClasspathResource(processes/leave.bpmn20.xml) .name(请假审批流程) .deploy(); System.out.println(部署ID: deployment.getId()); }我强烈建议手动部署因为自动部署在开发阶段很方便但到了生产环境你要的是版本可控。手动部署配合一个版本号记录表每次发版都留痕出问题可以快速回滚到上一个部署ID。4.3 发起请假申请的核心代码流程部署之后发起申请的第一步是创建流程实例。这里要给Flowable塞两个东西一个是流程变量leaveDays、manager另一个是业务单据号businessKey用来关联我们自己的业务表Autowired private RuntimeService runtimeService; public void startLeave(String userId, Integer leaveDays, String manager) { MapString, Object variables new HashMap(); variables.put(initiator, userId); variables.put(leaveDays, leaveDays); variables.put(manager, manager); variables.put(approved, null); ProcessInstance processInstance runtimeService.startProcessInstanceByKey( leaveProcess, LEAVE- System.currentTimeMillis(), variables ); System.out.println(流程实例ID: processInstance.getId()); }businessKey建议用有业务含义的单号比如LEAVE-加日期加随机数而不要用数据库自增ID。原因很实际排查问题时你拿到一个流程实例ID还要再查一次数据库才能知道是哪张请假单但拿到LEAVE-20250410-001一眼就能定位。如果是老系统改造甚至可以沿用原有的单据号规则这样就避免了同时维护两套编号体系。流程实例创建后当前待办是提交申请任务。查询待办列表的代码Autowired private TaskService taskService; public ListTask getTodoList(String userId) { return taskService.createTaskQuery() .taskAssignee(userId) .orderByTaskCreateTime() .desc() .list(); }注意请假流程里提交申请任务的assignee是发起人自己${initiator}所以发起人提交之后这个任务完成下一个任务才会到主管那里。如果对接前端这里通常需要两个接口一个是查询待办列表另一个是查询某个任务详情。提示Flowable的任务查询默认只查当前活动任务。如果你的业务需要展示已办抄送历史审批记录那就要查HistoryService了。后面会专门讲。4.4 主管审批与人事审批的落地逻辑主管看到待办之后点通过或者驳回。这个动作对应代码是public void completeTask(String taskId, boolean approved, String comment) { MapString, Object variables new HashMap(); variables.put(approved, approved); // 添加审批意见 taskService.addComment(taskId, null, comment); taskService.complete(taskId, variables); }这里有一个非常关键的细节complete()方法中的流程变量里一定要包含网关判断需要的所有变量。在主管审批这个节点如果approved变量没有在这时设进去流程走到下一个网关时表达式${approved false}会因为变量找不到而报错。我之前就踩过这个坑——主管点通过后流程直接卡住了查日志发现是变量缺失。然后是3天以上那条链路的特殊处理主管审批通过后任务自动流转到人事审批。注意人事审批这个节点的assignee我们在XML里写的是${hrUser}这个流程变量所以主管审批通过时还需要把hrUser传进去public void managerApprove(String taskId, String hrUser) { MapString, Object variables new HashMap(); variables.put(approved, true); variables.put(hrUser, hrUser); taskService.complete(taskId, variables); }这里hrUser怎么确定一般做法是在流程发起时就从配置表里查出来作为流程变量传入。如果公司规模大、人事有多个人这里可以改成候选组candidate group任何一个组内成员都可以认领审批任务。候选组的设置方式是在BPMN里写flowable:candidateGroupshrGroup查询时用taskCandidateGroup(hrGroup)来查。4.5 历史查询与流程状态跟踪请假单状态、审批记录查询是管理后台的核心功能。Flowable底层其实已经帮我们把流程实例ID、任务ID、处理人、处理时间、意见都记录在历史表里了直接用HistoryService查就行不用自己再建一张业务操作日志表Autowired private HistoryService historyService; public ListHistoricActivityInstance getHistory(String processInstanceId) { return historyService.createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .orderByHistoricActivityInstanceStartTime() .asc() .list(); }我个人习惯在业务表里冗余一个当前状态字段比如审批中、已通过、已驳回。这个字段不依赖Flowable业务列表页查询时直接走自己的业务表性能好很多。等到用户点进详情页再从Flowable查完整的历史轨迹。原因很简单业务列表页可能一次要查几十条请假单每条单子都实时去Flowable查当前节点代价太高。冗余状态字段配合Flowable的历史表是最省事的架构。5. 请假实例落地时最容易忽略的五个细节这一节完全是经验之谈。每个坑都是我实际踩过的写出来供大家参考。5.1 驳回逻辑是终止还是退回上一个节点我在第4节的XML里驳回走的是直接结束。这在简单请假场景够用但很多公司的真实规则是驳回后退回发起人发起人修改后重新提交或者主管驳回后发起人可以重新提交但保留原流程实例。这是一个重要的产品决策。Flowable里实现退回有两种主流方式第一种是用边界事件加补偿把流程回退到之前的节点这种实现复杂新手不建议碰第二种是逻辑驳回——不是真的回退流程而是在流程里加一个独占网关判断如果approved false则节点跳回提交申请任务同时驳回次数加1。这种方式实现简单而且自带修改后重新提交的语义实际业务里更好用。如果用第二种方式流程定义需要调整主管审批节点后增加一个独占网关如果驳回则回到提交申请节点并且把流程变量里的submitCount加1。前端展示时根据这个字段显示第2次提交。5.2 同一时刻只有一个审批人乐观锁与任务抢占请假这种轻量流程通常不会出现并发审批但如果是会签场景两个审批人同时点通过就可能出现Flowable的乐观锁异常ObjectOptimisticLockingFailureException。原因在于Flowable的任务表ACT_RU_TASK和流程实例表ACT_RU_EXECUTION都有版本号字段REV_。两个请求同时读到同一个任务都尝试完成任务时后提交的事务会因为版本号不匹配而失败。解决方法有几种最简单的是前端在用户点审批后立刻禁用按钮防止重复提交后端层面可以给审批接口加分布式锁Redis锁锁的key用任务ID。对于请假这种单人审批场景前端防重复就够了但如果你后续要做会签分布式锁或者数据库乐观锁一定要提前设计进去。5.3 流程变量别塞太多我见过有团队把整个请假表单对象几十个字段直接塞进流程变量里。这在数据量小的时候没什么感觉但流程变量表ACT_RU_VARIABLE会越来越大而且在网关判断时会占用大量内存反序列化。我的经验是流程变量只放流程流转需要的数据比如leaveDays、manager、approved、hrUser其他业务数据全部存自己的业务表通过businessKey关联。5.4 事务边界与业务数据一致性请假单的提交涉及两个动作往自己的业务表插入一条请假记录以及调用Flowable完成任务。这两个动作必须在一个事务里。Flowable的Spring Boot Starter默认是支持声明式事务的所以直接在你的Service方法上加Transactional就能保证一致性。这里有个潜在的坑Flowable内部也有自己的事务管理如果你在Service里手动开了事务又在代码里调用了taskService.complete()可能遇到TransactionException。解决办法很简单不要在Service内部混用编程式事务交给Spring的Transactional统一管理就够了。5.5 超时未审批的提醒机制请假审批经常出现主管出差、忘记处理的情况。处理方案不复杂不用改流程定义只需一个定时任务扫ACT_RU_TASK表找出创建时间超过24小时且仍处于活动状态的任务然后给审批人发通知。Component public class LeaveTimeoutJob { Autowired private TaskService taskService; Scheduled(cron 0 0 9 * * ?) public void remind() { Date deadline new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000L); ListTask tasks taskService.createTaskQuery() .taskCreatedBefore(deadline) .list(); for (Task task : tasks) { // 这里发企业微信/钉钉/邮件通知 System.out.println(任务超时: task.getId()); } } }如果还想更进一步可以在超时后自动提醒主管的上级或者自动驳回——这个业务规则就要和公司行政对齐了。注意定时任务需要处理重复推送的问题建议在业务表里加一个lastRemindTime字段每次推送后更新。6. 数据权限与集成把请假流程接进业务系统跑通LeaveProcess只是第一步。真正上线时你还要解决权限、数据隔离和外部系统集成这三个问题。6.1 审批人用机构树动态确定而不是写死在XML里很多团队的请假流程里主管审批人是从组织架构系统实时查出来的。这意味着流程发起时确定主管然后作为流程变量传入。我见过的问题写法是在BPMN里写死一个用户ID比如flowable:assigneezhangsan这样一旦组织调整所有流程实例就都跑错了。正确做法public String getManagerByUserId(String userId) { // 调用组织架构服务 return organizationClient.getManagerId(userId); }组织架构调整后历史流程实例不受影响因为主管已经在流程变量里了新发起的流程实例会拿到新的主管。这也体现了流程变量设计的价值——把可变信息从流程定义中解耦出来。6.2 待办列表的权限控制审批权限控制主要依靠Flowable的查询API。员工只能查自己的待办主管能查部门所有员工的请假任务人事能查全公司所有待办。public ListTask getManagerTodo(String managerId) { Group group identityService.createGroupQuery().groupMember(managerId).singleResult(); return taskService.createTaskQuery() .taskCandidateOrAssigned(managerId) .list(); }如果部门划分更复杂比如主管只负责某几个小组可以结合业务表查询可审批的部门ID列表再根据流程变量的部门ID做过滤。6.3 与考勤、通知系统集成的正确时机请假审批通过后通常要同步到考勤系统。这个同步动作不要放在主管审批通过这个任务的complete逻辑里直接处理因为如果人事审批还没完成同步就太早了。推荐方案是监听流程结束事件Component public class LeaveProcessEndListener implements ExecutionListener { Override public void notify(DelegateExecution execution) { if (execution.getEventName().equals(ExecutionListener.EVENTNAME_END)) { Boolean approved (Boolean) execution.getVariable(approved); if (Boolean.TRUE.equals(approved)) { // 同步考勤发送通知 } } } }在BPMN中给结束事件绑定这个监听器。这样不管是走主管审批直接通过还是主管、人事两级审批通过只要流程正常结束且approved为true都会触发同步逻辑不需要在多个任务节点里重复处理。这个设计是我在对接考勤系统时反复调整后确定的比在每个任务节点都写同步代码要干净得多。7. 请假实例跑通之后怎么扩展成通用审批流如果只是做一个请假这篇文章到这里就结束了。但团队引入Flowable大概率不只是为了请假。跑通这个实例之后我发现有三件事可以顺手做掉让引擎能力复用起来。第一抽象审批通用接口。请假、报销、用章申请本质都是提交一张单据走一条审批链。可以把发起申请查询待办审批通过审批驳回查询历史抽象成统一接口业务系统只需要传入流程定义标识如leaveProcess和业务单号即可。这一步做完后续接新流程的成本会降到极低。第二流程版本管理要尽早规范。Flowable升级流程定义后新发起的流程实例走新版旧实例继续走旧版这是默认行为。但在实际项目中流程模型V1和V2之间往往有较大的变量差异比如V2新增了一个是否涉及费用节点老实例在运行过程中如果引用了不存在的变量会报错。建议维护一张流程版本与业务规则对照表发版前检查所有存量活动实例的兼容性。第三预留会签和加签的扩展位。请假不一定需要会签但同一个引擎跑部门经费审批时很可能要求多人会签。Flowable的BPMN里支持多实例任务multi-instance我建议在流程设计阶段就给审批任务预留多实例的配置空间。具体做法是在BPMN的userTask上配置multiInstanceLoopCharacteristics元素集合用assigneeList流程变量传入而不是写死单个assignee。这样同一个审批节点既能跑单签也能跑会签切换成本只是一条XML配置。最后再分享一个实际体会工作流实例类的项目最耗时间的不是写代码而是和业务方对齐规则。请假看起来简单真做起来边界条件一大堆。你只要在开工前把天数边界、驳回语义、超时策略、组织架构联动这四件事全部落在纸面上后面的编码就是体力活。反过来如果需求没对齐就急着启动流程建模大概率会在项目后期被业务方一句哦对了我们驳回之后发起人要能重新提交折腾到返工。本文还有配套的精品资源点击获取