简介这是一份面向工作流初学者与Java Web开发入门者的请假流程实例源码围绕企业管理中常见的请假审批场景演示如何用工作流思想组织任务分配、审批与通知等环节并以Struts2框架承载请求处理与业务逻辑同时涉及中文编码过滤等常见问题的处理思路。压缩包共42个文件约51KB以xml配置、jsp页面、properties属性文件为主辅以java源码、class字节码及少量图片与工程配置分别对应流程定义、视图展示、字符编码设置和业务实现等用途结构紧凑便于按模块查阅。目前已有253人学习下载。读者可从中获得一套可运行的请假流程示例理解Action、拦截器与持久化层的协作方式并借鉴中文乱码的排查与配置经验适合作为工作流与Struts2入门练手项目。1. 工作流请假实例从零搭一套能跑通的审批流到底要填多少坑刚接手审批流开发那会儿我以为“请假”是最简单的场景——员工提交、主管审批、结束。结果真动手才发现光是“请假三天以上要不要总监审批”“审批人离职了流程卡死怎么办”“驳回后能不能修改重提”这几个问题就够写一整天。工作流请假实例之所以被反复拿来练手不是因为它简单而是因为它把流程定义、节点跳转、条件分支、任务分配、状态回滚这些核心机制全串起来了。你如果正在找一个能跑通、能改、能扩展的最小闭环这个实例就是干这个的。它适合刚接触工作流引擎的后端开发也适合需要快速验证审批模型的产品或测试同学。下面我按实际拆解的顺序把流程定义、引擎选型、代码落地和踩坑记录一次讲透。2. 流程定义与引擎选型为什么请假单最适合拿来试刀2.1 请假单里藏着的四个工作流核心概念很多人一上来就急着写代码结果流程定义文件写到一半发现节点类型选错了返工重来。请假单虽然业务简单但它天然覆盖了工作流最基础的四个概念流程定义、任务节点、条件网关、流转条件。流程定义就是整张请假单的“地图”它规定了从提交到结束一共经过哪些节点。任务节点分两种用户任务和系统任务。用户任务需要人参与比如“主管审批”系统任务自动执行比如“发送通知”。条件网关是请假单里最容易写错的地方——比如“请假天数大于3天走总监审批否则直接到人事备案”这个判断就挂在网关上。流转条件则是连接线上的表达式决定流程往哪走。我一般建议新手先把这四个概念在纸上画一遍再打开建模器拖拽。画的时候重点标三个东西谁发起、谁审批、什么条件下换人。这三样定了流程定义基本不会大改。2.2 引擎选型别为了请假单上重型武器工作流引擎的选择直接决定你后面写代码的体量。常见做法是分三档轻量级用状态机自己撸中量级用 Activiti 或 Flowable重量级用 Camunda 或 Zeebe。请假单这个场景我一般会先问一句后面要不要接会签、加签、驳回任意节点、动态改流程如果只是固定两三级审批状态机足够代码量少、调试直观。如果预期半年内会加会签和条件分支直接上 Flowable它的 BPMN 2.0 支持完整社区资料也多。这里有个选型误区很多人觉得 Camunda 功能强就无脑上结果请假单跑起来发现光配置就写了三百行后面维护的人看不懂。引擎不是越重越好是跟你的流程复杂度匹配。请假单的流程节点通常不超过十个Flowable 的轻量嵌入模式完全够用启动快、依赖少跟 Spring Boot 整合也顺。提示选型时先确认团队里有没有人写过 BPMN 文件。如果没人写过选带图形化建模器的引擎能省掉大量手写 XML 的时间。2.3 用 BPMN 画一张能跑的请假流程图下面这张流程定义我改过很多版最终稳定下来的结构是开始事件 → 提交请假申请用户任务→ 天数判断网关 → 主管审批用户任务→ 总监审批用户任务仅大于3天走→ 人事备案系统任务→ 结束事件。用 Flowable 的 BPMN XML 表示核心片段如下process idleaveProcess name请假审批流程 startEvent idstart / userTask idsubmitLeave name提交请假申请 / exclusiveGateway iddaysGateway / userTask idmanagerApprove name主管审批 / userTask iddirectorApprove name总监审批 / serviceTask idhrRecord name人事备案 / endEvent idend / sequenceFlow sourceRefstart targetRefsubmitLeave / sequenceFlow sourceRefsubmitLeave targetRefdaysGateway / sequenceFlow sourceRefdaysGateway targetRefmanagerApprove conditionExpression xsi:typetFormalExpression ${leaveDays 3} /conditionExpression /sequenceFlow sequenceFlow sourceRefdaysGateway targetRefdirectorApprove conditionExpression xsi:typetFormalExpression ${leaveDays 3} /conditionExpression /sequenceFlow sequenceFlow sourceRefmanagerApprove targetRefhrRecord / sequenceFlow sourceRefdirectorApprove targetRefhrRecord / sequenceFlow sourceRefhrRecord targetRefend / /process这段 XML 的逻辑很直白daysGateway是排他网关它只选一条路走。leaveDays是流程变量在提交申请时由前端传入。注意条件表达式里用的是${}包裹的布尔判断Flowable 会在网关节点自动求值。如果两个条件都不满足引擎会抛异常所以实际项目里我会再加一条默认流兜底。参数说明leaveDays建议用整数类型避免浮点数比较时出现精度问题。conditionExpression里的变量名必须和流程启动时传入的 key 完全一致大小写敏感。serviceTask的hrRecord需要绑定一个 Java 委托类或表达式否则流程走到这里会报“未实现”错误。3. 代码落地从提交申请到审批通过的完整链路3.1 启动流程实例与变量传递流程定义部署好之后第一步是启动实例。下面这段代码是 Spring Boot 里常见的写法我把它拆成三步拿引擎、设变量、启动。Autowired private RuntimeService runtimeService; public String startLeaveProcess(LeaveRequest request) { // 1. 准备流程变量 MapString, Object variables new HashMap(); variables.put(applicant, request.getApplicant()); variables.put(leaveDays, request.getLeaveDays()); variables.put(reason, request.getReason()); // 2. 用业务单号作为 businessKey方便后续查询 String businessKey LEAVE- request.getId(); // 3. 启动流程实例 ProcessInstance instance runtimeService.startProcessInstanceByKey( leaveProcess, businessKey, variables); return instance.getId(); }逻辑说明startProcessInstanceByKey的第一个参数是 BPMN 文件里process idleaveProcess的值必须完全一致。第二个参数businessKey是业务主键我习惯用“前缀单号”的格式后面查历史记录时直接用它过滤比用流程实例 ID 可读性好得多。第三个参数variables就是流程变量它在整个流程生命周期内都能被网关和任务监听器读取。参数坑点leaveDays如果传的是字符串 3网关里的${leaveDays 3}会抛类型转换异常。我一般在入口做一次强制类型转换或者用Integer.parseInt兜底。另外变量名不要用start、end这类跟 BPMN 元素 id 重名的词引擎解析时可能冲突。3.2 查询待办任务与完成审批流程启动后主管登录系统应该能看到待办。查询待办用TaskService完成审批也是用它。Autowired private TaskService taskService; // 查询某人的待办 public ListTask getTodoList(String assignee) { return taskService.createTaskQuery() .taskAssignee(assignee) .orderByTaskCreateTime().desc() .list(); } // 完成审批 public void approve(String taskId, boolean pass, String comment) { MapString, Object vars new HashMap(); vars.put(approved, pass); vars.put(comment, comment); // 先添加审批意见再完成任务 taskService.addComment(taskId, null, comment); taskService.complete(taskId, vars); }逻辑说明taskAssignee对应 BPMN 里userTask的assignee属性如果流程定义里没写死就需要在启动时通过变量动态指定或者用监听器在任务创建时设置。complete方法会把流程推进到下一个节点同时把vars合并进流程变量。approved这个变量在后面的网关或监听器里可以用来判断通过还是驳回。参数说明addComment的第二个参数是流程实例 ID传 null 时 Flowable 会自动关联当前任务所在的实例。审批意见建议单独存一张业务表引擎自带的 comment 表字段有限复杂意见比如带附件还是自己管理更灵活。3.3 驳回与重新提交的状态处理驳回是请假流里最容易出 bug 的地方。常见做法有两种一种是驳回到发起人流程回到“提交申请”节点另一种是驳回到上一个审批节点。我一般推荐第一种因为请假单的修改权在申请人手里驳回到上一级审批人没有意义。public void rejectToApplicant(String taskId, String reason) { // 1. 获取当前流程实例 Task task taskService.createTaskQuery().taskId(taskId).singleResult(); String processInstanceId task.getProcessInstanceId(); // 2. 给流程实例打上驳回标记 runtimeService.setVariable(processInstanceId, rejected, true); runtimeService.setVariable(processInstanceId, rejectReason, reason); // 3. 通过跳转指令回到提交节点 runtimeService.createChangeActivityStateBuilder() .processInstanceId(processInstanceId) .moveActivityIdTo(task.getTaskDefinitionKey(), submitLeave) .changeState(); }逻辑说明createChangeActivityStateBuilder是 Flowable 提供的动态跳转 API它能把流程从当前节点强制拉到指定节点。moveActivityIdTo的第一个参数是当前任务的定义 key第二个是目标节点 id。执行后流程会回到“提交请假申请”节点申请人可以修改天数后重新提交。注意动态跳转不会自动清理历史任务如果业务上需要展示“驳回记录”得自己查历史表。另外跳转后原来的流程变量还在重新提交时记得覆盖leaveDays否则网关判断用的还是旧值。这个坑我踩过申请人改了天数但审批人没变查了半天才发现变量没更新。4. 避坑与排查请假流跑不通时先看这五条4.1 流程启动报“找不到流程定义”现象调用startProcessInstanceByKey时抛FlowableObjectNotFoundException提示 no processes deployed with key。原因BPMN 文件没有部署成功或者部署时指定的 key 和代码里写的不一致。常见情况是文件放在resources/processes/下但没被自动扫描或者 XML 里process id写成了别的名字。解决先查act_re_procdef表看key_字段的值到底是什么。如果表是空的检查flowable.check-process-definitions配置是否为 true以及 BPMN 文件路径是否在flowable.process-definition-location-prefix指定的目录下。4.2 网关条件不生效流程走错分支现象请假天数明明大于3天流程却直接走到人事备案跳过了总监审批。原因条件表达式里的变量名写错或者变量类型不匹配。比如 BPMN 里写的是${leaveDays 3}但启动时传的 key 是leave_days引擎取不到值就默认 false于是走了另一条路。解决在网关节点前加一个执行监听器把当前所有流程变量打印出来。确认变量名和类型后再核对 BPMN 里的表达式。如果用的是字符串比较记得加引号比如${leaveType 事假}。4.3 审批人离职后任务卡死现象主管离职后他名下的待办任务没人能处理流程一直停在“主管审批”节点。原因任务分配时写死了 assignee没有考虑人员变动。解决把 assignee 改成 candidateUsers 或 candidateGroups让一组人都能看到待办。如果必须指定个人加一个定时任务扫描超时未处理的任务自动转交给上级或管理员。Flowable 的taskService.createTaskQuery().taskUnassigned()可以查出所有未签收的任务配合claim方法重新分配。4.4 驳回后重新提交历史记录重复现象申请人驳回后修改再提交待办列表里出现了两条相同的审批任务。原因动态跳转时没有结束当前任务或者跳转后旧任务的历史记录没清理干净。解决跳转前先调用taskService.complete或taskService.resolveTask把当前任务标记为已完成。如果业务上需要保留驳回痕迹就在业务表里单独记一条驳回日志不要依赖引擎的历史任务表。4.5 流程变量在并发审批时被覆盖现象会签场景下两个审批人同时提交意见后提交的把先提交的变量覆盖了。原因runtimeService.setVariable是全局变量多个任务同时写同一个 key 会互相覆盖。解决用setVariableLocal把变量绑定到当前任务或执行实例上或者把审批意见存到业务表流程变量只保留最终结果。如果必须用全局变量加乐观锁或分布式锁但请假单场景一般用不上这么重。5. 进阶技巧用监听器把审批日志和业务表串起来流程能跑通只是第一步真正上线还要解决“审批记录怎么查”“流程状态怎么同步到业务表”这两个问题。我一般会在流程定义里挂两类监听器执行监听器和任务监听器。执行监听器挂在sequenceFlow上用来记录节点流转时间。任务监听器挂在userTask上用来在任务创建和完成时写业务日志。下面这段代码是一个任务监听器的实现它把审批人、审批时间、审批意见写进自定义的leave_approve_log表。Component(leaveTaskListener) public class LeaveTaskListener implements TaskListener { Autowired private ApproveLogMapper approveLogMapper; Override public void notify(DelegateTask delegateTask) { String eventName delegateTask.getEventName(); if (create.equals(eventName)) { // 任务创建时记录待办生成时间 ApproveLog log new ApproveLog(); log.setTaskId(delegateTask.getId()); log.setProcessInstanceId(delegateTask.getProcessInstanceId()); log.setAssignee(delegateTask.getAssignee()); log.setAction(CREATE); log.setCreateTime(new Date()); approveLogMapper.insert(log); } else if (complete.equals(eventName)) { // 任务完成时更新审批结果 ApproveLog log new ApproveLog(); log.setTaskId(delegateTask.getId()); log.setAction(COMPLETE); log.setComment((String) delegateTask.getVariable(comment)); log.setCompleteTime(new Date()); approveLogMapper.insert(log); } } }逻辑说明delegateTask.getEventName()返回当前触发的事件名常见的有create、assignment、complete、delete。我一般只处理create和complete前者用来生成待办记录后者用来落审批结果。delegateTask.getVariable可以读取流程变量但注意在complete事件里变量可能还没合并进去稳妥做法是从delegateTask.getVariables()里取。参数说明监听器类需要用Component注册成 Spring Bean然后在 BPMN 里通过flowable:taskListener引用写法是flowable:taskListener eventcreate delegateExpression${leaveTaskListener}。如果不想用表达式也可以直接写全类名但那样没法注入 Mapper。验证监听器是否生效最简单的办法是看日志表里有没有数据。如果create事件没触发检查 BPMN 里监听器的event属性是否拼写正确如果complete事件没触发确认任务是不是通过complete方法结束的用resolveTask不会触发 complete 事件。还有一个实用技巧把流程状态同步到业务表时不要在每个监听器里单独写更新逻辑而是用一个统一的ProcessStatusSyncListener挂在流程结束事件上一次性把最终状态写回去。这样业务表的状态字段永远和流程实例的最终状态一致查历史数据时不用再关联引擎表。从那以后我每次搭新的审批流都强制先把监听器配好再写业务代码宁可前期多花半小时也不想到处补日志。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?