一、为什么你需要轻量级流程引擎?
在企业级开发中,审批流、工单流转、状态机管控等流程类需求无处不在。传统重量级流程引擎(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流程引擎,以极简的设计、极低的学习成本、极强的扩展性,完美解决了中小微型流程场景的痛点。它剥离了传统流程引擎的冗余特性,保留了核心的流程编排能力,让开发者可以在半小时内完成流程的设计、开发与上线。