流程步骤模板 - @StepStatus 注解方案
AOP 注解驱动的步骤状态管理,业务方法零侵入,加注解自动更新数据库状态。
核心特性
- 零侵入:业务方法只加一个注解,不写任何状态更新代码
- 统一切面:执行前自动更新"XX中",执行成功更新"XX完成",异常更新"失败"
- 灵活扩展 :每个业务自己定义
StatusUpdater实现,决定更新哪个表、怎么更新 - 任务 ID 灵活获取 :从方法参数按名称提取,支持
taskId、id等任意参数名
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 { ... }