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

51 阅读9分钟

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

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


核心特性

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

1. 通用步骤接口 ProcessStep.java

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

2. 注解定义 @StepStatus.java

import java.lang.annotation.*;
​
/**
 * 步骤状态注解:标注在业务方法上,AOP 自动在执行前后更新数据库状态。
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface StepStatus {
​
    /**
     * 当前执行到哪一步,通过枚举类标识。
     * 注解不支持传枚举实例(Java 语言限制),所以用 Class 承载类型信息,
     * 切面通过 isInstance() 从方法参数中提取具体枚举值。
     */
    Class<? extends ProcessStep> stepClass();
​
    /**
     * 方法参数名,从哪个参数提取 taskId。
     * 默认 "taskId",也可指定为其他参数名。
     */
    String taskIdParam() default "taskId";
​
    /**
     * 状态更新器的实现类,由业务方指定。
     * 切面通过 SpringContextHolder 获取该 Bean,调用其 update 方法。
     */
    Class<? extends StepStatusUpdater> updater();
}

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

通过泛型约束 taskId 类型,实现类在编译期就能保证类型安全。

/**
 * 状态更新器接口:每个业务场景自行实现,决定更新哪个表、用哪个 Mapper。
 * @param <T> 任务ID类型,如 Long、String、Integer
 */
public interface StepStatusUpdater<T> {
    /**
     * 更新任务状态。切面会自动从方法参数提取 taskId 并转换为目标类型。
     */
    void update(T taskId, String statusCode, String statusDesc);
}

4. AOP 切面 StepStatusAspect.java

import com.midea.logistics.pms.common.util.ApplicationContextUtils;
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.aop.support.AopUtils;
import org.springframework.core.annotation.Order;
​
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
​
@Aspect
@Order(0)
public class StepStatusAspect {
​
    @Around("@annotation(stepStatus)")
    public Object around(ProceedingJoinPoint joinPoint, StepStatus stepStatus) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        StepStatus annotation = signature.getMethod().getAnnotation(StepStatus.class);
​
        Object rawTaskId = extractTaskId(joinPoint, annotation.taskIdParam());
        ProcessStep step = resolveStepEnum(joinPoint, annotation.stepClass());
        StepStatusUpdater<?> updater = ApplicationContextUtils.getContext().getBean(annotation.updater());
​
        invokeUpdate(rawTaskId, step.getRunningCode(), step.getRunningDesc(), updater);
​
        try {
            Object result = joinPoint.proceed();
            invokeUpdate(rawTaskId, step.getFinishCode(), step.getFinishDesc(), updater);
            return result;
        } catch (Exception e) {
            invokeUpdate(rawTaskId, step.getFailCode(), step.getFailDesc(), updater);
            throw e;
        }
    }
​
    /**
     * 从方法参数中提取 taskId 原始值。
     */
    private Object extractTaskId(ProceedingJoinPoint joinPoint, String paramName) {
        String[] paramNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < paramNames.length; i++) {
            if (paramName.equals(paramNames[i])) {
                return args[i];
            }
        }
        throw new IllegalArgumentException("找不到 taskId 参数: " + paramName);
    }
​
    /**
     * 从方法参数中提取具体枚举值(当前步骤)。
     */
    private ProcessStep resolveStepEnum(ProceedingJoinPoint joinPoint, Class<? extends ProcessStep> stepClass) {
        for (Object arg : joinPoint.getArgs()) {
            if (arg != null && stepClass.isInstance(arg)) {
                return (ProcessStep) arg;
            }
        }
        throw new IllegalArgumentException(
            "方法参数中找不到 " + stepClass.getSimpleName() + " 类型的枚举,无法确定当前步骤");
    }
​
    private void invokeUpdate(Object rawTaskId, String code, String desc, StepStatusUpdater<?> updater) {
        Class<?> targetType = resolveGenericType(updater);
        Object taskId = convert(rawTaskId, targetType);
        updateWithCaptured(updater, taskId, code, desc);
    }
​
    /** 绕过通配符捕获导致的编译期类型检查 */
    private <T> void updateWithCaptured(StepStatusUpdater<T> updater, Object taskId, String code, String desc) {
        @SuppressWarnings("unchecked")
        T casted = (T) taskId;
        updater.update(casted, code, desc);
    }
​
    /**
     * 通过反射获取泛型参数类型。
     * 注意:updater 是 Spring 代理对象,需要用 AopUtils.getTargetClass() 拿到真实实现类。
     */
    private Class<?> resolveGenericType(StepStatusUpdater<?> updater) {
        Class<?> targetClass = AopUtils.getTargetClass(updater);
        for (Type genericInterface : targetClass.getGenericInterfaces()) {
            if (genericInterface instanceof ParameterizedType) {
                ParameterizedType pt = (ParameterizedType) genericInterface;
                if (pt.getRawType().equals(StepStatusUpdater.class)) {
                    Type typeArg = pt.getActualTypeArguments()[0];
                    if (typeArg instanceof Class) {
                        return (Class<?>) typeArg;
                    }
                }
            }
        }
        return Object.class;
    }
