简介这份资源是面向工作流初学者与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 idleaveProcess name请假申请流程 startEvent idstart / sequenceFlow sourceRefstart targetRefsubmitTask / !-- 发起节点由申请人填写表单 -- userTask idsubmitTask name填写请假单 / !-- 排他网关根据请假天数决定审批路径 -- exclusiveGateway iddaysGateway / sequenceFlow sourceRefsubmitTask targetRefdaysGateway / !-- 条件一天数小于等于3直接走主管审批 -- sequenceFlow sourceRefdaysGateway targetRefmanagerApprove conditionExpression xsi:typetFormalExpression ${leaveDays 3} /conditionExpression /sequenceFlow !-- 条件二天数大于3先走部门负责人 -- sequenceFlow sourceRefdaysGateway targetRefdirectorApprove conditionExpression xsi:typetFormalExpression ${leaveDays 3} /conditionExpression /sequenceFlow userTask idmanagerApprove name直属主管审批 / userTask iddirectorApprove name部门负责人审批 / !-- 两条路径汇合后进入人事备案 -- sequenceFlow sourceRefmanagerApprove targetRefhrRecord / sequenceFlow sourceRefdirectorApprove targetRefhrRecord / userTask idhrRecord name人事备案 / sequenceFlow sourceRefhrRecord targetRefend / endEvent idend / /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 启动实例并写入请假变量流程部署好之后下一步是启动一个请假实例并把影响流转的变量传进去。// 启动流程实例传入请假天数和类型 MapString, 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(); ListTask 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); MapString, 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 里写assigneezhangsan实际项目中这是大忌。解决审批人一律动态计算。常见做法是用任务监听器在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 全部过一遍包括启动、查询、签收、完成、驳回、撤销、历史查询。这一圈走下来引擎的脾气基本就摸清了后面再复杂的流程也只是节点和条件的组合。希望帮到你。本文还有配套的精品资源点击获取