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

