本节目标:把「一个
@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 初始化后把它替换成代理。
转化过程分四步:
AbstractAutoProxyCreator#postProcessAfterInitialization在 Bean 初始化完成后被调用,进入wrapIfNecessary。AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean找出适用于该 Bean 的所有 Advisor。AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors收集候选 Advisor,其中一部分来自普通AdvisorBean,另一部分来自切面类。- 切面类由
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 类 |
|---|---|
@Around | AspectJAroundAdvice |
@Before | AspectJMethodBeforeAdvice |
@After | AspectJAfterAdvice |
@AfterReturning | AspectJAfterReturningAdvice |
@AfterThrowing | AspectJAfterThrowingAdvice |
所以「一个 @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);
}
拆开看有两条规则:
- 先按注解种类排,固定次序是
Around → Before → After → AfterReturning → AfterThrowing。 - 同类多个方法再按方法名排(
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 代理失效的边界 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。