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

吃透 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的底层核心是「流程定义+状态机+动作执行」三层模型,用通俗的话讲:

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

七、总结

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

Read more

Godot被AI代码“围攻”!维护者崩溃发声:“不知道还能坚持多久”

Godot被AI代码“围攻”!维护者崩溃发声:“不知道还能坚持多久”

整理 | 郑丽媛 出品 | ZEEKLOG(ID:ZEEKLOGnews) 当大模型能在几秒钟内生成一段“看起来像那么回事”的补丁时,开源社区却开始付出另一种代价。 最近,开源游戏引擎 Godot 的核心维护团队公开吐槽:他们正被大量“AI 生成的低质量代码”淹没。那些代码往往结构完整、注释齐全、描述洋洋洒洒,但真正的问题是——提交者可能并不理解自己交上来的内容。 这件事,并不是简单的“有人偷懒用 AI 写代码”。它正在触及开源协作最核心的东西:信任。 一场悄无声息的“AI 洪水” 事情的导火索来自一条 Bluesky 讨论帖。 Godot 主要维护者之一、同时也是 Godot 商业支持公司 W4 Games 联合创始人的 Rémi Verschelde 表示,所谓的“AI slop”

By Ne0inhk
诺奖得主辛顿最新访谈:1 万个 AI 可以瞬间共享同一份“灵魂”,这就是为什么人类注定被超越

诺奖得主辛顿最新访谈:1 万个 AI 可以瞬间共享同一份“灵魂”,这就是为什么人类注定被超越

当宇宙级的“嘴炮”遇到降维打击。 编译 | 王启隆 来源 | youtu.be/l6ZcFa8pybE 出品丨AI 科技大本营(ID:rgznai100) 打开最新一期知名播客 StarTalk 的 YouTube 评论区,最高赞的一条留言是这样写的: “我长这么大,第一次看到尼尔·德葛司·泰森(Neil deGrasse Tyson)在一档节目里几乎全程闭嘴,像个手足无措的小学生一样乖乖听讲。” 作为全美最知名的天体物理学家,泰森平时的画风是充满激情、喋喋不休、用宇宙的宏大来震撼嘉宾。但这一次,坐在他对面的那位满头银发、带着温和英音的英国老人,仅仅用最平淡的语气,就让整个演播室陷入了数次令人窒息的沉默。 这位老人是 Geoffrey Hinton。深度学习三巨头之一,2024 年诺贝尔物理学奖得主,被公认为“AI 教父”。 对经常阅读 Hinton 演讲的我来说,这也是比较新奇的一幕—

By Ne0inhk
48小时“烧光”56万!三人创业团队濒临破产,仅因Gemini API密钥被盗:“AI账单远超我们的银行余额”

48小时“烧光”56万!三人创业团队濒临破产,仅因Gemini API密钥被盗:“AI账单远超我们的银行余额”

整理 | 苏宓 出品 | ZEEKLOG(ID:ZEEKLOGnews) 「仅过了 48 小时,一笔 8.2 万美元的天价费用凭空出现,较这家小型初创公司的正常月费暴涨近 46000%。」 这不是假设的虚幻故事,而是一家墨西哥初创公司正在经历的真实危机。 近日,一位名为 RatonVaquero 的开发者在 Reddit 发帖求助称,由于他的 Gemini API 密钥被盗用,原本每月仅约 180 美元(约 1242 元)的费用,在短短 48 小时内暴涨到 82,314.44 美元(约 56.8 万元)。对于这家只有三名开发者的小型创业团队来说,这笔突如其来的账单,几乎等同于灭顶之灾。 “我现在整个人都处在震惊和恐慌之中。”RatonVaquero

By Ne0inhk
假网站排全网第二,真官网翻五页都找不到!NanoClaw创始人破防:SEO之战,我快要输了

假网站排全网第二,真官网翻五页都找不到!NanoClaw创始人破防:SEO之战,我快要输了

整理 | 苏宓 出品 | ZEEKLOG(ID:ZEEKLOGnews) 自从 OpenClaw 爆火之后,各种“Claw”项目接连出现,其中以安全优化版 NanoClaw 最为知名。它的核心代码仅有 4000 行,却获得了 AI 大牛 Andrej Karpathy 的点赞。 可谁也没想到,这款口碑极佳的开源项目,近来竟被一个仿冒网站抢了风头。 投诉无门之下,NanoClaw 创始人 Gavriel Cohen 在 X 社交平台上无奈发文怒斥:谷歌搜索错误地将假网站排在真官网前面,不仅破坏了项目声誉,还埋下了严重的安全隐患,而他费尽心力,却只能哀叹一句——“我正在为自己的开源项目打 SEO 战,但我快要输了。” 那么,NanoClaw 究竟发生了什么?又是怎么走红的?事情还要从 OpenClaw

By Ne0inhk