简介:这是一份面向工作流初学者与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 id="leaveProcess" name="请假审批流程"> <startEvent id="start" /> <userTask id="submitLeave" name="提交请假申请" /> <exclusiveGateway id="daysGateway" /> <userTask id="managerApprove" name="主管审批" /> <userTask id="directorApprove" name="总监审批" /> <serviceTask id="hrRecord" name="人事备案" /> <endEvent id="end" /> <sequenceFlow sourceRef="start" targetRef="submitLeave" /> <sequenceFlow sourceRef="submitLeave" targetRef="daysGateway" /> <sequenceFlow sourceRef="daysGateway" targetRef="managerApprove"> <conditionExpression xsi:type="tFormalExpression"> ${leaveDays <= 3} </conditionExpression> </sequenceFlow> <sequenceFlow sourceRef="daysGateway" targetRef="directorApprove"> <conditionExpression xsi:type="tFormalExpression"> ${leaveDays > 3} </conditionExpression> </sequenceFlow> <sequenceFlow sourceRef="managerApprove" targetRef="hrRecord" /> <sequenceFlow sourceRef="directorApprove" targetRef="hrRecord" /> <sequenceFlow sourceRef="hrRecord" targetRef="end" /> </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. 准备流程变量 Map<String, 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 id="leaveProcess">的值,必须完全一致。第二个参数businessKey是业务主键,我习惯用“前缀+单号”的格式,后面查历史记录时直接用它过滤,比用流程实例 ID 可读性好得多。第三个参数variables就是流程变量,它在整个流程生命周期内都能被网关和任务监听器读取。
参数坑点:leaveDays如果传的是字符串 "3",网关里的${leaveDays <= 3}会抛类型转换异常。我一般在入口做一次强制类型转换,或者用Integer.parseInt兜底。另外,变量名不要用start、end这类跟 BPMN 元素 id 重名的词,引擎解析时可能冲突。
3.2 查询待办任务与完成审批
流程启动后,主管登录系统应该能看到待办。查询待办用TaskService,完成审批也是用它。
@Autowired private TaskService taskService; // 查询某人的待办 public List<Task> getTodoList(String assignee) { return taskService.createTaskQuery() .taskAssignee(assignee) .orderByTaskCreateTime().desc() .list(); } // 完成审批 public void approve(String taskId, boolean pass, String comment) { Map<String, 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 event="create" delegateExpression="${leaveTaskListener}"。如果不想用表达式,也可以直接写全类名,但那样没法注入 Mapper。
验证监听器是否生效,最简单的办法是看日志表里有没有数据。如果create事件没触发,检查 BPMN 里监听器的event属性是否拼写正确;如果complete事件没触发,确认任务是不是通过complete方法结束的,用resolveTask不会触发 complete 事件。
还有一个实用技巧:把流程状态同步到业务表时,不要在每个监听器里单独写更新逻辑,而是用一个统一的ProcessStatusSyncListener挂在流程结束事件上,一次性把最终状态写回去。这样业务表的状态字段永远和流程实例的最终状态一致,查历史数据时不用再关联引擎表。
从那以后我每次搭新的审批流,都强制先把监听器配好再写业务代码,宁可前期多花半小时,也不想到处补日志。希望帮到你。
本文还有配套的精品资源,点击获取