吃透 Java 轻量级流程引擎 Easy Work:从核心原理到生产级落地全指南
一、为什么你需要轻量级流程引擎?
在企业级开发中,审批流、工单流转、状态机管控等流程类需求无处不在。传统重量级流程引擎(Activiti、Flowable、Camunda)虽功能完备,但存在部署复杂、学习成本高、对中小微型流程场景过度设计的痛点;而自研流程引擎又极易出现扩展性差、边界处理不到位、维护成本高的问题。
Easy Work作为一款开源轻量级Java流程引擎,完美解决了上述矛盾。它基于状态机模型设计,剥离了BPMN2.0规范中90%的低频复杂特性,仅保留核心的流程编排、任务管控、扩展监听能力,学习成本降低90%,性能较传统引擎提升3倍以上,是国内开发者中小微型流程场景的首选方案。
二、Easy Work核心架构与底层原理
2.1 核心架构设计
Easy Work采用分层架构设计,极致解耦,完美兼容Spring生态,整体架构如下:

各层核心职责:
- 业务应用层:对接业务系统,发起流程、处理任务、查询流程数据
- 核心API层:提供极简的统一操作入口,屏蔽底层复杂实现
- 核心功能模块:流程定义、实例、任务、扩展四大核心模块,覆盖全流程生命周期
- 持久化层:基于MyBatis-Plus实现数据持久化,兼容MySQL等主流数据库
- 数据存储层:MySQL 8.0+存储流程与业务数据,支持自动建表
2.2 底层核心原理(通俗讲透)
流程引擎的本质,是对业务状态流转的标准化管控。Easy Work的底层核心是「流程定义+状态机+动作执行」三层模型,用通俗的话讲:
- 流程定义层:相当于流程的「施工图纸」,是静态模板,定义了流程有哪些节点、节点间的流转规则、每个节点的执行动作、审批人、监听器等,一个流程定义可生成无数个流程实例。
- 状态机核心层:相当于流程的「大脑」,核心逻辑是「当前状态+触发动作=下一个状态」,负责校验流转合法性、触发节点执行动作、管理流程全生命周期,杜绝非法流转。
- 动作执行层:相当于流程的「手脚」,负责执行节点绑定的业务逻辑、审批逻辑、条件判断、监听器触发等,是流程引擎与业务系统的核心对接点。
2.3 核心概念与易混淆点明确区分
| 概念 | 核心定义 | 本质 | 易混淆点澄清 |
|---|---|---|---|
| 流程定义(ProcessDefinition) | 流程的静态模板,定义了流程的全链路规则 | 相当于Java中的类 | 流程定义修改后,已启动的流程实例不会生效,需升级版本号 |
| 流程实例(ProcessInstance) | 基于流程定义启动的具体流程,是流程的一次执行 | 相当于Java中的对象 | 一个流程定义可对应无数个流程实例,实例之间相互隔离 |
| 流程节点(ProcessNode) | 流程中的单个步骤,是流程的最小执行单元 | 相当于类中的方法 | 节点是静态定义,任务是节点运行时的动态实例 |
| 任务(TaskInfo) | 流程实例运行到用户任务节点时生成的待办事项 | 相当于方法的执行实例 | 只有USER_TASK类型节点会生成任务,其他节点自动执行 |
| 流转规则(Transition) | 节点之间的跳转规则,分为无条件流转和条件流转 | 相当于代码中的if-else/跳转逻辑 | 排他网关必须设置默认分支,否则会出现流程卡死 |
| 节点监听器(TaskListener) | 节点事件触发时执行的自定义逻辑,支持创建/完成/取消事件 | 相当于切面AOP | 监听器异常会阻断流程流转,非核心逻辑需做好异常处理 |
三、5分钟快速搭建可运行的Easy Work项目
3.1 环境要求
- JDK 17+
- Spring Boot 3.x
- MySQL 8.0+
- Maven 3.8+
3.2 Maven依赖配置
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.3</version> <relativePath/> </parent> <groupId>com.jam</groupId> <artifactId>easy-work-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>easy-work-demo</name> <description>Easy Work流程引擎演示项目</description> <properties> <java.version>17</java.version> <easy-work.version>2.2.0</easy-work.version> <mybatis-plus.version>3.5.7</mybatis-plus.version> <fastjson2.version>2.0.52</fastjson2.version> <guava.version>33.2.1-jre</guava.version> <springdoc.version>2.6.0</springdoc.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>com.william</groupId> <artifactId>easy-work-spring-boot-starter</artifactId> <version>${easy-work.version}</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.34</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>${fastjson2.version}</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>${guava.version}</version> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>${springdoc.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project> 3.3 应用配置文件application.yml
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/easy_work_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: root transaction: default-timeout: 30 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml type-aliases-package: com.jam.demo.entity configuration: map-underscore-to-camel-case: true cache-enabled: false log-impl: org.apache.ibatis.logging.stdout.StdOutImpl easy-work: auto-ddl: true table-prefix: ew_ async-execute: false springdoc: api-docs: enabled: true path: /v3/api-docs swagger-ui: enabled: true path: /swagger-ui.html tags-sorter: alpha operations-sorter: alpha 3.4 数据库初始化
- 创建数据库(MySQL 8.0+)
CREATE DATABASE IF NOT EXISTS easy_work_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - 业务表创建(请假申请表)
CREATE TABLE IF NOT EXISTS `t_leave_apply` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `process_instance_id` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '流程实例ID', `user_id` bigint NOT NULL COMMENT '申请人ID', `user_name` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '申请人姓名', `dept_id` bigint NOT NULL COMMENT '部门ID', `leave_type` tinyint NOT NULL COMMENT '请假类型:1-事假,2-病假,3-年假', `start_time` datetime NOT NULL COMMENT '请假开始时间', `end_time` datetime NOT NULL COMMENT '请假结束时间', `leave_days` decimal(10,1) NOT NULL COMMENT '请假天数', `leave_reason` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '请假原因', `status` tinyint NOT NULL DEFAULT '0' COMMENT '申请状态:0-草稿,1-审批中,2-已通过,3-已驳回', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_process_instance_id` (`process_instance_id`), KEY `idx_user_id` (`user_id`), KEY `idx_status` (`status`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='请假申请表'; - Easy Work核心表:开启
auto-ddl: true后,项目启动时会自动创建,无需手动执行。
3.5 项目启动类
package com.jam.demo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Easy Work演示项目启动类 * @author ken */ @SpringBootApplication @MapperScan("com.jam.demo.mapper") public class EasyWorkDemoApplication { public static void main(String[] args) { SpringApplication.run(EasyWorkDemoApplication.class, args); } } 四、完整实战示例:员工请假审批流程
4.1 流程设计
请假审批流程是企业最常用的流程场景,流程链路如下:

4.2 流程节点枚举定义
package com.jam.demo.enums; import lombok.Getter; /** * 请假流程节点枚举 * @author ken */ @Getter public enum LeaveProcessNodeEnum { START("start", "发起请假申请"), MANAGER_APPROVE("manager_approve", "部门经理审批"), HR_RECORD("hr_record", "人事备案"), END("end", "流程结束"); private final String nodeCode; private final String nodeName; LeaveProcessNodeEnum(String nodeCode, String nodeName) { this.nodeCode = nodeCode; this.nodeName = nodeName; } } 4.3 流程定义配置
package com.jam.demo.config; import com.jam.demo.enums.LeaveProcessNodeEnum; import com.william.easywork.definition.ProcessDefinition; import com.william.easywork.definition.ProcessNode; import com.william.easywork.enums.NodeTypeEnum; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 请假流程定义配置 * @author ken */ @Configuration public class LeaveProcessConfig { /** * 请假审批流程定义 * @return 流程定义对象 */ @Bean public ProcessDefinition leaveProcessDefinition() { return ProcessDefinition.builder() .processCode("leave_apply_process") .processName("员工请假审批流程") .version(1) .description("员工请假申请,部门经理审批,人事备案的标准流程") .startNodeCode(LeaveProcessNodeEnum.START.getNodeCode()) .endNodeCode(LeaveProcessNodeEnum.END.getNodeCode()) .addNode(buildStartNode()) .addNode(buildManagerApproveNode()) .addNode(buildHrRecordNode()) .addNode(buildEndNode()) .build(); } /** * 构建发起申请节点 * @return 流程节点对象 */ private ProcessNode buildStartNode() { return ProcessNode.builder() .nodeCode(LeaveProcessNodeEnum.START.getNodeCode()) .nodeName(LeaveProcessNodeEnum.START.getNodeName()) .nodeType(NodeTypeEnum.START) .addTransition(LeaveProcessNodeEnum.MANAGER_APPROVE.getNodeCode()) .build(); } /** * 构建部门经理审批节点 * @return 流程节点对象 */ private ProcessNode buildManagerApproveNode() { return ProcessNode.builder() .nodeCode(LeaveProcessNodeEnum.MANAGER_APPROVE.getNodeCode()) .nodeName(LeaveProcessNodeEnum.MANAGER_APPROVE.getNodeName()) .nodeType(NodeTypeEnum.USER_TASK) .addTransition("pass", LeaveProcessNodeEnum.HR_RECORD.getNodeCode()) .addTransition("reject", LeaveProcessNodeEnum.END.getNodeCode()) .build(); } /** * 构建人事备案节点 * @return 流程节点对象 */ private ProcessNode buildHrRecordNode() { return ProcessNode.builder() .nodeCode(LeaveProcessNodeEnum.HR_RECORD.getNodeCode()) .nodeName(LeaveProcessNodeEnum.HR_RECORD.getNodeName()) .nodeType(NodeTypeEnum.SERVICE_TASK) .addTransition(LeaveProcessNodeEnum.END.getNodeCode()) .build(); } /** * 构建结束节点 * @return 流程节点对象 */ private ProcessNode buildEndNode() { return ProcessNode.builder() .nodeCode(LeaveProcessNodeEnum.END.getNodeCode()) .nodeName(LeaveProcessNodeEnum.END.getNodeName()) .nodeType(NodeTypeEnum.END) .build(); } } 4.4 业务实体与数据层
4.4.1 请假申请实体类
package com.jam.demo.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDateTime; /** * 请假申请实体类 * @author ken */ @Data @TableName("t_leave_apply") @Schema(description = "请假申请实体") public class LeaveApply implements Serializable { private static final long serialVersionUID = 1L; @TableId(type = IdType.AUTO) @Schema(description = "主键ID") private Long id; @Schema(description = "流程实例ID") private String processInstanceId; @Schema(description = "申请人ID") private Long userId; @Schema(description = "申请人姓名") private String userName; @Schema(description = "部门ID") private Long deptId; @Schema(description = "请假类型:1-事假,2-病假,3-年假") private Integer leaveType; @Schema(description = "请假开始时间") private LocalDateTime startTime; @Schema(description = "请假结束时间") private LocalDateTime endTime; @Schema(description = "请假天数") private BigDecimal leaveDays; @Schema(description = "请假原因") private String leaveReason; @Schema(description = "申请状态:0-草稿,1-审批中,2-已通过,3-已驳回") private Integer status; @Schema(description = "创建时间") private LocalDateTime createTime; @Schema(description = "更新时间") private LocalDateTime updateTime; } 4.4.2 Mapper接口
package com.jam.demo.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.jam.demo.entity.LeaveApply; import org.apache.ibatis.annotations.Mapper; /** * 请假申请Mapper接口 * @author ken */ @Mapper public interface LeaveApplyMapper extends BaseMapper<LeaveApply> { } 4.5 请求参数定义
4.5.1 请假申请请求参数
package com.jam.demo.req; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; /** * 请假申请请求参数 * @author ken */ @Data @Schema(description = "请假申请请求参数") public class LeaveApplyReq { @Schema(description = "申请人ID", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "申请人ID不能为空") private Long userId; @Schema(description = "申请人姓名", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(message = "申请人姓名不能为空") private String userName; @Schema(description = "部门ID", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "部门ID不能为空") private Long deptId; @Schema(description = "请假类型:1-事假,2-病假,3-年假", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "请假类型不能为空") private Integer leaveType; @Schema(description = "请假开始时间", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "请假开始时间不能为空") private LocalDateTime startTime; @Schema(description = "请假结束时间", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "请假结束时间不能为空") private LocalDateTime endTime; @Schema(description = "请假天数", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "请假天数不能为空") private BigDecimal leaveDays; @Schema(description = "请假原因", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(message = "请假原因不能为空") private String leaveReason; } 4.5.2 审批请求参数
package com.jam.demo.req; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; /** * 请假审批请求参数 * @author ken */ @Data @Schema(description = "请假审批请求参数") public class LeaveApproveReq { @Schema(description = "申请ID", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "申请ID不能为空") private Long applyId; @Schema(description = "审批人ID", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "审批人ID不能为空") private Long approveUserId; @Schema(description = "审批人姓名", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(message = "审批人姓名不能为空") private String approveUserName; @Schema(description = "审批结果:pass-通过,reject-驳回", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(message = "审批结果不能为空") private String approveResult; @Schema(description = "审批意见") private String approveRemark; } 4.6 业务服务层实现
4.6.1 服务接口
package com.jam.demo.service; import com.baomidou.mybatisplus.extension.service.IService; import com.jam.demo.entity.LeaveApply; import com.jam.demo.req.LeaveApplyReq; import com.jam.demo.req.LeaveApproveReq; /** * 请假申请服务接口 * @author ken */ public interface LeaveApplyService extends IService<LeaveApply> { /** * 发起请假申请 * @param req 请假申请请求参数 * @return 申请ID */ Long submitLeaveApply(LeaveApplyReq req); /** * 部门经理审批 * @param req 审批请求参数 */ void managerApprove(LeaveApproveReq req); /** * 人事备案 * @param processInstanceId 流程实例ID */ void hrRecord(String processInstanceId); } 4.6.2 服务实现类
package com.jam.demo.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.google.common.collect.Maps; import com.jam.demo.entity.LeaveApply; import com.jam.demo.enums.LeaveProcessNodeEnum; import com.jam.demo.mapper.LeaveApplyMapper; import com.jam.demo.req.LeaveApplyReq; import com.jam.demo.req.LeaveApproveReq; import com.jam.demo.service.LeaveApplyService; import com.william.easywork.core.ProcessEngine; import com.william.easywork.entity.ProcessInstance; import com.william.easywork.entity.TaskInfo; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Map; /** * 请假申请服务实现类 * @author ken */ @Slf4j @Service public class LeaveApplyServiceImpl extends ServiceImpl<LeaveApplyMapper, LeaveApply> implements LeaveApplyService { private final ProcessEngine processEngine; private final PlatformTransactionManager transactionManager; private final LeaveApplyMapper leaveApplyMapper; public LeaveApplyServiceImpl(ProcessEngine processEngine, PlatformTransactionManager transactionManager, LeaveApplyMapper leaveApplyMapper) { this.processEngine = processEngine; this.transactionManager = transactionManager; this.leaveApplyMapper = leaveApplyMapper; } @Override public Long submitLeaveApply(LeaveApplyReq req) { DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition(); transactionDefinition.setTimeout(30); TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition); try { if (ObjectUtils.isEmpty(req)) { throw new IllegalArgumentException("请假申请参数不能为空"); } if (!StringUtils.hasText(req.getUserName())) { throw new IllegalArgumentException("申请人姓名不能为空"); } if (req.getLeaveDays() == null || req.getLeaveDays().compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("请假天数必须大于0"); } LeaveApply leaveApply = new LeaveApply(); leaveApply.setUserId(req.getUserId()); leaveApply.setUserName(req.getUserName()); leaveApply.setDeptId(req.getDeptId()); leaveApply.setLeaveType(req.getLeaveType()); leaveApply.setStartTime(req.getStartTime()); leaveApply.setEndTime(req.getEndTime()); leaveApply.setLeaveDays(req.getLeaveDays()); leaveApply.setLeaveReason(req.getLeaveReason()); leaveApply.setStatus(1); leaveApply.setCreateTime(LocalDateTime.now()); leaveApply.setUpdateTime(LocalDateTime.now()); leaveApplyMapper.insert(leaveApply); Map<String, Object> variables = Maps.newHashMap(); variables.put("applyId", leaveApply.getId()); variables.put("userId", req.getUserId()); variables.put("userName", req.getUserName()); variables.put("deptId", req.getDeptId()); ProcessInstance processInstance = processEngine.getRuntimeService() .startProcessInstanceByCode("leave_apply_process", variables); leaveApply.setProcessInstanceId(processInstance.getInstanceId()); leaveApplyMapper.updateById(leaveApply); transactionManager.commit(transactionStatus); log.info("请假申请提交成功,申请ID:{},流程实例ID:{}", leaveApply.getId(), processInstance.getInstanceId()); return leaveApply.getId(); } catch (Exception e) { transactionManager.rollback(transactionStatus); log.error("请假申请提交失败,参数:{}", req, e); throw new RuntimeException("请假申请提交失败:" + e.getMessage(), e); } } @Override public void managerApprove(LeaveApproveReq req) { DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition(); transactionDefinition.setTimeout(30); TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition); try { if (ObjectUtils.isEmpty(req)) { throw new IllegalArgumentException("审批参数不能为空"); } if (!StringUtils.hasText(req.getApproveResult())) { throw new IllegalArgumentException("审批结果不能为空"); } if (!"pass".equals(req.getApproveResult()) && !"reject".equals(req.getApproveResult())) { throw new IllegalArgumentException("审批结果只能是pass或reject"); } LeaveApply leaveApply = leaveApplyMapper.selectById(req.getApplyId()); if (ObjectUtils.isEmpty(leaveApply)) { throw new IllegalArgumentException("请假申请不存在"); } if (leaveApply.getStatus() != 1) { throw new IllegalArgumentException("申请状态异常,无法审批"); } if (!StringUtils.hasText(leaveApply.getProcessInstanceId())) { throw new IllegalArgumentException("流程实例ID不存在"); } TaskInfo taskInfo = processEngine.getTaskService() .getCurrentTaskByInstanceId(leaveApply.getProcessInstanceId()); if (ObjectUtils.isEmpty(taskInfo)) { throw new IllegalArgumentException("当前没有待审批任务"); } if (!LeaveProcessNodeEnum.MANAGER_APPROVE.getNodeCode().equals(taskInfo.getNodeCode())) { throw new IllegalArgumentException("当前节点不是部门经理审批节点"); } Map<String, Object> variables = Maps.newHashMap(); variables.put("approveUserId", req.getApproveUserId()); variables.put("approveUserName", req.getApproveUserName()); variables.put("approveResult", req.getApproveResult()); variables.put("approveRemark", req.getApproveRemark()); processEngine.getTaskService() .completeTask(taskInfo.getTaskId(), req.getApproveResult(), variables); if ("pass".equals(req.getApproveResult())) { leaveApply.setStatus(2); } else { leaveApply.setStatus(3); } leaveApply.setUpdateTime(LocalDateTime.now()); leaveApplyMapper.updateById(leaveApply); transactionManager.commit(transactionStatus); log.info("部门经理审批完成,申请ID:{},审批结果:{}", req.getApplyId(), req.getApproveResult()); } catch (Exception e) { transactionManager.rollback(transactionStatus); log.error("部门经理审批失败,参数:{}", req, e); throw new RuntimeException("部门经理审批失败:" + e.getMessage(), e); } } @Override public void hrRecord(String processInstanceId) { DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition(); transactionDefinition.setTimeout(30); TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition); try { if (!StringUtils.hasText(processInstanceId)) { throw new IllegalArgumentException("流程实例ID不能为空"); } LeaveApply leaveApply = lambdaQuery() .eq(LeaveApply::getProcessInstanceId, processInstanceId) .one(); if (ObjectUtils.isEmpty(leaveApply)) { throw new IllegalArgumentException("请假申请不存在"); } TaskInfo taskInfo = processEngine.getTaskService() .getCurrentTaskByInstanceId(processInstanceId); if (ObjectUtils.isEmpty(taskInfo)) { throw new IllegalArgumentException("当前没有待处理任务"); } if (!LeaveProcessNodeEnum.HR_RECORD.getNodeCode().equals(taskInfo.getNodeCode())) { throw new IllegalArgumentException("当前节点不是人事备案节点"); } processEngine.getTaskService().completeTask(taskInfo.getTaskId()); leaveApply.setStatus(2); leaveApply.setUpdateTime(LocalDateTime.now()); leaveApplyMapper.updateById(leaveApply); transactionManager.commit(transactionStatus); log.info("人事备案完成,流程实例ID:{}", processInstanceId); } catch (Exception e) { transactionManager.rollback(transactionStatus); log.error("人事备案失败,流程实例ID:{}", processInstanceId, e); throw new RuntimeException("人事备案失败:" + e.getMessage(), e); } } } 4.7 自动执行监听器(人事备案节点)
package com.jam.demo.listener; import com.jam.demo.service.LeaveApplyService; import com.william.easywork.annotation.NodeListener; import com.william.easywork.enums.ListenerEventEnum; import com.william.easywork.listener.NodeTaskListener; import com.william.easywork.model.ListenerContext; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; /** * 人事备案节点任务监听器 * @author ken */ @Slf4j @Component @NodeListener(nodeCode = "hr_record", event = ListenerEventEnum.TASK_CREATE) public class HrRecordTaskListener implements NodeTaskListener { private final LeaveApplyService leaveApplyService; public HrRecordTaskListener(LeaveApplyService leaveApplyService) { this.leaveApplyService = leaveApplyService; } @Override public void onEvent(ListenerContext context) { log.info("人事备案节点监听器触发,流程实例ID:{}", context.getInstanceId()); try { String processInstanceId = context.getInstanceId(); if (!StringUtils.hasText(processInstanceId)) { log.error("流程实例ID为空,无法执行人事备案"); return; } leaveApplyService.hrRecord(processInstanceId); log.info("人事备案自动执行完成,流程实例ID:{}", processInstanceId); } catch (Exception e) { log.error("人事备案自动执行失败,流程实例ID:{}", context.getInstanceId(), e); throw new RuntimeException("人事备案执行失败:" + e.getMessage(), e); } } } 4.8 接口Controller层
package com.jam.demo.controller; import com.jam.demo.req.LeaveApplyReq; import com.jam.demo.req.LeaveApproveReq; import com.jam.demo.service.LeaveApplyService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; /** * 请假申请Controller * @author ken */ @RestController @RequestMapping("/leave/apply") @Tag(name = "请假申请管理", description = "请假申请流程相关接口") public class LeaveApplyController { private final LeaveApplyService leaveApplyService; public LeaveApplyController(LeaveApplyService leaveApplyService) { this.leaveApplyService = leaveApplyService; } @PostMapping("/submit") @Operation(summary = "发起请假申请", description = "提交请假申请并启动流程实例") public ResponseEntity<Long> submitLeaveApply(@Valid @RequestBody LeaveApplyReq req) { Long applyId = leaveApplyService.submitLeaveApply(req); return ResponseEntity.ok(applyId); } @PostMapping("/manager/approve") @Operation(summary = "部门经理审批", description = "部门经理对请假申请进行审批") public ResponseEntity<Void> managerApprove(@Valid @RequestBody LeaveApproveReq req) { leaveApplyService.managerApprove(req); return ResponseEntity.ok().build(); } @PostMapping("/hr/record") @Operation(summary = "人事备案", description = "人事对审批通过的请假申请进行备案") public ResponseEntity<Void> hrRecord(@RequestParam String processInstanceId) { leaveApplyService.hrRecord(processInstanceId); return ResponseEntity.ok().build(); } } 五、生产级最佳实践
5.1 事务控制最佳实践
- 优先使用编程式事务:流程引擎操作与业务操作必须在同一个事务中,编程式事务可精准控制事务边界,避免声明式事务的失效场景(如非public方法、异常被捕获)。
- 事务超时控制:核心流程操作设置30s以内的超时时间,避免长事务占用数据库连接,影响系统性能。
- 事务回滚兜底:所有流程操作必须捕获异常,异常时强制回滚事务,杜绝业务数据与流程数据不一致的问题。
5.2 性能优化最佳实践
- 索引优化:对流程表的
process_code、instance_id、node_code、create_time字段建立联合索引,业务表的流程实例ID字段必须建立索引。 - 缓存优化:开启Easy Work的流程定义缓存,避免每次启动流程实例都查询数据库;高频查询的待办任务可加入二级缓存。
- 异步解耦:非核心逻辑(如消息通知、日志记录、第三方接口调用)使用异步执行,避免阻塞主流程,提升接口响应速度。
- 历史数据归档:对已完成超过3个月的流程实例,定期归档到历史表,避免主表数据量过大导致查询性能下降。
5.3 分布式场景最佳实践
- 分布式锁控制:集群部署时,同一个流程实例的操作必须加分布式锁,避免多个节点同时操作导致数据不一致。
- 异步任务可靠性:异步执行的流程任务,使用消息队列(RocketMQ/Kafka)实现,避免异步任务丢失。
- 数据库高可用:流程引擎数据库必须使用主从复制+读写分离,避免单点故障导致流程系统不可用。
六、常见坑与避坑指南
- 流程定义修改后,已启动的流程实例不生效
- 原因:流程实例启动时会复制当前版本的流程定义数据,后续修改不会影响已启动的实例
- 避坑:修改流程定义后必须升级版本号,新启动的实例使用新版本,已启动的实例继续使用旧版本
- 排他网关无匹配分支,流程卡死
- 原因:条件表达式编写错误,或未设置默认分支,导致网关没有匹配到任何流转路径
- 避坑:所有排他网关必须设置默认分支,条件表达式覆盖所有可能场景,上线前必须全场景测试
- 监听器异常阻断流程流转
- 原因:监听器中的业务逻辑抛出未捕获的异常,导致任务完成失败,流程无法继续流转
- 避坑:非核心逻辑的异常必须捕获,不影响主流程;核心逻辑异常必须抛出,触发事务回滚,同时做好告警通知
- 事务失效导致数据不一致
- 原因:使用声明式事务,流程操作与业务操作不在同一个事务中,或异常被捕获未触发回滚
- 避坑:核心流程操作优先使用编程式事务,确保流程与业务操作的原子性,异常必须向上抛出
七、总结
Easy Work作为一款国产轻量级Java流程引擎,以极简的设计、极低的学习成本、极强的扩展性,完美解决了中小微型流程场景的痛点。它剥离了传统流程引擎的冗余特性,保留了核心的流程编排能力,让开发者可以在半小时内完成流程的设计、开发与上线。