简介:这份资源是面向工作流初学者与Java Web开发入门者的请假流程实例,围绕Struts2框架演示如何将工作流技术落地到企业管理中的请假审批场景,帮助读者理解任务分配、审批流转与通知等环节的自动化实现。压缩包共42个文件,约51KB,以xml配置、jsp页面、properties属性文件为主,辅以java源码、class字节码及少量图片与工程配置,涵盖Action映射、拦截器栈、字符编码设置以及请假申请表单与审批结果展示等模块。资源中涉及Struts2处理中文字符编码的常见问题,可通过struts.properties或struts.xml统一设置UTF-8,并借助拦截器扩展日志与权限检查,对理解MVC分层与业务逻辑组织有实际参考价值。目前已有253人学习,适合作为工作流与Struts2整合练习的起步案例。
1. 工作流请假实例:从一张审批单看引擎的骨架
很多人第一次接触工作流引擎,都是从「请假」这个场景开始的。它足够小,小到一张表单、两个审批节点就能跑通;又足够典型,串起了表单定义、节点流转、条件分支、任务分配、状态回写这几件事。我见过不少团队,用请假实例做技术选型验证,结果上线后真正卡住的不是引擎本身,而是「请假三天以上要不要加签」「销假怎么回写考勤」这类业务细节。这篇笔记就围绕工作流请假实例,把从建模到落地的完整路径拆开讲:先讲清楚一个请假流程在引擎里到底长什么样,再给可复现的建模与代码,最后把踩过的坑摊开。适合正在做审批类系统、准备引入工作流引擎,或者已经用了但总觉得别扭的开发者。
2. 请假流程建模:先把节点和变量定死
2.1 为什么请假实例适合做工作流验证
请假流程的节点数量少,但覆盖了工作流最核心的几类行为:串行审批、条件分支、任务分配、状态变更。一个典型的请假流程通常包含发起、直属主管审批、人事备案三个节点,如果请假天数超过某个阈值,还要插入部门负责人审批。这个结构用 BPMN 画出来就是一条主干加一个排他网关,简单到不会掩盖引擎本身的问题。
我一般建议团队在选型阶段就用这个场景跑一遍,重点观察四件事:引擎能不能在不改代码的前提下调整审批人、条件表达式是否支持热更新、任务列表能不能按候选人过滤、流程实例的状态能不能被外部系统查询。这四点决定了后续接入考勤、薪资系统时会不会返工。请假实例的价值不在于它简单,而在于它把「流程定义」和「业务数据」的边界暴露得很清楚——哪些该放在流程变量里,哪些该留在业务表里,一开始就要想明白。
2.2 用 BPMN 定义请假流程的关键元素
下面是一段用 BPMN 2.0 描述的请假流程定义,这里用常见的 XML 形式表达,实际落地时可以用引擎自带的建模器生成。重点看网关的条件表达式和用户任务的分配方式。
<process id="leaveProcess" name="请假申请流程"> <startEvent id="start" /> <sequenceFlow sourceRef="start" targetRef="submitTask" /> <!-- 发起节点:由申请人填写表单 --> <userTask id="submitTask" name="填写请假单" /> <!-- 排他网关:根据请假天数决定审批路径 --> <exclusiveGateway id="daysGateway" /> <sequenceFlow sourceRef="submitTask" targetRef="daysGateway" /> <!-- 条件一:天数小于等于3,直接走主管审批 --> <sequenceFlow sourceRef="daysGateway" targetRef="managerApprove"> <conditionExpression xsi:type="tFormalExpression"> ${leaveDays <= 3} </conditionExpression> </sequenceFlow> <!-- 条件二:天数大于3,先走部门负责人 --> <sequenceFlow sourceRef="daysGateway" targetRef="directorApprove"> <conditionExpression xsi:type="tFormalExpression"> ${leaveDays > 3} </conditionExpression> </sequenceFlow> <userTask id="managerApprove" name="直属主管审批" /> <userTask id="directorApprove" name="部门负责人审批" /> <!-- 两条路径汇合后进入人事备案 --> <sequenceFlow sourceRef="managerApprove" targetRef="hrRecord" /> <sequenceFlow sourceRef="directorApprove" targetRef="hrRecord" /> <userTask id="hrRecord" name="人事备案" /> <sequenceFlow sourceRef="hrRecord" targetRef="end" /> <endEvent id="end" /> </process>这段定义里有两个地方值得注意。第一,条件表达式直接引用了leaveDays这个流程变量,意味着发起节点必须把请假天数写入流程变量,而不是只存在业务表里。第二,两条审批路径最终都指向hrRecord,这里没有用并行网关,因为实际业务中主管审批和部门负责人审批不会同时发生,用排他网关更贴合语义。
参数说明:leaveDays是数值型流程变量,单位是天;条件表达式里的${}是常见表达式语言写法,不同引擎可能用 JUEL、SpEL 或 FEEL,迁移时要注意语法差异。用户任务的分配这里没有写,实际项目中通常用assignee或candidateUsers指定,也可以交给监听器动态计算。
2.3 流程变量与业务表字段的划分原则
这是请假实例里最容易翻车的地方。我的经验是:流程变量只放「影响流转决策」的数据,比如请假天数、请假类型、申请人部门;而请假事由、附件、销假时间这些只用于展示和归档的字段,留在业务表里。原因很简单,流程变量会随流程实例持久化,引擎每次流转都可能读写它,放太多字段会让历史表膨胀,查询也变慢。
具体做法是:业务表存完整请假单,流程启动时只把leaveDays、leaveType、applicantDept三个字段通过runtimeService.startProcessInstanceByKey的 variables 参数传进去。后续节点如果需要展示完整信息,用业务主键去查业务表,而不是从流程变量里取。这样即使流程定义调整,业务数据也不受影响。
3. 用代码跑通一个最小请假实例
3.1 引擎初始化与流程部署
下面以常见的 Java 工作流引擎为例,给出一段可复现的初始化与部署代码。不同引擎的 API 名称会有差异,但步骤一致:建引擎、拿仓库服务、部署流程定义。
// 1. 创建流程引擎(这里用默认配置,实际项目会配数据源) ProcessEngine engine = ProcessEngineConfiguration .createStandaloneProcessEngineConfiguration() .setJdbcUrl("jdbc:h2:mem:leave;DB_CLOSE_DELAY=-1") .setJdbcDriver("org.h2.Driver") .setJdbcUsername("sa") .setJdbcPassword("") .setDatabaseSchemaUpdate("true") .buildProcessEngine(); // 2. 拿到仓库服务,部署上面定义的 BPMN 文件 RepositoryService repositoryService = engine.getRepositoryService(); Deployment deployment = repositoryService.createDeployment() .addClasspathResource("leaveProcess.bpmn20.xml") .name("请假流程部署") .deploy(); // 3. 确认部署结果,拿到流程定义 ID ProcessDefinition definition = repositoryService.createProcessDefinitionQuery() .deploymentId(deployment.getId()) .singleResult(); System.out.println("流程定义 ID: " + definition.getId());逻辑说明:第一步用内存数据库是为了本地验证,生产环境换成连接池配置即可。第二步的addClasspathResource要求 BPMN 文件放在资源目录下,文件名要和代码里一致。第三步查询部署结果,是为了拿到流程定义 ID,后续启动实例时可以用 key 也可以用 ID。
参数说明:setDatabaseSchemaUpdate("true")表示自动建表,仅建议在开发和测试环境使用,生产环境应该用 SQL 脚本手动建表,避免引擎版本升级时表结构被意外修改。DB_CLOSE_DELAY=-1是 H2 内存库的常见配置,保证连接关闭后数据不丢,方便调试。
3.2 启动实例并写入请假变量
流程部署好之后,下一步是启动一个请假实例,并把影响流转的变量传进去。
// 启动流程实例,传入请假天数和类型 Map<String, Object> variables = new HashMap<>(); variables.put("leaveDays", 5); // 请假天数,决定走哪条审批路径 variables.put("leaveType", "年假"); // 请假类型,用于后续展示 variables.put("applicantDept", "研发部"); // 申请人部门,用于任务分配 ProcessInstance instance = engine.getRuntimeService() .startProcessInstanceByKey("leaveProcess", variables); System.out.println("实例 ID: " + instance.getId()); System.out.println("当前节点: " + instance.getActivityId());逻辑说明:startProcessInstanceByKey会根据流程定义的 key 找到最新版本的流程并启动。传入的 variables 会作为流程变量持久化,排他网关在流转时会读取leaveDays的值。如果这里传了 5,流程会走部门负责人审批那条路径。
参数说明:leaveDays必须是数值类型,如果传字符串,条件表达式比较时可能报类型转换错误。leaveType和applicantDept虽然不影响流转,但后续任务分配监听器可能会用到,所以一并传入。实际项目中,这些变量建议封装成一个 DTO,避免散落在各处。
3.3 查询待办与完成任务
实例启动后,任务会落到对应的审批人头上。下面这段代码演示如何查询待办并完成审批。
// 查询当前实例下的待办任务 TaskService taskService = engine.getTaskService(); List<Task> tasks = taskService.createTaskQuery() .processInstanceId(instance.getId()) .list(); for (Task task : tasks) { System.out.println("待办任务: " + task.getName() + ",任务 ID: " + task.getId()); } // 假设当前是部门负责人审批,完成时传入审批意见 if (!tasks.isEmpty()) { Task current = tasks.get(0); Map<String, Object> approveVars = new HashMap<>(); approveVars.put("approved", true); approveVars.put("comment", "同意"); taskService.complete(current.getId(), approveVars); }逻辑说明:createTaskQuery按流程实例 ID 过滤,拿到当前待办。实际系统中还会按候选人、候选人组过滤,这里为了简化只按实例查。complete方法完成任务并推动流程往下走,传入的变量会合并到流程变量中。
参数说明:approved和comment是审批结果变量,如果后续节点需要根据审批结果做分支,这两个变量就会派上用场。注意complete之后任务会从待办列表消失,如果重复调用会抛异常,所以生产代码里要先判断任务是否存在。
4. 请假实例落地时的避坑清单
4.1 审批人写死在流程定义里
现象:流程上线后,组织架构调整,主管换人,但流程定义里的审批人还是旧的,导致任务派给了离职员工。
原因:很多示例代码为了演示方便,直接在 BPMN 里写assignee="zhangsan",实际项目中这是大忌。
解决:审批人一律动态计算。常见做法是用任务监听器,在create事件里根据申请人部门去查组织架构,然后设置assignee或candidateUsers。如果组织架构变动频繁,还可以把审批人规则外置到配置表,监听器读表决定。
4.2 条件表达式里的变量类型不匹配
现象:流程启动时报错,提示条件表达式求值失败,或者明明请假 5 天却走了 3 天以内的分支。
原因:leaveDays传了字符串 "5",而表达式里按数值比较,引擎在做类型转换时行为不一致。
解决:启动实例前统一做类型校验,数值型变量必须传 Number。如果前端传过来的是字符串,在服务层先转成 Integer 或 BigDecimal。另外,条件表达式里尽量写${leaveDays > 3}而不是${leaveDays > '3'},避免隐式转换。
4.3 流程变量越塞越多导致历史表膨胀
现象:系统运行半年后,历史流程实例查询越来越慢,数据库磁盘占用增长很快。
原因:把请假事由、附件路径、审批意见全文都塞进了流程变量,每次流转都往历史表写一份。
解决:流程变量只保留影响流转的字段,其余数据放业务表。审批意见这类会随节点变化的数据,可以放在任务本地变量里,任务结束后随历史任务表归档,不会污染流程实例级别的变量。
4.4 销假和撤销没有对应的流程路径
现象:员工请假后提前返岗,或者提交后发现填错想撤回,系统没有入口,只能让管理员去数据库改状态。
原因:建模时只考虑了正常审批路径,忽略了异常路径。
解决:在流程定义里增加边界事件或撤销节点。常见做法是用一个「撤销」用户任务,或者在发起节点上挂一个消息边界事件,收到撤销消息后终止流程。销假则可以用一个新的流程实例关联原请假单,或者用子流程处理。关键是建模阶段就要把这些异常路径画出来,不要等上线后打补丁。
4.5 并发审批导致状态覆盖
现象:主管和人事同时打开待办,主管先同意,人事后同意,结果流程状态被后一次操作覆盖,或者流程被重复推进。
原因:没有做乐观锁或任务签收,两个线程同时操作同一个任务。
解决:完成任务前先调用claim签收任务,确保同一时刻只有一个处理人。如果业务允许会签,改用会签节点,由引擎保证计数。另外,业务表更新时带上版本号,避免状态回写时覆盖。
5. 把请假实例用成流程引擎的验证模板
请假实例跑通之后,不要急着上生产,先把它当成一个验证模板,用几个变体压一压引擎的边界。我一般会做三组测试:第一组把审批节点从两个加到五个,观察任务列表查询和流程流转的耗时变化;第二组把条件表达式改成多条件组合,比如「天数大于 3 且类型为事假」,看表达式引擎是否支持逻辑运算;第三组模拟审批人离职,验证动态分配规则能不能兜住。
下面这张表是我常用的验证清单,每次引入新引擎或升级版本都会过一遍。
| 验证项 | 操作方式 | 观察指标 |
|---|---|---|
| 流程定义热更新 | 修改 BPMN 后重新部署,不重启应用 | 新实例是否走新定义,旧实例是否不受影响 |
| 条件表达式复杂度 | 增加三条件组合的网关 | 表达式求值是否报错,性能是否明显下降 |
| 任务分配动态化 | 删除流程定义里的 assignee,改用监听器 | 任务能否正确派给组织架构中的人 |
| 历史数据查询 | 跑 1000 个实例后查历史表 | 查询耗时是否在可接受范围 |
| 异常路径 | 模拟撤销、驳回、超时 | 流程能否正确终止或回退 |
一个具体的技巧是:把请假实例的流程定义和业务代码分开管理。流程定义文件放在独立的配置仓库,业务代码只依赖流程 key 和变量约定。这样调整审批规则时不需要重新编译发版,运维在管理后台重新部署即可。我见过团队把 BPMN 文件打在 jar 包里,改一个审批人就要走一次发版流程,血泪经验就是别这么干。
另外,流程变量的命名要有统一前缀,比如leave_开头,避免和引擎内置变量冲突。引擎通常会有initiator、processDefinitionId这类内置变量,如果业务变量重名,排查起来非常费劲。我习惯在项目初期就定一份变量命名规范,写进团队文档,后面接手的同学照着填就行。
最后说一个我自己的习惯:每次新建一个工作流项目,先不写业务代码,而是用请假实例把引擎的 API 全部过一遍,包括启动、查询、签收、完成、驳回、撤销、历史查询。这一圈走下来,引擎的脾气基本就摸清了,后面再复杂的流程也只是节点和条件的组合。希望帮到你。
本文还有配套的精品资源,点击获取