《Spring Boot 高级》3.2 切面织入与顺序

讲清 @Aspect 如何经 AnnotationAwareAspectJAutoProxyCreator 与 ReflectiveAspectJAdvisorFactory 变成 Advisor,再看 ReflectiveMethodInvocation 如何串起拦截器链,并核实 @Order、@Priority 与同一切面内各类 advice 的相对顺序。

本节目标:把「一个 @Aspect 类」到「拦截器链上的一串 Advisor」这段转化过程拆开,并讲清切面之间、以及同一切面内多个 advice 之间的执行顺序由谁决定。
适用版本:Spring Boot 4.1.x(Java 21)

3.2 切面织入与顺序

3.1 节看清了「Bean 被换成了代理」。本节回答下一个问题:代理里那串拦截器是怎么装进去的,以及它们按什么顺序跑。顺序错乱是 AOP 最常见的隐蔽 bug——事务没生效、审计日志记错耗时、幂等判断排在业务之后,往往都源于没搞清这里的规则。

继续用图书借阅的场景,我们在 3.1 的 BorrowService 上挂两个切面:一个审计切面 AuditAspect,一个限流切面 RateLimitAspect。

3.2.1 从 @Aspect 到 Advisor

@Aspect 类本身不会自动生效,真正干活的是 AnnotationAwareAspectJAutoProxyCreator(org.springframework.aop.aspectj.annotation,javap 于 spring-aop-7.0.9.jar 核实)。它继承自 AspectJAwareAdvisorAutoProxyCreator,后者又继承 AbstractAdvisorAutoProxyCreator / AbstractAutoProxyCreator。这条继承链决定了它的身份:它是一个 BeanPostProcessor,在 Bean 初始化后把它替换成代理。

转化过程分四步:

  1. AbstractAutoProxyCreator#postProcessAfterInitialization 在 Bean 初始化完成后被调用,进入 wrapIfNecessary。
  2. AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean 找出适用于该 Bean 的所有 Advisor。
  3. AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors 收集候选 Advisor,其中一部分来自普通 Advisor Bean,另一部分来自切面类。
  4. 切面类由 BeanFactoryAspectJAdvisorsBuilder 扫描,交给 ReflectiveAspectJAdvisorFactory#getAdvisors 逐个 advice 方法转换成 Advisor。

第 4 步是本节的核心。ReflectiveAspectJAdvisorFactory.getAdvisors 对切面类里的每个 advice 方法调用 getAdvisor(...),最终产出一个 InstantiationModelAwarePointcutAdvisorImpl(javap 核实它 implements InstantiationModelAwarePointcutAdvisor, AspectJPrecedenceInformation)。这个类同时是 PointcutAdvisor 和 Ordered,它把两样东西绑在一起:

  • 切点:一个 AspectJExpressionPointcut,由 advice 方法上的 @Around("...") 等表达式解析而来。
  • 通知:一个具体的 Advice 实现,由 advice 注解种类决定。

映射关系如下(均于 spring-aop-7.0.9.jar 核实类名存在):

注解生成的 Advice 类
@AroundAspectJAroundAdvice
@BeforeAspectJMethodBeforeAdvice
@AfterAspectJAfterAdvice
@AfterReturningAspectJAfterReturningAdvice
@AfterThrowingAspectJAfterThrowingAdvice

所以「一个 @Aspect 类」在运行时并不存在,存在的是一堆挂在切面实例上的 Advisor。理解这一点,后面所有关于顺序的规则才有落点。

3.2.2 调用链:ReflectiveMethodInvocation

代理被调用时,无论走 JDK 还是 CGLIB,最终都会构造一个 ReflectiveMethodInvocation 并调用 proceed()。CGLIB 路径的入口是 CglibAopProxy$DynamicAdvisedInterceptor#intercept(spring-aop-7.0.9-sources.jar 核实):

List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
Object retVal;
if (chain.isEmpty()) {
    Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
    retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
    retVal = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain).proceed();
}

注意:Spring 7.0.9 的 jar 里没有 CglibAopProxy$CglibMethodInvocation 这个类(早期版本存在)。JDK 与 CGLIB 两条路径都用同一个 ReflectiveMethodInvocation。若你在旧资料里看到 CglibMethodInvocation,那是过时信息。

ReflectiveMethodInvocation 的字段(javap 核实)里有 interceptorsAndDynamicMethodMatchers(List<?>)与私有的 currentInterceptorIndex(初值 -1)。proceed() 的骨架是(源码核实):

