You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot AOP:如何让@Before通知优先于@Around通知执行?

解决Spring Boot AOP中@Before通知晚于@Around起始日志的问题

问题背景

我在构建Spring Boot v3.1.1应用时,用AOP实现日志打印功能:自定义@LogAround注解,通过@Around通知logAroundImpl打印方法的进入和退出日志;新增@Before通知putUserIdToMdc,为标注@UserIdRestricted的方法向MDC存入用户ID数据。但实际执行时,putUserIdToMdc晚于logAroundImpl的起始日志打印,导致第一条日志无MDC数据,需要让putUserIdToMdc优先执行。

原相关代码

// 自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogAround {}

// 业务方法示例
@LogAround
public String getName(@Long userId) {
   return "Lorem";
}

// 切面代码
@Aspect
@Component
public class AspectImpl {
    // Pointcut表达式
    @Pointcut("execution(@UserIdRestricted * *(..))")
    public void userIdRestricted() {}

    @Pointcut("@annotation(LogAround)")
    public void logAround() {}

    @Around("userIdRestricted()")
    public Object verifyUserIdRestriction(ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed();
    }

    @Around("logAround()")
    public Object logAroundImpl(ProceedingJoinPoint joinPoint) throws Throwable {
       Logger logger = LoggerFactory.getLogger(joinPoint.getSignature().getDeclaringType());
       String methodName = joinPoint.getSignature().getName();

       logger.info("{} started", methodName);

       Object proceed;
       try {
           proceed = joinPoint.proceed();
       } catch (Throwable ignored) {}

       logger.info("{} finished", methodName);

       return proceed;
    }

    // 新增的@Before通知
    @Before("userIdRestricted()")
    public void putUserIdToMdc(JoinPoint joinPoint) throws Throwable {
       log.info("putUserIdToMdc");
       MDC.put("userId", String.valueOf(1234));
    }
}

原控制台输出

03:54PM INFO MyService : {} getName started. // 无MDC数据,先执行
03:54PM INFO AspectImpl : {} putUserIdToMdc
03:54PM INFO MyService : {userId=1234} getName finished.

问题原因

当方法同时标注@LogAround和@UserIdRestricted时,两个切面(或同一切面内的不同通知)的执行顺序未明确,导致logAroundImpl这个@Around通知先执行前置日志打印,再调用proceed()触发putUserIdToMdc的@Before通知,因此第一条日志没有MDC数据。

解决方案

方案1:通过@Order明确切面优先级

给包含putUserIdToMdc的切面设置更高优先级(更小的@Order值),确保其通知先执行:

// 用户ID处理切面,优先级更高
@Aspect
@Component
@Order(1)
public class UserIdAspect {
    @Pointcut("execution(@UserIdRestricted * *(..))")
    public void userIdRestricted() {}

    @Before("userIdRestricted()")
    public void putUserIdToMdc(JoinPoint joinPoint) throws Throwable {
        log.info("putUserIdToMdc");
        MDC.put("userId", String.valueOf(1234));
    }

    @Around("userIdRestricted()")
    public Object verifyUserIdRestriction(ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed();
    }
}

// 日志切面,优先级更低
@Aspect
@Component
@Order(2)
public class LogAspect {
    @Pointcut("@annotation(LogAround)")
    public void logAround() {}

    @Around("logAround()")
    public Object logAroundImpl(ProceedingJoinPoint joinPoint) throws Throwable {
       Logger logger = LoggerFactory.getLogger(joinPoint.getSignature().getDeclaringType());
       String methodName = joinPoint.getSignature().getName();

       // 此时MDC已存入userId,日志会带上数据
       logger.info("{} started", methodName);

       Object proceed;
       try {
           proceed = joinPoint.proceed();
       } catch (Throwable e) {
           // 不建议忽略异常,需打印错误日志并抛出
           logger.error("{} failed", methodName, e);
           throw e;
       } finally {
           // 建议移除MDC数据,避免内存泄漏
           MDC.remove("userId");
       }

       logger.info("{} finished", methodName);

       return proceed;
    }
}

