吃透 Java 轻量级流程引擎 Easy Work:从核心原理到生产级落地全指南

简介: Easy Work是一款开源轻量级Java流程引擎,基于状态机设计,摒弃BPMN复杂特性,学习成本降90%、性能提升3倍以上,专为中小微流程场景优化,5分钟即可快速集成上线。

一、为什么你需要轻量级流程引擎?

在企业级开发中,审批流、工单流转、状态机管控等流程类需求无处不在。传统重量级流程引擎(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的底层核心是「流程定义+状态机+动作执行」三层模型,用通俗的话讲:

  1. 流程定义层:相当于流程的「施工图纸」,是静态模板,定义了流程有哪些节点、节点间的流转规则、每个节点的执行动作、审批人、监听器等,一个流程定义可生成无数个流程实例。
  2. 状态机核心层:相当于流程的「大脑」,核心逻辑是「当前状态+触发动作=下一个状态」,负责校验流转合法性、触发节点执行动作、管理流程全生命周期,杜绝非法流转。
  3. 动作执行层:相当于流程的「手脚」,负责执行节点绑定的业务逻辑、审批逻辑、条件判断、监听器触发等,是流程引擎与业务系统的核心对接点。

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 数据库初始化

  1. 创建数据库(MySQL 8.0+)

CREATE DATABASE IF NOT EXISTS easy_work_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

  1. 业务表创建(请假申请表)

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='请假申请表';

  1. 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 事务控制最佳实践

  1. 优先使用编程式事务:流程引擎操作与业务操作必须在同一个事务中,编程式事务可精准控制事务边界,避免声明式事务的失效场景(如非public方法、异常被捕获)。
  2. 事务超时控制:核心流程操作设置30s以内的超时时间,避免长事务占用数据库连接,影响系统性能。
  3. 事务回滚兜底:所有流程操作必须捕获异常,异常时强制回滚事务,杜绝业务数据与流程数据不一致的问题。

5.2 性能优化最佳实践

  1. 索引优化:对流程表的process_codeinstance_idnode_codecreate_time字段建立联合索引,业务表的流程实例ID字段必须建立索引。
  2. 缓存优化:开启Easy Work的流程定义缓存,避免每次启动流程实例都查询数据库;高频查询的待办任务可加入二级缓存。
  3. 异步解耦:非核心逻辑(如消息通知、日志记录、第三方接口调用)使用异步执行,避免阻塞主流程,提升接口响应速度。
  4. 历史数据归档:对已完成超过3个月的流程实例,定期归档到历史表,避免主表数据量过大导致查询性能下降。

5.3 分布式场景最佳实践

  1. 分布式锁控制:集群部署时,同一个流程实例的操作必须加分布式锁,避免多个节点同时操作导致数据不一致。
  2. 异步任务可靠性:异步执行的流程任务,使用消息队列(RocketMQ/Kafka)实现,避免异步任务丢失。
  3. 数据库高可用:流程引擎数据库必须使用主从复制+读写分离,避免单点故障导致流程系统不可用。

六、常见坑与避坑指南

  1. 流程定义修改后,已启动的流程实例不生效
  • 原因:流程实例启动时会复制当前版本的流程定义数据,后续修改不会影响已启动的实例
  • 避坑:修改流程定义后必须升级版本号,新启动的实例使用新版本,已启动的实例继续使用旧版本
  1. 排他网关无匹配分支,流程卡死
  • 原因:条件表达式编写错误,或未设置默认分支,导致网关没有匹配到任何流转路径
  • 避坑:所有排他网关必须设置默认分支,条件表达式覆盖所有可能场景,上线前必须全场景测试
  1. 监听器异常阻断流程流转
  • 原因:监听器中的业务逻辑抛出未捕获的异常,导致任务完成失败,流程无法继续流转
  • 避坑:非核心逻辑的异常必须捕获,不影响主流程;核心逻辑异常必须抛出,触发事务回滚,同时做好告警通知
  1. 事务失效导致数据不一致
  • 原因:使用声明式事务,流程操作与业务操作不在同一个事务中,或异常被捕获未触发回滚
  • 避坑:核心流程操作优先使用编程式事务,确保流程与业务操作的原子性,异常必须向上抛出

七、总结

Easy Work作为一款国产轻量级Java流程引擎,以极简的设计、极低的学习成本、极强的扩展性,完美解决了中小微型流程场景的痛点。它剥离了传统流程引擎的冗余特性,保留了核心的流程编排能力,让开发者可以在半小时内完成流程的设计、开发与上线。

目录
相关文章
|
23天前
|
算法 数据可视化 Java
Java 规则引擎封神指南:从底层原理到生产落地,零冗余全干货实战
规则引擎解耦业务规则与系统代码,实现规则快速迭代。本文系统讲解规则引擎原理与实战,对比Drools、EasyRules等主流方案,剖析Rete算法核心逻辑。通过电商风控系统实战,展示生产级规则引擎架构,包含规则持久化、动态热更新等关键功能。总结性能优化、规则治理等最佳实践,解答常见问题,帮助开发者掌握规则引擎选型与落地。规则引擎适用于规则频繁变更场景,能将规则迭代周期从天级压缩到分钟级,但需根据业务复杂度合理选用。
214 2
|
1月前
|
人工智能 API 机器人
OpenClaw 用户部署和使用指南汇总
本文档为OpenClaw(原MoltBot)官方使用指南,涵盖一键部署(阿里云轻量服务器年仅68元)、钉钉/飞书/企微等多平台AI员工搭建、典型场景实践及高频问题FAQ。同步更新产品化修复进展,助力用户高效落地7×24小时主动执行AI助手。
21706 130
|
缓存 NoSQL Java
【JetCache】JetCache的使用方法与步骤
【JetCache】JetCache的使用方法与步骤
8740 1
|
23天前
|
人工智能 监控 API
阿里云及Windows本地部署OpenClaw skill:AI Agent全自动炒股票,重构量化交易逻辑实战指南
2026年,AI Agent领域最震撼的突破来自OpenClaw(原Clawdbot)——这个能自主规划、执行任务的智能体,用50美元启动资金创造了48小时滚雪球至2980美元的奇迹,收益率高达5860%。其核心逻辑堪称教科书级:每10分钟扫描Polymarket近千个预测市场,借助多模型深度推理,交叉验证多维度信息,捕捉8%以上的定价偏差,再通过凯利准则将单仓位严格控制在总资金6%以内,实现低风险高频套利。
825 1
|
4天前
|
SQL 关系型数据库 Java
吃透 Seata 分布式事务:原理拆解 + 生产级落地 + 全场景避坑实战
本文深度解析阿里开源分布式事务框架Seata:剖析TC/TM/RM三大角色与全局事务流程,详解AT(零侵入)、TCC(强控制)、SAGA(长事务)、XA(强一致)四大模式原理、适用场景及核心对比,并通过电商下单实战演示AT模式落地,最后系统梳理生产环境高可用、SQL限制、幂等处理、XID传播等全链路避坑指南。
93 3
|
25天前
|
机器学习/深度学习 自然语言处理 并行计算
大模型应用:混合专家模型(MoE):大模型性能提升的关键技术拆解.37
MoE(混合专家模型)是一种高效大模型架构,通过“智能调度+稀疏激活”机制,让多个专业化子网络(专家)按需协作。它兼顾性能与效率:参数规模大但推理仅激活2-4个专家,显著降本提速;既保持通用能力,又在医疗、法律等细分领域更专精,是当前大模型落地的关键技术。
503 17
|
23天前
|
安全 开发工具 git
别再瞎用 Git 合并了!Merge vs Rebase 底层逻辑、适用场景与零坑操作全指南
本文深度解析Git中Merge与Rebase的本质区别:Merge安全可追溯,适合公共分支合并;Rebase线性整洁,仅限本地私有分支整理。从底层对象模型出发,结合实战示例与企业级最佳实践,厘清使用红线、避坑误区,助你彻底掌握分支合并决策逻辑。(239字)
416 1
|
23天前
|
缓存 Java 数据安全/隐私保护
吃透 Spring 12 个核心扩展点:从源码底层到生产级实战,90% 的高级开发都在用
本文系统详解Spring 12个核心扩展点,覆盖Bean生命周期全阶段:从BeanDefinition注册(BeanDefinitionRegistryPostProcessor)、实例化控制(InstantiationAwareBeanPostProcessor),到初始化(@PostConstruct/InitializingBean)、AOP代理(BeanPostProcessor)、事务同步(TransactionSynchronization)及容器关闭(DisposableBean)等,配实战代码与对比表格,助你深入掌握Spring扩展本质。
106 3
|
27天前
|
消息中间件 人工智能 边缘计算
空地协同让电力巡检更智能 ——从人工攀爬到立体监测的技术演进
这是一套“空地协同”智能电力巡检系统:无人机搭载RTK+20倍变焦相机自主巡线,工程车集成AI边缘计算(Jetson AGX Orin)与红外检测,实现厘米级定位、小目标识别(92%+准确率)、无网环境本地分析+断点续传。系统已落地应用,显著降低人工登塔风险。
105 4