public Object proceed() throws Throwable {
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        return invokeJoinpoint();
    }
    Object interceptorOrInterceptionAdvice =
            this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
    if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher dm) {
        Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
        if (dm.matcher().matches(this.method, targetClass, this.arguments)) {
            return dm.interceptor().invoke(this);
        }
        else {
            return proceed();
        }
    }
    else {
        return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
    }
}

关键在于:proceed() 是一个游标自增的递归链。每个拦截器拿到的是同一个 MethodInvocation,想继续往下走就得回调 mi.proceed()。于是:

  • 拦截器在 proceed() 之前写的代码,构成「前环绕」;
  • proceed() 之后写的代码,构成「后环绕」。

@Around 能改变返回值、抛异常、甚至完全不调 proceed(),正是因为它拿到的 ProceedingJoinPoint 就是 ReflectiveMethodInvocation 的包装。核实:org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint 的构造参数就是 org.springframework.aop.ProxyMethodInvocation(javap 核实)。

3.2.3 切面之间的顺序:@Order、Ordered、@Priority

链条里 Advisor 的先后,由每个 Advisor 的 order 值决定:值越小越靠外(越先进入、越后退出)。Ordered 接口给了两个常量:HIGHEST_PRECEDENCE(Integer.MIN_VALUE)与 LOWEST_PRECEDENCE(Integer.MAX_VALUE)。

对切面类来说,order 从哪里取?InstantiationModelAwarePointcutAdvisorImpl#getOrder 返回 this.aspectInstanceFactory.getOrder()(源码核实),而 SimpleMetadataAwareAspectInstanceFactory / SingletonMetadataAwareAspectInstanceFactory 的实现是:

protected int getOrderForAspectClass(Class<?> aspectClass) {
    return OrderUtils.getOrder(aspectClass, Ordered.LOWEST_PRECEDENCE);
}

也就是说:切面的顺序读的是切面类上的排序注解,缺省值是 LOWEST_PRECEDENCE。OrderUtils.getOrderFromAnnotations 的取值优先级(spring-core-7.0.9-sources.jar 核实):

private static Integer findOrder(MergedAnnotations annotations) {
    MergedAnnotation<Order> orderAnnotation = annotations.get(Order.class);
    if (orderAnnotation.isPresent()) {
        return orderAnnotation.getInt(MergedAnnotation.VALUE);
    }
    MergedAnnotation<?> priorityAnnotation = annotations.get(JAKARTA_PRIORITY_ANNOTATION);
    if (priorityAnnotation.isPresent()) {
        return priorityAnnotation.getInt(MergedAnnotation.VALUE);
    }
    return null;
}

归纳成三条规则:

排序来源位置说明
@Order(n)org.springframework.core.annotation.Order优先级最高,存在即用它
jakarta.annotation.Priority(n)JSR-250仅当没有 @Order 时作为回退
Ordered 接口org.springframework.core.Ordered类实现该接口时由 OrderComparator 直接读取

给两个切面标号:

@Aspect
@Order(1)
@Component
public class AuditAspect {
    @Around("execution(* com.example.audit.BorrowService.*(..))")
    public Object audit(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        try {
            return pjp.proceed();
        }
        finally {
            System.out.printf("[audit] %s cost=%dns%n",
                    pjp.getSignature().getName(), System.nanoTime() - start);
        }
    }
}

@Aspect
@Order(2)
@Component
public class RateLimitAspect {
    @Before("execution(* com.example.audit.BorrowService.*(..))")
    public void check() {
        System.out.println("[rate-limit] check");
    }
}

AuditAspect 的 order=1 小于 RateLimitAspect 的 2,所以审计在外层、限流在内层。一次 borrow 调用的示例输出是:

[rate-limit] check
[audit] borrow cost=1234567ns

限流先打印,是因为它更靠内;审计的耗时统计包住了限流。

两个必须记住的坑:

  • order 相同的两个切面,先后顺序是未定义的。 不要依赖 @Order(0) 和另一个 @Order(0) 的相对位置,也不要用「类名字母序」这种未文档化的行为去推断。
  • @Order 写错方向会得到反效果。 想把某个切面放到最外层,就给它一个更小的数(或 Ordered.HIGHEST_PRECEDENCE),不是更大的数。

3.2.4 同一个切面内多个 advice 的顺序

同一个切面类里写了多个 advice 方法时,它们的相对顺序由 ReflectiveAspectJAdvisorFactory 里的一个静态比较器决定(源码核实):

private static final Comparator<Method> adviceMethodComparator;

