流程步骤模板 - @StepStatus 注解方案

流程步骤模板 - @StepStatus 注解方案

AOP 注解驱动的步骤状态管理,业务方法零侵入,加注解自动更新数据库状态。


核心特性

  • 零侵入:业务方法只加一个注解,不写任何状态更新代码
  • 统一切面:执行前自动更新"XX中",执行成功更新"XX完成",异常更新"失败"
  • 灵活扩展 :每个业务自己定义 StatusUpdater 实现,决定更新哪个表、怎么更新
  • 任务 ID 灵活获取 :从方法参数按名称提取,支持 taskIdid 等任意参数名

1. 通用步骤接口 ProcessStep.java

复制代码

csharp

代码解读

复制代码

/** * 流程步骤通用接口:所有业务的步骤枚举都实现此接口 */ public interface ProcessStep { /** 步骤名称 */ String getStepName(); /** 运行中状态码 */ String getRunningCode(); /** 运行中描述 */ String getRunningDesc(); /** 完成状态码 */ String getFinishCode(); /** 完成描述 */ String getFinishDesc(); /** 失败状态码 */ String getFailCode(); /** 失败描述 */ String getFailDesc(); }


2. 注解定义 @StepStatus.java

复制代码

scss

代码解读

复制代码

import java.lang.annotation.*; ​ /** * 步骤状态注解:标注在业务方法上,AOP 自动在执行前后更新数据库状态。 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface StepStatus { ​ /** * 当前步骤对应的枚举值。 * 业务方传入自己定义的 ProcessStep 实现类。 */ Class<? extends ProcessStep> step(); ​ /** * 方法参数名,从哪个参数提取 taskId。 * 默认 "taskId",也可指定为其他参数名。 */ String taskIdParam() default "taskId"; ​ /** * 状态更新器的实现类,由业务方指定。 * 切面通过 SpringContextHolder 获取该 Bean,调用其 update 方法。 */ Class<? extends StepStatusUpdater> updater(); }


3. 状态更新器接口 StepStatusUpdater.java

复制代码

php

代码解读

复制代码

/** * 状态更新器接口:每个业务场景自行实现,决定更新哪个表、用哪个 Mapper。 */ public interface StepStatusUpdater { ​ /** * 更新任务状态。 * @param taskId 任务ID(从方法参数中提取) * @param statusCode 状态码(来自 ProcessStep 枚举) * @param statusDesc 状态描述 */ void update(Long taskId, String statusCode, String statusDesc); }


4. AOP 切面 StepStatusAspect.java

复制代码

scss

代码解读

复制代码