​
    private Object convert(Object value, Class<?> targetType) {
        if (value == null || targetType.isInstance(value)) {
            return value;
        }
        if (targetType == Long.class || targetType == long.class) {
            return Long.valueOf(value.toString());
        }
        if (targetType == String.class) {
            return value.toString();
        }
        if (targetType == Integer.class || targetType == int.class) {
            return Integer.valueOf(value.toString());
        }
        return value;
    }
}

5. 业务枚举 DataSyncStep.java

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

业务方在自己的 Updater 上指定泛型类型,编译期约束 taskId 类型。

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
​
/**
 * 数据同步业务的状态更新器,决定更新哪个表、用哪个 Mapper。
 * 指定泛型为 Long,taskId 只能是 Long,编译期检查。
 */
@Component
public class DataSyncStatusUpdater implements StepStatusUpdater<Long> {
​
    @Resource
    private TaskMapper taskMapper;
​
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void update(Long taskId, String statusCode, String statusDesc) {
        taskMapper.updateStatus(taskId, statusCode, statusDesc);
    }
}

7. 业务使用示例 DataSyncService.java

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(stepClass = DataSyncStep.class, taskIdParam = "taskId", updater = DataSyncStatusUpdater.class)
    private void pullFromThirdParty(Long taskId, DataSyncStep step) {
        // 纯业务逻辑,无需关心状态
        System.out.println("[业务] 正在从第三方接口拉取原始数据...");
    }
​
    @StepStatus(stepClass = DataSyncStep.class, taskIdParam = "taskId", updater = DataSyncStatusUpdater.class)
    private void parseRawData(Long taskId, DataSyncStep step) {
        System.out.println("[业务] 正在解析原始数据格式...");
    }
​
    @StepStatus(stepClass = DataSyncStep.class, taskIdParam = "taskId", updater = DataSyncStatusUpdater.class)
    private Integer calculateMetrics(Long taskId, DataSyncStep step) {
        System.out.println("[业务] 正在执行数据指标计算...");
        return 200;
    }
​
    @StepStatus(stepClass = DataSyncStep.class, taskIdParam = "taskId", updater = DataSyncStatusUpdater.class)
    private void saveFinalResult(Long taskId, DataSyncStep step, Integer data) {
        System.out.println("[业务] 正在保存计算结果,数据量:" + data);
    }
}

运行效果

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

[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() 匹配提取:

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

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

2. taskId 类型通过泛型约束

StepStatusUpdater<T> 是泛型接口,实现类指定具体类型,编译期保证类型安全:

@Component
public class LongTaskStatusUpdater implements StepStatusUpdater<Long> { ... }
​
@Component
public class StringTaskStatusUpdater implements StepStatusUpdater<String> { ... }

切面通过反射读取 Updater 实现类的泛型参数,自动将 taskId 转换为目标类型(支持 Long/String/Integer)。

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

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

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

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

4. 事务由 Updater 管理

切面内直接调用 updater.update(),如果切面自己加 @Transactional 属于类内调用不生效。

正确做法:Updater 实现类自己加 @Transactional(REQUIRES_NEW) 。切面通过 SpringContextHolder.getBean() 拿到的是 Spring 代理对象,调用会经过代理,事务生效。即使业务方法回滚,状态记录仍保留,便于排查问题。


与 StepStatusTemplate 链式方案对比

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

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


使用注意事项

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

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

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

@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 调用(不经过代理)

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

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

@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(stepClass = DataSyncStep.class, updater = DataSyncStatusUpdater.class)
    public void pullFromThirdParty(Long taskId, DataSyncStep step) { ... }
}

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

2. 方法必须是 public

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

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

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

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

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

4. taskId 参数名必须匹配

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

// ✅ 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() 找不到。

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

6. 事务由 Updater 自行管理

切面内部直接调用 updater.update(),切面自己加 @Transactional 属于类内调用,不会生效。

正确做法:在 Updater 的 update() 方法上加 @Transactional(REQUIRES_NEW) 。切面通过 getBean() 拿到的是 Spring 代理对象,调用会经过代理,事务生效。

@Component
public class DataSyncStatusUpdater implements StepStatusUpdater<Long> {
    @Resource
    private TaskMapper taskMapper;
​
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void update(Long taskId, String statusCode, String statusDesc) {
        taskMapper.updateStatus(taskId, statusCode, statusDesc);
    }
}