static {
    // Note: although @After is ordered before @AfterReturning and @AfterThrowing,
    // an @After advice method will actually be invoked after @AfterReturning and
    // @AfterThrowing methods due to the fact that AspectJAfterAdvice.invoke(MethodInvocation)
    // invokes proceed() in a `try` block and only invokes the @After advice method
    // in a corresponding `finally` block.
    Comparator<Method> adviceKindComparator = new ConvertingComparator<Method, Annotation>(
            new InstanceComparator<>(
                    Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
            method -> { /* 取方法上的 AspectJ 注解 */ });
    Comparator<Method> methodNameComparator = new ConvertingComparator<>(Method::getName);
    adviceMethodComparator = adviceKindComparator.thenComparing(methodNameComparator);
}

拆开看有两条规则:

  1. 先按注解种类排,固定次序是 Around → Before → After → AfterReturning → AfterThrowing。
  2. 同类多个方法再按方法名排(Method::getName,即字典序),所以同一类 advice 写两个方法,名字靠前的先执行。

第 1 条里那个注释特别重要,它点出了「链上顺序 ≠ 运行顺序」:

  • 在链上,@After 排在 @AfterReturning / @AfterThrowing 之前(更靠外)。
  • 但在运行时,AspectJAfterAdvice.invoke 把 proceed() 包在 try 里、把 @After 方法放在 finally 里,所以 @After 实际在 @AfterReturning / @AfterThrowing 之后才执行。

把「链上位置」和「实际运行时刻」画成一张对照表:

advice链上位置(由外到内)一次正常返回时的运行时刻
@Around最外前半段最先,后半段最后
@Before第二进入目标方法前
@After第三目标方法返回后(finally)
@AfterReturning第四正常返回时,早于 @After
@AfterThrowing最内抛异常时,早于 @After

一次正常返回的示例输出(一个切面里同时写五种 advice):

[around] before
[before]
--- 目标方法执行 ---
[afterReturning] result=BorrowRecord(...)
[after]
[around] after

一次抛异常的示例输出:

[around] before
[before]
--- 目标方法抛出 IllegalStateException ---
[afterThrowing] ex=IllegalStateException
[after]
[around] after

@AfterReturning 与 @AfterThrowing 互斥,同一次调用只会出现其中一个。

3.2.5 怎么观察与验证

想确认自己应用里的顺序,有两个实用手段。

其一,在 advice 里打印方法名,按上面的对照表核对输出。这是最直接的。

其二,打开 AOP 相关的 DEBUG 日志,观察代理创建过程:

logging:
  level:
    org.springframework.aop: DEBUG

启动时会看到类似「Creating CGLIB proxy」「Creating advisor …」的调试行,可以据此确认某个 Bean 到底被哪个切面织入了。注意这类日志量很大,只在排查时临时开。

还有一个容易被忽略的点:@Around 若忘了调 proceed(),链上后面的 advice 与目标方法都不会执行,且不会有任何报错——返回值会直接是 null。这类「静默短路」是排查 AOP 问题时最该先看的地方。

小结

  • @Aspect 不直接生效,由 AnnotationAwareAspectJAutoProxyCreator 在 Bean 初始化后扫描,经 ReflectiveAspectJAdvisorFactory 把每个 advice 方法转成 InstantiationModelAwarePointcutAdvisorImpl(既是 PointcutAdvisor 又是 Ordered)。
  • 拦截器链由 AdvisedSupport#getInterceptorsAndDynamicInterceptionAdvice 生成,运行时靠 ReflectiveMethodInvocation.proceed() 的 currentInterceptorIndex 递归推进;Spring 7.0.9 中 JDK 与 CGLIB 路径共用该类,不存在 CglibMethodInvocation。
  • 切面之间的顺序取自切面类的 @Order,没有才回退到 jakarta.annotation.Priority,都没有则 LOWEST_PRECEDENCE;值越小越靠外,相同值顺序未定义。
  • 同一切面内的 advice 先按 Around → Before → After → AfterReturning → AfterThrowing 排序,同类再按方法名字典序。
  • 链上顺序不等于运行顺序:@After 链上在 @AfterReturning / @AfterThrowing 之前,但运行在它们之后(finally 语义)。
  • 观察手段:advice 内打日志、logging.level.org.springframework.aop=DEBUG。

顺序理清了,还剩最后一个、也是最容易在线上踩到的坑:明明加了注解,为什么一点效果都没有——这就是 3.3 节要讲的「代理失效的边界」。

阅读导航:上一节:3.1 JDK 动态代理与 CGLIB · 下一节:3.3 代理失效的边界 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计