import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; ​ import java.lang.reflect.Parameter; ​ @Aspect @Component public class StepStatusAspect { ​ /** * 拦截所有标注了 @StepStatus 的方法,在执行前后自动更新状态。 */ @Around("@annotation(stepStatus)") public Object around(ProceedingJoinPoint pjp, StepStatus stepStatus) throws Throwable { Long taskId = extractTaskId(pjp, stepStatus.taskIdParam()); ProcessStep step = resolveStepEnum(pjp, stepStatus.step()); StepStatusUpdater updater = SpringContextHolder.getBean(stepStatus.updater()); ​ // 执行前:更新为运行中 updateStatusWithNewTx(taskId, step.getRunningCode(), step.getRunningDesc(), updater); ​ try { // 执行业务方法 Object result = pjp.proceed(); ​ // 执行成功:更新为完成 updateStatusWithNewTx(taskId, step.getFinishCode(), step.getFinishDesc(), updater); return result; } catch (Exception e) { // 执行异常:更新为失败 updateStatusWithNewTx(taskId, step.getFailCode(), step.getFailDesc(), updater); throw e; } } ​ /** * 从方法参数中提取 taskId。 */ private Long extractTaskId(ProceedingJoinPoint pjp, String paramName) { MethodSignature signature = (MethodSignature) pjp.getSignature(); String[] paramNames = signature.getParameterNames(); Object[] args = pjp.getArgs(); ​ for (int i = 0; i < paramNames.length; i++) { if (paramName.equals(paramNames[i]) && args[i] instanceof Long) { return (Long) args[i]; } } throw new IllegalArgumentException("找不到 taskId 参数: " + paramName); } ​ /** * 从方法参数中提取具体枚举值(通过参数类型匹配枚举类)。 * 如果方法参数中有对应 ProcessStep 类型的枚举,直接取第一个; * 否则抛异常要求业务方在方法签名中声明枚举参数。 */ private ProcessStep resolveStepEnum(ProceedingJoinPoint pjp, Class<? extends ProcessStep> stepClass) { Object[] args = pjp.getArgs(); for (Object arg : args) { if (arg != null && stepClass.isInstance(arg)) { return (ProcessStep) arg; } } throw new IllegalArgumentException( "方法参数中找不到 " + stepClass.getSimpleName() + " 类型的枚举,无法确定当前步骤"); } ​ /** * 独立事务更新状态,避免业务回滚导致状态丢失。 */ @Transactional(propagation = Propagation.REQUIRES_NEW) public void updateStatusWithNewTx(Long taskId, String code, String desc, StepStatusUpdater updater) { updater.update(taskId, code, desc); } }


5. 业务枚举 DataSyncStep.java

复制代码

arduino

代码解读

复制代码

import lombok.AllArgsConstructor; import lombok.Getter; ​ @Getter @AllArgsConstructor public enum DataSyncStep implements ProcessStep { PULL("数据拉取", "SYNC_PULLING", "拉取中", "SYNC_PULL_OK", "拉取完成", "SYNC_PULL_FAIL", "拉取失败"), PARSE("数据解析", "SYNC_PARSING", "解析中", "SYNC_PARSE_OK", "解析完成", "SYNC_PARSE_FAIL", "解析失败"), CALCULATE("指标计算", "SYNC_CALCULATING", "计算中", "SYNC_CALC_OK", "计算完成", "SYNC_CALC_FAIL", "计算失败"), SAVE("结果落库", "SYNC_SAVING", "保存中", "SYNC_SAVE_OK", "保存完成", "SYNC_SAVE_FAIL", "保存失败"); ​ private final String stepName; private final String runningCode; private final String runningDesc; private final String finishCode; private final String finishDesc; private final String failCode; private final String failDesc; }


6. 业务 Updater 实现 DataSyncStatusUpdater.java

复制代码

typescript

代码解读

复制代码

import org.springframework.stereotype.Component; import javax.annotation.Resource; ​ /** * 数据同步业务的状态更新器,决定更新哪个表、用哪个 Mapper。 */ @Component public class DataSyncStatusUpdater implements StepStatusUpdater { ​ @Resource private TaskMapper taskMapper; ​ @Override public void update(Long taskId, String statusCode, String statusDesc) { taskMapper.updateStatus(taskId, statusCode, statusDesc); } }


7. 业务使用示例 DataSyncService.java

复制代码

arduino

代码解读

复制代码

import org.springframework.stereotype.Service; ​ @Service public class DataSyncService { ​ @Resource private ThirdPartyApi thirdPartyApi; ​ /** * 主流程:方法加注解即完成状态更新,方法体内零状态代码。 * 枚举值通过方法参数传入,切面自动从参数中提取。 */ public void syncData(Long taskId) { pullFromThirdParty(taskId, DataSyncStep.PULL); parseRawData(taskId, DataSyncStep.PARSE); Integer result = calculateMetrics(taskId, DataSyncStep.CALCULATE); saveFinalResult(taskId, DataSyncStep.SAVE, result); } ​ @StepStatus(step = DataSyncStep.class, taskIdParam = "taskId", updater = DataSyncStatusUpdater.class) private void pullFromThirdParty(Long taskId, DataSyncStep step) { // 纯业务逻辑,无需关心状态 System.out.println("[业务] 正在从第三方接口拉取原始数据..."); } ​ @StepStatus(step = DataSyncStep.class, taskIdParam = "taskId", updater = DataSyncStatusUpdater.class) private void parseRawData(Long taskId, DataSyncStep step) { System.out.println("[业务] 正在解析原始数据格式..."); } ​ @StepStatus(step = DataSyncStep.class, taskIdParam = "taskId", updater = DataSyncStatusUpdater.class) private Integer calculateMetrics(Long taskId, DataSyncStep step) { System.out.println("[业务] 正在执行数据指标计算..."); return 200; } ​ @StepStatus(step = DataSyncStep.class, taskIdParam = "taskId", updater = DataSyncStatusUpdater.class) private void saveFinalResult(Long taskId, DataSyncStep step, Integer data) { System.out.println("[业务] 正在保存计算结果,数据量:" + data); } }


运行效果

调用 dataSyncService.syncData(1001L) 后输出:

复制代码

ini

代码解读

复制代码

[DB更新] 任务ID:1001 | 状态:SYNC_PULLING | 描述:拉取中 [业务] 正在从第三方接口拉取原始数据... [DB更新] 任务ID:1001 | 状态:SYNC_PULL_OK | 描述:拉取完成 [DB更新] 任务ID:1001 | 状态:SYNC_PARSING | 描述:解析中 [业务] 正在解析原始数据格式... [DB更新] 任务ID:1001 | 状态:SYNC_PARSE_OK | 描述:解析完成 [DB更新] 任务ID:1001 | 状态:SYNC_CALCULATING | 描述:计算中 [业务] 正在执行数据指标计算... [DB更新] 任务ID:1001 | 状态:SYNC_CALC_OK | 描述:计算完成 [DB更新] 任务ID:1001 | 状态:SYNC_SAVING | 描述:保存中 [业务] 正在保存计算结果,数据量:200 [DB更新] 任务ID:1001 | 状态:SYNC_SAVE_OK | 描述:保存完成


设计要点

1. 枚举值从方法参数获取

注解本身不支持传枚举常量(Java 注解限制),所以枚举值作为方法参数传入 ,切面通过 isInstance() 匹配提取:

复制代码

arduino

代码解读

复制代码

// 方法签名中包含枚举参数 private void pullFromThirdParty(Long taskId, DataSyncStep step) // 切面从 args 中找到 DataSyncStep 类型的参数,即为当前步骤

业务方调用时传具体枚举值即可。

2. 每个业务自己决定更新哪个表

StepStatusUpdater 是接口,每个业务场景自己实现:

  • 同步业务 → DataSyncStatusUpdater → 更新 data_sync_task
  • 对账业务 → ReconcileStatusUpdater → 更新 reconcile_task
  • 计算业务 → CalcStatusUpdater → 更新 calc_task

注解通过 updater = xxx.class 指定用哪个实现。

3. 独立事务

updateStatusWithNewTx 使用 REQUIRES_NEW 传播级别,即使业务方法回滚,状态记录仍保留,便于排查问题。


与 StepStatusTemplate 链式方案对比

维度 @StepStatus 注解 StepStatusTemplate 链式
代码侵入性 最低,只加注解 低,改调用方式
状态与业务耦合 完全解耦 轻度耦合(链式编排)
步骤间结果传递 需要局部变量 supply + then 原生支持
灵活性 高(每个方法独立) 中(需要链式编排)
适用场景 方法级状态跟踪 流程级链式编排
事务控制 AOP 内建 需要手动处理

建议:简单流程用注解,复杂流程用链式,两者可共存。


使用注意事项

1. 必须通过 Spring 代理调用才能生效

@StepStatus 基于 Spring AOP 实现,只有经过 Spring 代理的方法调用才会触发切面

正确 ✅ --- 通过 Spring Bean 调用(跨类调用)

复制代码

typescript

代码解读

复制代码

@Service public class DataSyncOrchestrator { ​ @Resource private DataSyncService dataSyncService; // Spring 代理对象 ​ public void orchestrate(Long taskId) { // 跨类调用 → 经过代理 → 注解生效 dataSyncService.pullFromThirdParty(taskId, DataSyncStep.PULL); dataSyncService.parseRawData(taskId, DataSyncStep.PARSE); } }

错误 ❌ --- 同类内部 this 调用(不经过代理)

复制代码

arduino

代码解读

复制代码

@Service public class DataSyncService { public void syncData(Long taskId) { // this.pullFromThirdParty(...) → 直接调用,绕过代理 → 注解不生效! pullFromThirdParty(taskId, DataSyncStep.PULL); parseRawData(taskId, DataSyncStep.PARSE); } @StepStatus(step = DataSyncStep.class, updater = DataSyncStatusUpdater.class) private void pullFromThirdParty(Long taskId, DataSyncStep step) { ... } }

修复方式 --- 注入自己(拿到代理对象)

复制代码

ruby

代码解读

复制代码

@Service public class DataSyncService { @Resource private DataSyncService self; // 注入自己,拿到代理对象 public void syncData(Long taskId) { // self.pullFromThirdParty(...) → 经过代理 → 注解生效 self.pullFromThirdParty(taskId, DataSyncStep.PULL); self.parseRawData(taskId, DataSyncStep.PARSE); } @StepStatus(step = DataSyncStep.class, updater = DataSyncStatusUpdater.class) public void pullFromThirdParty(Long taskId, DataSyncStep step) { ... } }

或者拆成两个 Service 互相调用,天然走代理。

2. 方法必须是 public

Spring AOP 基于 JDK 动态代理或 CGLIB,只有 public 方法才能被代理private / protected 方法上的注解不生效。

复制代码

less

代码解读

复制代码

// ❌ private 方法,注解不生效 @StepStatus(...) private void pullFromThirdParty(Long taskId, DataSyncStep step) { ... } // ✅ public 方法,注解生效 @StepStatus(...) public void pullFromThirdParty(Long taskId, DataSyncStep step) { ... }

3. 枚举值必须作为方法参数传入

切面通过 isInstance() 从方法参数中提取当前步骤枚举,如果方法签名中没有枚举参数,切面会抛异常:

复制代码

less

代码解读

复制代码

// ✅ 方法参数中有 DataSyncStep,切面能提取到 @StepStatus(step = DataSyncStep.class, ...) public void pull(Long taskId, DataSyncStep step) { ... } // ❌ 方法参数中没有枚举类型,切面找不到当前步骤 @StepStatus(step = DataSyncStep.class, ...) public void pull(Long taskId) { ... } // 抛 IllegalArgumentException

4. taskId 参数名必须匹配

taskIdParam 指定的参数名必须和方法签名中的参数名一致:

复制代码

less

代码解读

复制代码

// ✅ taskIdParam = "taskId" 匹配参数名 taskId @StepStatus(taskIdParam = "taskId", ...) public void pull(Long taskId, ...) { ... } // ❌ taskIdParam = "taskId" 但参数名是 id,找不到 @StepStatus(taskIdParam = "taskId", ...) public void pull(Long id, ...) { ... } // 抛 IllegalArgumentException

5. Updater 必须是 Spring Bean

updater 指定的类必须标注了 @Component / @Service 等注解,否则 SpringContextHolder.getBean() 找不到。

复制代码

java

代码解读

复制代码

@Component // ✅ 必须是 Spring 管理的 Bean public class DataSyncStatusUpdater implements StepStatusUpdater { ... }

相关推荐
万亿少女的梦1681 小时前
基于Python的高考志愿填报辅助系统设计与实现
java·spring boot·python·mysql·vue
闲猫2 小时前
Python FastAPI + SQLAlchemy 入门教程:从零搭建你的第一个 Web 应用
前端·python·fastapi
北极星日淘5 小时前
中古货品品相评级算法实战|Java权重计分实现标准化五级品相体系
开发语言·python
hangyuekejiGEO5 小时前
临沂GEO服务企业技术选型分析
人工智能·python
闲猫7 小时前
Python 虚拟环境 virtualenv & uvicorn 服务搭建 & FAstAPI 使用
开发语言·python
AI视觉网奇7 小时前
vllm 多卡部署
python
精明的身影7 小时前
网络计划WebApp求解:融合Python与AI决策的项目管理系统
网络·python·web app
AI科技星7 小时前
全域谱分析:无穷维超复数信息场分形统一场论——自然、量子、金融多重分形第一性原理完整体系(中英双语终稿)
人工智能·python·算法·金融·乖乖数学·全域数学
用户0332126663677 小时前
使用 Python 在 Word 文档中添加批注
python
蜡笔削薪7 小时前
财联万业(杭州)数字科技有限公司能否给代理划定独家经营区域?
大数据·人工智能·python·科技