执行顺序变为:UserIdAspect.putUserIdToMdc → LogAspect.logAroundImpl打印started日志 → 执行业务方法 → LogAspect.logAroundImpl打印finished日志 → 移除MDC数据。

方案2:合并通知逻辑(同一切面内)

如果两个通知原本属于同一个切面,可在logAroundImpl中先判断方法是否标注@UserIdRestricted,提前存入MDC:

@Aspect
@Component
public class CombinedAspect {
    @Pointcut("execution(@UserIdRestricted * *(..))")
    public void userIdRestricted() {}

    @Pointcut("@annotation(LogAround)")
    public void logAround() {}

    @Around("logAround()")
    public Object logAroundImpl(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        boolean hasUserIdRestriction = method.isAnnotationPresent(UserIdRestricted.class);

        // 提前存入MDC
        if (hasUserIdRestriction) {
            MDC.put("userId", String.valueOf(1234));
        }

        Logger logger = LoggerFactory.getLogger(joinPoint.getSignature().getDeclaringType());
        String methodName = joinPoint.getSignature().getName();

        logger.info("{} started", methodName);

        Object proceed;
        try {
            proceed = joinPoint.proceed();
        } catch (Throwable e) {
            logger.error("{} failed", methodName, e);
            throw e;
        } finally {
            // 移除MDC数据
            if (hasUserIdRestriction) {
                MDC.remove("userId");
            }
        }

        logger.info("{} finished", methodName);

        return proceed;
    }

    // 若已在logAroundImpl中处理MDC,可移除该@Before通知
    @Before("userIdRestricted()")
    public void putUserIdToMdc(JoinPoint joinPoint) throws Throwable {
        log.info("putUserIdToMdc");
        MDC.put("userId", String.valueOf(1234));
    }

    @Around("userIdRestricted()")
    public Object verifyUserIdRestriction(ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed();
    }
}

这种方式无需调整切面优先级,直接在日志通知前置逻辑中处理MDC存入,确保日志打印时MDC已有数据。

方案3:将@Before改为@Around通知并设置优先级

把putUserIdToMdc改为@Around通知,通过优先级确保先执行MDC存入:

@Aspect
@Component
@Order(1)
public class UserIdAspect {
    @Pointcut("execution(@UserIdRestricted * *(..))")
    public void userIdRestricted() {}

    @Around("userIdRestricted()")
    public Object putUserIdToMdcAndProceed(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            MDC.put("userId", String.valueOf(1234));
            log.info("putUserIdToMdc");
            return joinPoint.proceed();
        } finally {
            MDC.remove("userId");
        }
    }

    @Around("userIdRestricted()")
    public Object verifyUserIdRestriction(ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed();
    }
}

@Aspect
@Component
@Order(2)
public class LogAspect {
    @Around("logAround()")
    public Object logAroundImpl(ProceedingJoinPoint joinPoint) throws Throwable {
        // 原日志逻辑,此时MDC已存入数据
        Logger logger = LoggerFactory.getLogger(joinPoint.getSignature().getDeclaringType());
        String methodName = joinPoint.getSignature().getName();

        logger.info("{} started", methodName);

        Object proceed;
        try {
            proceed = joinPoint.proceed();
        } catch (Throwable e) {
            logger.error("{} failed", methodName, e);
            throw e;
        }

        logger.info("{} finished", methodName);

        return proceed;
    }
}

执行顺序为:UserIdAspect.putUserIdToMdcAndProceed存入MDC → LogAspect.logAroundImpl打印started日志 → 执行业务方法 → LogAspect.logAroundImpl打印finished日志 → UserIdAspect移除MDC数据。

内容的提问来源于stack exchange,提问作者RRGT19

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 00:12:02