Spring AOP源码解析:从代理创建到通知链执行的完整链路
Spring 源码里最容易被忽视的一层AOP 的完整实现链路做 Java 开发的人几乎每天都在和各种 Spring 注解打交道。Transactional、Async、自定义的日志切面、权限拦截切面背地里都是同一套机制在工作这套机制就是 AOPAspect Oriented Programming面向切面编程。很多同学能熟练使用这几个注解可真到面试被问一句“Spring 的 AOP 原理是什么”或者线上遇到“切面为什么不生效”就很容易卡壳。这篇文章想从源码层面把 Spring AOP 的完整实现链路掰开揉碎讲一遍代理对象是怎么创建的、切点怎么匹配、通知链怎么执行、常见的不生效场景到底卡在哪一步。适合刚看完 IOC 源码、想顺着 Bean 生命周期继续往下啃 Spring 核心的读者也适合被 AOP 疑难问题折磨过、想系统补一遍底层逻辑的同学。先说为什么要关注 AOP 的实现细节。IOC 容器解决了对象的创建和依赖注入问题但业务代码里大量的横切逻辑比如日志、事务、权限、缓存如果散落在每个业务方法里代码就会非常臃肿。AOP 的价值就是把这类横切逻辑从业务代码中抽离出来以“切面”的形式挂在业务方法执行链路中。Spring 最终选择用“代理机制”来实现这个效果核心原因在于 Java 本身没有语言级的切面支持只能通过在运行时生成一个增强版的代理对象让它包裹住原始 Bean把额外逻辑织入目标方法调用的前后。你不用动原来的业务类代理对象已经帮你在调用链路上加了该有的逻辑这就是“面向切面”落地的关键一步。1. AOP 整体设计与核心概念Spring 为什么非要走代理这条路1.1 三个核心抽象Pointcut、Advice、Advisor 的组合逻辑跟很多入门者想的“AOP 就是动态代理”不一样Spring AOP 里的核心模型由三个抽象共同构成切入点Pointcut、通知Advice、切面描述Advisor。切入点负责回答“哪些方法要被增强”通知负责回答“增强的具体逻辑是什么”而 Advisor 则是把二者绑定起来的组装单元。先看切入点。Spring 里Pointcut接口有两个核心方法getClassFilter()用于类级别的快速筛选getMethodMatcher()用于方法级别的精确匹配。以AspectJExpressionPointcut为例它内部会持有PointcutExpression对象在matches(Method, Class, Object[])这一步对方法名、参数、注解条件做表达式求值。实际运行时Spring 一定会先做类级别的粗筛减少进入方法级匹配的次数这是很重要的性能优化细节。再看通知。Advice本身是标记接口真正的逻辑都被拆成MethodBeforeAdvice、AfterReturningAdvice、ThrowsAdvice、MethodInterceptor等子类型。需要特别说明的是MethodInterceptor才是 Spring AOP 执行链路里最核心的抽象它只有一个invoke(MethodInvocation)方法通过链式调用来控制“前置逻辑-目标方法-返回/异常处理”的执行顺序。最后看 Advisor。Advisor接口只要求返回一条Advice而 Spring 真正提供的是InstantiationModelAwarePointcutAdvisor和AspectJPointcutAdvisor这些实现类它们内部通常持有一个AspectJExpressionPointcut和一个Advice。这里有一个初学者容易混淆的点注解里直接写的Around、Before、After方法并不是一个完整的Advice它们要经过适配器转换后才会变成不同类型的Advice。后面讲源码解析时会看到这个转换过程。抽象核心接口解决的问题对应注解PointcutPointcut哪个类的哪个方法被增强Pointcut(execution(...))AdviceMethodInterceptor等增强逻辑是什么样的Before、AfterReturning、AroundAdvisorAdvisor把切点表达式和通知组装内部的组合结果1.2 一个代理模式的现实类比从“代理记账”到“JDK 动态代理”理解 Spring AOP 为什么采用代理模式可以拿“代理记账”来类比。公司老板业务调用方想处理财务但他不希望自己懂所有税务细节于是找了一个代理记账公司代理对象。代理记账公司接手后会在你原有的做账流程前后插入大量额外操作先检查发票合规性、再入账、最后申报纳税。老板只需要提交原始票据剩下的横切流程全部由代理公司完成原公司的核心记账逻辑目标方法没变。这个类比对应到代码里就是Spring 容器创建的“代理 Bean”和“原始 Bean”实现同样的接口但代理 Bean 内部持有原 Bean 的引用和一条通知链。调用方拿到的其实是代理对象当它调用业务方法时代理不会直接透传给你而是先把通知链里的逻辑依次执行在合适的时机比如Around里的proceed()才真正调用原始类的方法。原始类从头到尾不知道代理的存在这就是“非侵入式增强”的本质原因。那 Spring 为什么不直接用继承来做增强呢继承虽然简单但有两个致命问题一是 Java 是单继承一个类只能有一个父类增强逻辑很难叠加二是继承无法对没有父类关系的兄弟类统一做增强比如两个实现同一个接口的类你想用同一套日志逻辑继承就做不到接口级别的统一。代理模式没有这些限制它基于接口或类生成一个平行对象可以叠加多层增强天然适合横切逻辑的复用。这就是 Spring 选择代理机制而不用继承的根本原因。1.3 动态代理的两条实现路线JDK 代理与 CGLIB 的取舍Spring 支持两种动态代理JDK 动态代理和 CGLIB。很多同学只记得一个口诀“优先用接口就用 JDK没接口就用 CGLIB”但真正到源码层面这个选择是DefaultAopProxyFactory里做出来的。public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { Override public AopProxy createAopProxy(AdvisedSupport config) { if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class? targetClass config.getTargetClass(); if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); } return new ObjenesisCglibAopProxy(config); } else { return new JdkDynamicAopProxy(config); } } }注意这里config是AdvisedSupport我们平时用的ProxyFactory、ProxyFactoryBean都继承自它它里面存放着advisors、targetSource、interfaces等关键配置。看到条件判断了吗内部类如果实现了接口后续又设置spring.aop.proxytargetclasstrue就会强制走 CGLIB。从 Spring Boot 2.x 开始Boot 默认把proxyTargetClass设为true所以现在大部分项目实际走的是 CGLIB 路线。JDK 动态代理要求目标必须实现接口运行时通过java.lang.reflect.Proxy和InvocationHandler生成代理类字节码简洁但不能代理无接口的类。CGLIB 通过继承目标类生成子类可以对没有接口的普通类做增强但如果目标方法是final或类本身是finalCGLIB 也无法代理。这个细节很重要后面排查“切面不生效”时会反复遇到。2. 核心源码拆解代理对象到底是怎么被“变”出来的2.1 PostProcessor 入口Bean 创建流程中的“暗渡陈仓”读过 Spring IOC 源码的同学应该清楚每个 Bean 创建出来后都会经过BeanPostProcessor的“后置处理”环节。AOP 能自动生成代理对象靠的是 Spring 内置的AnnotationAwareAspectJAutoProxyCreator它是AbstractAutoProxyCreator的子类同时实现了BeanPostProcessor接口。关键源码在AbstractAutoProxyCreator.postProcessAfterInitializationpublic Object postProcessAfterInitialization(Nullable Object bean, String beanName) { if (bean ! null) { Object cacheKey getCacheKey(bean.getClass(), beanName); if (this.earlyProxyReferences.remove(cacheKey) ! bean) { return wrapIfNecessary(bean, beanName, cacheKey); } } return bean; }wrapIfNecessary的英文名非常直白如果目标 Bean 需要被增强就返回一个代理对象否则返回原对象。这里earlyProxyReferences跟三级缓存有关如果 Bean 在实例化早期就已经被提前代理过就不会重复代理。这也是网上流传的“Spring 三级缓存解决循环依赖时可能产生早期代理对象”这一说法的出处之一。wrapIfNecessary内部会先查缓存如果当前 Bean 已经属于某个切面就包装它否则调用getAdvicesAndAdvisorsForBean找出能匹配当前 Bean 的所有 Advisor。这一步的返回值如果为空说明当前 Bean 不需要增强直接返回原 Bean。protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { if (this.targetSourcedBeans.contains(beanName)) { return bean; } Object[] specificInterceptors getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); if (specificInterceptors ! DO_NOT_PROXY) { this.advisedBeans.add(cacheKey); Object proxy createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); this.proxyTypes.put(cacheKey, proxy.getClass()); return proxy; } return bean; }注意SingletonTargetSource它把原始 Bean 包了一层目的就是让代理对象在真正调用目标方法时能拿到原始实例。如果 Bean 是多例或 prototype 作用域这里会使用不同的TargetSource实现每次调用时再获取目标实例。这就是为什么原型 Bean 也能走 AOP只是每次获取的代理对象会重新绑定一个全新的目标对象。2.2 Advisor 的收集与排序AspectJ注解怎么被翻译成 Advisor真正决定哪些 Bean 需要代理的关键方法是AbstractAdvisorAutoProxyCreator.getAdvicesAndAdvisorsForBean它会调用findEligibleAdvisors。AnnotationAwareAspectJAutoProxyCreator重写了这个流程负责从容器中所有声明了Aspect注解的类里解析出 Advisor。我画一条完整的调用链AbstractAutoProxyCreator.postProcessAfterInitialization→wrapIfNecessary→getAdvicesAndAdvisorsForBean→AbstractAdvisorAutoProxyCreator.findEligibleAdvisors→AnnotationAwareAspectJAutoProxyCreator.findCandidateAdvisors。protected ListAdvisor findCandidateAdvisors() { ListAdvisor advisors super.findCandidateAdvisors(); if (this.aspectJAdvisorsBuilder ! null) { advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors()); } return advisors; }super.findCandidateAdvisors()收集的是基于Advisor类型注册的 Bean比如你自己定义的DefaultPointcutAdvisor。buildAspectJAdvisors()才负责扫Aspect类。它在容器初始化时会遍历所有 BeanName过滤出类上标注了Aspect的 Bean内部会排除Aspect的ajc编译器生成的辅助类然后用ReflectiveAspectJAdvisorFactory逐个解析切面类中的方法。ReflectiveAspectJAdvisorFactory.getAdvisor是核心它做了这样一个关键转换读取方法上的Before、Around、After、AfterReturning、AfterThrowing注解提取切点表达式把表达式解析成AspectJExpressionPointcut对象把切面方法包装成对应的Advice对象比如AroundAdvice、AspectJMethodBeforeAdvice创建一个InstantiationModelAwarePointcutAdvisor把这二者绑在一起对切面本身做单例化处理或延迟实例化懒加载确保切面 Bean 能被正确初始化。在实际代码里Around对应AspectJAroundAdvice它的invoke方法内部会反射调用切面方法并且允许通过ProceedingJoinPoint.proceed()继续执行下一个通知或目标方法。Before对应AspectJMethodBeforeAdvice它的before方法通过invokeAdviceMethod反射执行切面方法执行完后会自动进入下一个拦截器。排序问题也发生在这步findEligibleAdvisors完成收集后会调用sortAdvisors对切面规则排序。Spring 提供了一套OrderComparator和支持Order注解的排序策略同时内置了复杂的“同一切面内部环绕通知优先于前置通知”的规则。这个顺序在实战中非常重要我后面单独展开讲。2.3 createProxy 与 AopProxy 的最终产物代理对象的“出厂”拿到匹配的 Advisor 列表后AbstractAutoProxyCreator.createProxy开始真正组装代理对象。它先构建一个ProxyFactory也就是ProxyCreatorSupport的子类把目标 Bean 的类信息、接口、Advisor 列表全部设置进去然后调用getProxy(classLoader)。protected Object createProxy(Class? beanClass, Nullable String beanName, Nullable Object[] specificInterceptors, TargetSource targetSource) { ProxyFactory proxyFactory new ProxyFactory(); proxyFactory.copyFrom(this); if (!proxyFactory.isProxyTargetClass()) { if (shouldProxyTargetClass(beanClass, beanName)) { proxyFactory.setProxyTargetClass(true); } else { evaluateProxyInterfaces(beanClass, proxyFactory); } } Advisor[] advisors buildAdvisors(beanName, specificInterceptors); proxyFactory.addAdvisors(advisors); proxyFactory.setTargetSource(targetSource); customizeProxyFactory(proxyFactory); proxyFactory.setFrozen(this.freezeProxy); return proxyFactory.getProxy(classLoader); }ProxyFactory.getProxy最终会调DefaultAopProxyFactory.createAopProxy得到JdkDynamicAopProxy或ObjenesisCglibAopProxy然后调用getProxy生成真正的代理对象。这里有个容易踩的坑如果目标类没有实现任何接口evaluateProxyInterfaces不会注册任何接口那么DefaultAopProxyFactory判断hasNoUserSuppliedProxyInterfaces为 true会强行选择 CGLIB。所以即使你不开proxyTargetClasstrue一个完全没接口的类也能被代理只是形态从 JDK 代理变成了 CGLIB 代理。2.4 CGLIB 代理的独特之处方法重写与拦截器链如果走 CGLIBObjenesisCglibAopProxy会通过 ASM 字节码生成目标类的子类并重写可代理的方法。每个被重写的方法都会生成一个DynamicAdvisedInterceptor也就是AopProxy内部回调当代理对象的方法被调用时会进入DynamicAdvisedInterceptor.intercept。这个拦截器和 JDK 代理里的JdkDynamicAopProxy.invoke逻辑几乎一样都是从AdvisedSupport中取出advisors经过DefaultAdvisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice把 Advisor 转换成MethodInterceptor列表再封装成一个ReflectiveMethodInvocation执行链。区别仅仅在于生成代理类的方式不同执行逻辑完全一致。所以后续讲“通知链怎么执行”对 JDK 和 CGLIB 是通用的你可以把心态放宽不用各记一套。3. 从配置到执行一次切面调用的完整生命线3.1 一个可复现的切面示例需求与配置纸上谈兵不如动手实操。下面这个例子非常贴近日常开发给订单服务的createOrder方法增加方法耗时统计和操作日志。定义一个切面类Aspect Component Order(1) public class OrderLogAspect { Pointcut(execution(* com.example.demo.service.OrderService.createOrder(..))) public void orderCreatePointcut() { } Before(orderCreatePointcut()) public void logBefore(JoinPoint joinPoint) { String methodName joinPoint.getSignature().getName(); System.out.println([before] 开始调用: methodName); } AfterReturning(value orderCreatePointcut(), returning result) public void logAfterReturning(JoinPoint joinPoint, Object result) { System.out.println([afterReturning] 返回结果: result); } Around(orderCreatePointcut()) public Object logAround(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); try { Object result pjp.proceed(); return result; } finally { long cost System.currentTimeMillis() - start; System.out.println([around] 方法耗时: cost ms); } } }做这样一个切面时要注意一个动作给切面类加上Order(1)。如果没有显式声明Spring 对同切面内的不同通知也有一个内置顺序但跨切面时顺序完全取决于 Bean 加载顺序容易出问题。我在实际项目里见过两次日志切面同时生效控制台打印顺序跟预期相反最后查出来就是因为没设置Order。配置方面如果用的是 Spring Boot不需要额外加EnableAspectJAutoProxy因为 Boot 的自动配置已经注册了AnnotationAwareAspectJAutoProxyCreator。如果是传统 Spring XML 项目需要显式加上aop:aspectj-autoproxy/或EnableAspectJAutoProxy注解。3.2 容器启动阶段到底发生了什么断点视角看代理创建当我们启动 Spring 容器加载OrderService时会经过BeanFactory的创建流程。这里推荐你直接在AbstractAutoProxyCreator.postProcessAfterInitialization打一个条件断点条件写成beanName.equals(orderService)能看到以下事实orderService被普通方式创建出来是一个真实的OrderServiceImpl实例进入wrapIfNecessary后findEligibleAdvisors拿到了我们定义的OrderLogAspect里解析出的三个 Advisor分别对应Before、Around、AfterReturningspecificInterceptors ! DO_NOT_PROXY成立进入createProxy最终返回的orderService已经不是原始对象而是CGLIB生成的代理对象类名看起来像OrderServiceImpl$$EnhancerBySpringCGLIB$$123abc。如果你不去看源码只靠 Debug 观察到这一步可能只会觉得“诶Bean 怎么变了”。但一旦理解了postProcessAfterInitialization的位置你就明白这是 IOC 生命周期里“最后一个后处理器”在起作用。它发生在 Bean 属性填充和初始化之后因此 AOP 能拦截到后续所有对 Bean 方法的调用但是已经执行完的PostConstruct、InitializingBean方法不会被切面拦截。这一点也是很多人忽略的AOP 只能代理“外部调用”类型内部的this调用和生命周期回调调用是无法触达的自调用问题。3.3 调用阶段的通知链执行顺序当orderService.createOrder被外部调用时最终会进入DynamicAdvisedInterceptor.interceptCGLIB 路线或JdkDynamicAopProxy.invokeJDK 路线。二者都会调用AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice将 Advisor 列表转换为MethodInterceptor数组。Override Nullable public Object invoke(MethodInvocation invocation) throws Throwable { TargetAccessor targetAccessor (TargetAccessor) invocation; Object target targetAccessor.getTarget(); Method method invocation.getMethod(); Object[] arguments invocation.getArguments(); ... }结合ReflectiveMethodInvocation的proceed()方法链条执行顺序会是这样先执行拦截器链的第一个元素也就是我们在切面解析时得到的第一个Advice如果是ExposeInvocationInterceptor它会先把当前MethodInvocation放到ThreadLocalcurrentInvocation保证嵌套调用时也能拿到正确的MethodInvocation如果是AspectJMethodBeforeAdvice先执行Before逻辑然后调用invocation.proceed()进入下一个拦截器如果是AspectJAroundAdvice执行切面的Around方法切面内部的pjp.proceed()会继续驱动链路往下走目标方法真正执行完后返回结果一层层回传后置通知们在返回路径上各自执行。注意ExposeInvocationInterceptor在链路最前面它是 Spring 内部为了让JoinPoint能拿到当前调用上下文而插入的一个“内部拦截器”平时刷日志看到它不用慌。为了更直观我把转换之后的拦截器链顺序整理成一个表格按最坏情况展开顺序拦截器/通知类型做什么来源1ExposeInvocationInterceptor暴露当前 MethodInvocation 到 ThreadLocalSpring 内部自动添加2AspectJMethodBeforeAdvice执行Before逻辑OrderLogAspectlogBefore3AspectJAroundAdvice执行Around逻辑等待proceed()OrderLogAspectlogAround4MethodBeforeAdvice可能来自 XML 配置或事务增强其他 Advisor5目标方法OrderServiceImpl.createOrder真正执行业务无6AfterReturningAdvice正常返回后执行AfterReturningOrderLogAspectlogAfterReturning从表格可以直观看到Around虽然代码上写的是包裹整个方法但在执行链里它只是最外层控制者真正调用proceed()之前的前置逻辑先于Before执行proceed()之后的后置逻辑晚于AfterReturning执行。这解释了为什么在Around里可以通过手动不调用proceed()来“短路”整个方法而不影响链路里其他行为这是 AOP 最灵活的拦截控制点。3.4 为什么this方法调用绕过了 AOP 代理最后一个实操问题也是最经典的问题在同一个类里一个方法调用另一个被切面拦截的方法为什么切面不生效Service public class OrderServiceImpl implements OrderService { public void createOrder(OrderDTO order) { validate(order); save(order); } Loggable public void save(OrderDTO order) { // 保存逻辑 } }这里的createOrder方法内部直接调用this.save()。this指向的是代理对象内部持有的原始目标对象而不是代理对象本身。也就是说调用链根本没有经过DynamicAdvisedInterceptorsave方法上的Loggable自然不会被解析切面也就完全不执行。这个问题的定位经验是打断点放在save方法第一行看调用栈最外层是不是代理对象的入口如果栈里没有CGLIB或JdkDynamicAopProxy这类类名基本就是this自调用。解决的几个常见方案从容器中获取代理对象用代理对象调用save比如AopContext.currentProxy()把被切面的方法拆到另一个 Bean 里通过依赖注入调用让调用经过代理如果是Transactional失效场景也可以通过事务同步管理器等机制变相规避但最推荐的做法是拆分 Bean语义更清晰。4. 高频故障排查切面不生效、自调用失效、通知顺序错乱4.1 切面不生效的六步排查法我排查“AOP 不生效”问题已经形成一套固定的排查路径基本几分钟内能定位到具体原因分享出来给你参考。第一步确认切面类是否被 Spring 管理。检查切面类上有没有Component或者是否通过Bean注册。没有注册的话AnnotationAwareAspectJAutoProxyCreator根本扫不到它其他一切无从谈起。断点位置建议打在buildAspectJAdvisors上观察切面实例列表里有没有你定义的切面类。第二步确认注解是否被正确解析。打开ReflectiveAspectJAdvisorFactory的getAdvisor方法看是否找到了Around、Before这些注解。一个容易忽略的点是Aspect类上还必须完成Component扫描如果你是手动 new 出来的切面对象Spring 也不会扫描到它的方法。第三步确认切点表达式是否匹配目标方法。可以临时写一个最简单的execution(* com.example..*.*(..))来排除表达式写错的可能。execution表达式的粒度特别容易漏*号只匹配一个方法名段或类型段..才能匹配多层包路径或参数列表我见过有人把execution(* com.example.demo.service.*.*(..))与execution(* com.example.demo.service..*.*(..))搞混结果 service 包下的子包方法全部不匹配。第四步确认目标方法/类是否可被代理。如果是 final 类、final 方法CGLIB 无法通过继承重写它们如果是 private 方法execution表达式默认根本不会匹配到私有方法因为 AOP 匹配的方法级信息来自getMethods()或getDeclaredMethods()私有方法即便能被反射拿到Spring 也会明确过滤掉。处理这种场景只能改设计或者把公共逻辑抽出来放到另一个可代理的类里。第五步确认代理模式和proxyTargetClass的设置。Spring Boot 默认spring.aop.proxytargetclasstrue所以大部分场景是 CGLIB。如果你强制设置成了 false而目标类没有接口或者目标类确实有接口但动态代理强转了实现类就会出现ClassCastException。更隐蔽的是某些第三方库内部对代理对象做了强转比如(OrderServiceImpl) proxy在 CGLIB 模式下没问题但在 JDK 代理模式会直接崩溃因为 JDK 代理类是接口实现类不是目标类子类。第六步也是最容易忽略的确认调用链路确实经过了代理对象。排查方法很简单在createOrder方法内获取AopUtils.isAopProxy(this)打印出来如果是 false说明this是原始对象。这种情况多半是从 Bean 内部直接调用或者通过反射绕过代理本质上还是发生了某种“自调用”。4.2 事务切面为什么不生效一个高频经典案例在日常工作中Transactional失效是最常被 AOP 机制影响的场景。Transactional本身就是一个基于 AOP 实现的事务管理切面它用了TransactionInterceptor。当Transactional方法被外部调用时TransactionInterceptor会开启事务、执行目标方法、提交或回滚。但如果事务方法被同类下的其他方法调用自调用this指向原始对象事务切面根本进不去。典型的代码长这样Service public class AccountService { Transactional public void transfer() { accountDao.debit(); accountDao.credit(); } public void doTransfer() { this.transfer(); } }把断点打在AccountService.transfer上观察调用栈里如果没有TransactionInterceptor那就说明事务没被触发。一个常见的错误修法是在方法内部手动 new 一个AccountService或者使用this.transfer()这不会改变“this 是原始对象”的事实。正确做法要么把transfer移到另一个Service类里要么注入AccountService的代理对象要么通过AopContext.currentProxy()拿到当前代理再调用。这里补充一个排查小技巧在启动日志里把spring.aop.proxy-target-class和spring.aop.auto两个属性打出来。前者决定走 JDK 还是 CGLIB后者决定是否启用自动代理。Spring Boot 2.x 之后spring.aop.auto默认就是 true但在某些自定义配置的 Spring 项目里可能被关掉关掉后Transactional和Aspect会同时失效。4.3 多切面通知顺序控制Order与执行链的组合法则多个切面同时作用在同一个目标方法上时执行顺序由Order注解的值决定。值越小优先级越高越先进入链路。一个容易混淆的点是Order的数值小前置通知先执行但后置通知反而是最后执行的因为整个执行链就像洋葱最外层先进后出。具体到Around和Before假设两个切面 AOrder(1)和 BOrder(2)都定义了Before和AfterReturning那么执行顺序是A.before - B.before - 目标方法 - B.afterReturning - A.afterReturning这个和“先进后出”的栈结构一致。如果两个切面使用同样Order顺序就会变得不稳定受 Bean 扫描和排序影响。所以规范的团队里都会约定切面的Order分区间比如日志切面统一用 10缓存切面用 20事务切面用Ordered.LOWEST_PRECEDENCE。这样能保证后续加切面时不用改动既有代码。4.4 一次真实的线上排查AOP 不生效最后查到一个简单原因最后分享一个我印象很深的案例。线上有个支付回调服务里面的Async注解偶尔不生效。排查过程很曲折先查了线程池配置再查了EnableAsync注册都没问题。最后在代理对象上打断点发现这个回调服务根本不是 Spring 的代理对象。根因是支付回调的入口类被开发手动new出来而不是从 Spring 容器里拿 Bean。Async和Transactional、自定义 AOP 一样都依赖代理机制手动 new 出来的对象不经过postProcessAfterInitialization自然不会被代理注解全部失效。这件事让我养成了一个习惯凡是使用任何声明式框架特性事务、异步、缓存、安全的类一律通过 Spring 容器获取 Bean绝不在业务代码里new这类对象。排查时优先看对象是不是代理这也是最快速定位 AOP 相关问题的思路。4.5 速查表AOP 高频问题快速定位现象可能原因优先排查位置自定义切面完全不执行切面未注册 / 表达式不匹配buildAspectJAdvisors()、切点表达式Transactional失效自调用 / 方法非 public / 异常被吞调用栈有没有TransactionInterceptorAsync不生效手动 new Bean / 代理未启用调用对象是否代理类ClassCastException强转实现类 JDK 动态代理proxyTargetClass设置多切面顺序错乱未设置Order/ 值未留区间Order注解及排序算法接口中的 default 方法不被增强代理类未继承接口默认方法proxyTargetClass与接口设计controller 层 AOP 不生效未交给 Spring 容器 / AOP 未被正确扫描SpringBoot 自动配置是否托底实际开发中约八成 AOP 问题都能通过“确认对象是不是代理对象”“确认切点表达式是否匹配”“确认切面是否被容器管理”这三板斧解决。剩下的两成基本都是需要深入源码或排查框架冲突的场景但只要定位到代理创建链路这一步问题基本就明朗了。个人体会把 AOP 源码当成一张地图而不是一座孤岛我在实际使用 Spring AOP 的过程中最大的感受是不要单独理解 AOP它跟 IOC 生命周期、BeanPostProcessor、代理模式、事务抽象甚至 Spring Boot 的自动配置都是连在一起的。你能看到AbstractAutoProxyCreator挂着BeanPostProcessor说明 AOP 深度嵌在 Bean 生命周期里你能看到AdvisedSupport同时被ProxyFactory和切面解析器持有说明 AOP 的配置模型是统一抽象的你能看到ReflectiveMethodInvocation把拦截器链串成一条循环说明哪怕通知再多最终执行逻辑仍然是一个递归遍历。理解这些连接点比记住几个类名和源码行号有价值得多。最后再分享一个小技巧在 Debug 代理链路时给AbstractAutoProxyCreator.wrapIfNecessary和ReflectiveMethodInvocation.proceed()各打一个条件断点前者条件写目标 BeanName后者条件写目标方法名。观察一次请求下来在这两个断点上的命中次数能非常直观地看到“代理是否创建成功”和“通知链是否完整执行”。这个技巧我用了很久排查 AOP 问题时能省至少一半时间。