ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring AOP源码拆解:从代理创建到拦截器链的完整流程

Spring AOP源码拆解:从代理创建到拦截器链的完整流程 Spring 的 AOP 源码我看过好几遍但说实话真正让我通了的不是跟着断点一步步走而是先把“代理创建”和“方法拦截”这两件事彻底分开。很多朋友一上来就扎进ProxyFactory或者CglibAopProxy里面结果越看越懵因为源代码里类太多、调用太绕一个wrapIfNecessary能把你带到五六个类里面去。这篇文章我想换个讲法从业务痛点出发顺着 Spring 处理 AOP 的两条主线——切面如何变成代理、代理如何拦截方法——把这个过程完整拆开。全程以 Spring Framework 5.x 的源码为例我会标注关键类、关键方法并附上我在实际调试中的断点位置和踩坑经验。适合刚啃完 Spring IOC 源码、准备攻 AOP 的同学也适合面试前想理清“AOP 原理”这条线的朋友。1. 先明确 AOP 到底解决什么问题网上聊 AOP 的文章很多但大多数一上来就列概念切面、通知、切点、连接点搞得跟背单词一样。我建议先把场景想明白再回头对照概念你会发现每个词都落在实处。1.1 横切逻辑的前世今生你在一个老项目里会发现这样的代码某个 service 方法开头打印日志中间写业务结尾打印耗时然后 try-catch 里可能还要做权限校验、事务提交。如果你有二十个 service就得把这套代码复制到二十个地方。更麻烦的是需求一变比如日志格式从“短横线分隔”改成“JSON 输出”你得全部搜索替换漏改一个就是线上事故。这种“散落在业务代码里却又跟业务无关”的逻辑我们称为横切逻辑。它们有一个共同点不属于任何一门业务却横跨了所有业务模块。典型代表就是日志、事务、权限、缓存、审计。AOP 的核心思路很简单把横切逻辑抽出来放到一个统一的地方然后通过“织入”的方式让这些逻辑在合适的方法调用时机自动执行。业务代码里不需要写任何重复样板甚至不需要知道这些横切逻辑的存在。1.2 从代理模式看 AOP 的本质AOP 的底层本质上就是代理模式。你调用目标对象的方法时实际上拿到的可能是一个代理对象代理对象在调用真实方法的前后插入额外逻辑。举个例子public interface UserService { void createUser(String name); } public class UserServiceImpl implements UserService { Override public void createUser(String name) { System.out.println(创建用户 name); } }如果不改动UserServiceImpl的源码又想给createUser加上日志最土的办法就是写一个代理类public class UserServiceLogProxy implements UserService { private final UserService target; public UserServiceLogProxy(UserService target) { this.target target; } Override public void createUser(String name) { System.out.println(【日志】开始创建用户参数 name); target.createUser(name); System.out.println(【日志】创建用户完成); } }Spring AOP 就是把这个过程自动化、通用化你不用为每个接口手写代理类只要声明“哪些方法需要什么增强”Spring 在 Bean 创建阶段自动帮你生成代理对象。理解这一点后再看源码里的各种类就顺了——ProxyFactory就是那个“生成代理对象”的工厂AspectJExpressionPointcut就是用来匹配“哪些方法需要增强”的规则MethodBeforeAdvice、AfterReturningAdvice这些就是被抽出来的“额外逻辑”。2. 源码入口一个注解如何拉起整个 AOP 体系Spring AOP 的入口其实藏在一个注解里EnableAspectJAutoProxy。绝大多数 Spring Boot 项目没有显式加这个注解因为spring-boot-autoconfigure里的 AOP 自动配置类会帮你加上。但研究源码时从这个注解入手是最清晰的。2.1 EnableAspectJAutoProxy 干了什么打开EnableAspectJAutoProxy的源码你会发现它其实就是个元注解的组合Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Documented Import(AspectJAutoProxyRegistrar.class) public interface EnableAspectJAutoProxy { boolean proxyTargetClass() default false; boolean exposeProxy() default false; }核心是Import(AspectJAutoProxyRegistrar.class)。AspectJAutoProxyRegistrar实现了ImportBeanDefinitionRegistrar它的作用是在容器启动时向 BeanDefinitionRegistry 注册一个关键的 BeanDefinition。注册的 Bean 是什么就是AnnotationAwareAspectJAutoProxyCreator。这是整个 Spring AOP 的“大脑”。你可以通过 debug 来看这一步在AspectJAutoProxyRegistrar.registerBeanDefinitions方法打断点会看到它内部判断一下容器里有没有AUTO_PROXY_CREATOR_BEAN_NAME即org.springframework.aop.config.internalAutoProxyCreator这个 Bean 定义没有就注册。这个方法里有个故意设计的小细节registerOrEscapeBeanDefinitionAlias如果 BeanDefinition 冲突它会把自己的注册改名成org.springframework.aop.config.internalAutoProxyCreator加上后缀避免覆盖真正的配置。2.2 核心角色AnnotationAwareAspectJAutoProxyCreator先看继承关系这条链路非常重要AnnotationAwareAspectJAutoProxyCreator extends AspectJAwareAdvisorAutoProxyCreator extends AbstractAdvisorAutoProxyCreator extends AbstractAutoProxyCreator implements InstantiationAwareBeanPostProcessor, BeanFactoryAware这个继承结构几乎把所有核心能力都串起来了。AbstractAutoProxyCreator实现了SmartInstantiationAwareBeanPostProcessor这是一个BeanPostProcessor。Spring 容器在创建 Bean 的过程中凡是符合“后置处理”的时机都会回调它。postProcessAfterInitialization就是其中的关键回调AOP 代理主要在这里触发。AspectJAwareAdvisorAutoProxyCreator增加了对 AspectJ 注解的支持它能识别Aspect注解标注的类并把里面的Before、After、Around等方法解析成 Spring AOP 能识别的通知。AnnotationAwareAspectJAutoProxyCreator则是最终的应用类它把“注解扫描”和“AspectJ 解析”都集成到一起。这里有个概念容易混淆Aspect这个注解是 AspectJ 框架的但 Spring 只是借用了它的注解语法底层解析和执行机制还是 Spring 自己的。所以面试问“AOP 和 AspectJ 区别”时别答成“Spring AOP 就是 AspectJ”它们只是语法层面的借用关系。我在调试时通常会直接在这个类名上打条件断点条件是beanName.equals(userService)然后看它是在哪个阶段被调用的。这样能非常直观地感受 AOP 和 Bean 生命周期是如何耦合的。3. 切面筛选与代理创建全过程理解了入口后接下来是核心中的核心Spring 如何为一个普通 Bean 生成代理对象。整个过程分三步找到候选切面、筛选适用的通知、创建代理。3.1 从 postProcessAfterInitialization 到 wrapIfNecessary当 Spring 容器创建一个 Bean 并完成初始化后会调用所有BeanPostProcessor的postProcessAfterInitialization。AbstractAutoProxyCreator重写了这个方法Override public Object postProcessAfterInitialization(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; }这段代码有两个关键点第一earlyProxyReferences这个集合是用来解决循环依赖的。如果当前 Bean 在“早期暴露”阶段已经被提前代理过那这里就不会再次包装直接返回原 bean避免重复代理。第二wrapIfNecessary是真正干活的方法。它的名字取得很形象如果需要包装就包装不需要就原样返回。wrapIfNecessary内部的核心逻辑可以概括为判断是不是基础设施类Advice、Advisor、AopInfrastructureBean等如果是直接返回。判断 Bean 是否需要代理通过getAdvicesAndAdvisorsForBean去查找适用通知。如果有适用的通知就调用createProxy生成代理对象。3.2 Advisor 的收集过程进入getAdvicesAndAdvisorsForBean后实际调用的是AbstractAdvisorAutoProxyCreator.findEligibleAdvisorsprotected ListAdvisor findEligibleAdvisors(Class? beanClass, String beanName) { ListAdvisor candidateAdvisors findCandidateAdvisors(); ListAdvisor eligibleAdvisors findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); extendAdvisors(eligibleAdvisors); if (eligibleAdvisors.isEmpty()) { eligibleAdvisors null; } return eligibleAdvisors; }两步走先找所有候选 Advisor再筛出能应用于当前 Bean 的那部分。findCandidateAdvisors在AnnotationAwareAspectJAutoProxyCreator里的实现要复杂一些。它通过BeanFactoryAdvisorRetrievalHelper从容器中取出所有Advisor类型的 Bean再通过BeanFactoryAspectJAdvisorsBuilder扫描容器中所有Aspect标注的 Bean把注解方法解析成InstantiationModelAwarePointcutAdvisor。这一步你会在源码里看到大量名字带AspectJ的类AspectJExpressionPointcut、AspectJMethodBeforeAdvice、AspectJAroundAdvice。它们的作用就是把注解上的表达式和配置转换成 Spring 内部可以理解、执行的数据结构。findAdvisorsThatCanApply则是遍历候选 Advisor调用AopUtils.canApply判断当前 Bean 是否匹配。public static boolean canApply(Pointcut pc, Class? targetClass, boolean hasIntroductions) { // 先做类级别匹配 if (!pc.getClassFilter().matches(targetClass)) { return false; } // 再做方法级别匹配 MethodMatcher methodMatcher pc.getMethodMatcher(); ... }类过滤器先拦一道方法匹配器再逐个比对。这里要注意AspectJExpressionPointcut的matches非常耗时Spring 内部会做ShadowMatch缓存但很多同学在调试时容易忽略这一点以为每次匹配都是即时计算。3.3 代理类型选择JDK 动态代理还是 CGLIB找到适用的 Advisor 后createProxy会创建ProxyFactory然后配置目标对象、Advisor 列表最后调用getProxy获取代理对象。真正决定代理类型的代码在DefaultAopProxyFactory.createAopProxypublic AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { 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); } }这里有几个分支条件很容易被忽视proxyTargetClass为 true强制使用 CGLIB 代理走 CGLIB 分支。没有用户自定义的代理接口也就是说目标类没有实现任何接口那也只能走 CGLIB。目标类是接口本身或者已经是一个 JDK 动态代理类走 JDK 动态代理。其他情况默认走 JDK 动态代理。JDK 动态代理有一个硬性要求被代理对象必须实现至少一个接口。如果你的业务类没有实现接口但也没设置proxyTargetClasstrueSpring 会因为你没有用户接口而自动退回到 CGLIB。这也是为什么很多场景下“没有实现接口也能被代理”的原因——不是 JDK 动态代理能做而是 Spring 悄悄切到了 CGLIB。CGLIB 的底层是操作字节码直接生成了目标类的子类所以它不要求目标类实现接口但是也不能代理被final修饰的类和方法。这一点在 Spring Boot 2.x 之后尤其重要因为 Boot 2.x 默认spring.aop.proxy-target-classtrue。我在生产环境里就踩过一次坑有个类被final修饰同事在上面加了Transactional结果事务完全不生效查了半天才发现是 CGLIB 无法继承 final 类代理对象压根没生成成功。这种问题编译期不会报错运行时日志也未必有明确提示排查起来相当隐蔽。4. 拦截器链的调用与织入细节代理对象创建完成后调用目标方法时就会进入代理的拦截方法。这一层就是 AOP 运行的动态链路也是面试里最容易问出细节的地方。4.1 ReflectiveMethodInvocation 如何推进调用链以 JDK 动态代理为例核心入口是JdkDynamicAopProxy.invokeOverride public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { ... ListObject chain this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); if (chain.isEmpty()) { // 没有通知直接反射调用目标方法 retVal AopUtils.invokeJoinpointUsingReflection(this.target, method, args); } else { // 有通知构建调用链 MethodInvocation invocation new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); retVal invocation.proceed(); } ... }这里最关键的概念就是chain它是一个通知列表里面每个元素的类型是MethodInterceptor。你可能会有疑问前面说的Before、After不是Advice吗怎么变成MethodInterceptor了答案在DefaultAdvisorAdapterRegistry里。Spring 内置了几个适配器MethodBeforeAdviceAdapter、AfterReturningAdviceAdapter、ThrowsAdviceAdapter它们会把不同类型的Advice包装成对应的MethodInterceptor。比如MethodBeforeAdvice会被包装成MethodBeforeAdviceInterceptor内部逻辑就是“先执行 advice 的 before 方法再调用下一个拦截器”。ReflectiveMethodInvocation.proceed()是整个调用链的执行器Override public Object proceed() throws Throwable { if (this.currentInterceptorIndex this.interceptorsAndDynamicMethodMatchers.size() - 1) { return invokeJoinpoint(); } Object interceptorOrInterceptionAdvice this.interceptorsAndDynamicMethodMatchers.get(this.currentInterceptorIndex); return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); }这段代码看起来简单但这是理解“责任链模式”的关键。每个拦截器拿到MethodInvocation后可以选择执行自己的逻辑再调用invocation.proceed()放行下一个也可以直接短路不再放行比如Around里没有调用pjp.proceed()后面的逻辑就不会执行。我用生活化的例子解释一下拦截器链就像公司里的审批流。你提交一个申请方法调用第一个审批人MethodBeforeAdvice看一眼没问题把单子传给下一个人中间有一个会签领导Around他可以决定“我同意了继续传”或者“我不同意直接打回”最后到出纳那里目标方法钱打出去回执再一级一级传回来。4.2 通知执行顺序是怎么保证的通知的执行顺序是 AOP 面试里最高频的追问点之一。先说结论同一 Aspect 内不同类型的通知有固定的先后顺序Around-Before- 目标方法 -AfterReturning/AfterThrowing-After。不同 Aspect 之间按Order注解或Ordered接口的 order 值决定顺序order 越小越先执行。若 order 相同执行顺序取决于AspectJ的优先级规则不同版本可能有细微差别常规项目里最好显式指定 order。源码层面ReflectiveMethodInvocation在创建时会把 Advisor 按照AdvisorAdapterRegistry转换后排序。排序逻辑在AdvisorComparator里核心就是比较 order 值。这里有个坑容易被忽略Around和Before的“顺序”在不同的执行阶段是反的。比如 A 切面 order1B 切面 order2进入阶段A 的 Around 先执行然后 A 的 Before接着 B 的 AroundB 的 Before最后目标方法。退出阶段反向执行B 的 AfterAfterReturning / After 先执行然后才是 A 的对应通知。原因也好理解因为 Around 包裹了整条链先进入的先出就像一个套娃最外层最后打开。理解了这个你在写多切面代码时就不会对顺序感到迷惑。4.3 invoke 方法里容易被忽略的细节JdkDynamicAopProxy.invoke开头有一段特殊方法处理逻辑这也是很多人不会注意的if (!this.equalsDefined AopUtils.isEqualsMethod(method)) { return equals(args[0]); } else if (!this.hashCodeDefined AopUtils.isHashCodeMethod(method)) { return hashCode(); } else if (method.getDeclaringClass() DecoratingProxy.class) { ... } else if (!this.advised.opaque method.getDeclaringClass().isInterface() method.getDeclaringClass().isAssignableFrom(Advised.class)) { // 处理 Advised 接口的方法 return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args); }也就是说equals和hashCode这两个方法不会走拦截器链代理对象自己处理了。如果你在业务里希望通过 AOP 拦截equals方法是拦不到的。另外还有Advised接口的处理这意味着你能把代理对象强转成Advised然后动态增删通知Advised advised (Advised) proxy; advised.addAdvice(new AfterReturningAdvice() { ... });这种动态增强在生产上不常用但排查问题或做测试框架时非常有用。说完调用链我还想补充一个概念interceptorsAndDynamicMethodMatchers。这个列表里除了普通的MethodInterceptor还有一种元素是InterceptorAndDynamicMethodMatcher。它表示“这个拦截器还需要动态匹配方法”也就是说某些切点的匹配结果依赖运行时参数无法在 Bean 创建阶段静态确定。这时 Spring 会在调用链中额外做一次运行时匹配匹配不过就直接跳过该拦截器。5. 实战中的典型坑与排查思路源码看懂了只是第一步。真正让 AOP“看起来懂了却用不起来”的往往是几个经典坑。我把这几年踩过的和帮别人排查过的问题集中整理一下。5.1 自调用失效问题最经典的问题同一个类内部A 方法调用 B 方法B 方法上的Transactional或Around不生效。Service public class OrderService { public void createOrder() { // 内部调用 this.doSomething(); } Transactional public void doSomething() { // 事务不会生效 } }原因很简单createOrder里调用的是this.doSomething()而this是原始对象不是 Spring 容器里那个 AOP 代理对象。代理逻辑只在“从外部拿到的代理对象上调用方法”时才会触发。解决方案有三种注入自身代理Autowired private OrderService self;然后self.doSomething()。使用ExposeProxy模式在配置里设置exposeProxy true然后((OrderService) AopContext.currentProxy()).doSomething()。拆成两个 Bean互调。方案 2 在 Spring Boot 里可以这样配置EnableAspectJAutoProxy(exposeProxy true)然后在调用处((OrderService) AopContext.currentProxy()).doSomething();注意AopContext.currentProxy()内部是ThreadLocal存储的所以跨线程调用会拿到 null这又带来另一个限制。5.2 循环依赖与提前暴露AOP 和循环依赖的组合问题也很容易踩雷。Spring 通过三级缓存解决循环依赖但这其中涉及“早期 Bean 暴露”。当一个 Bean 正在创建且处于循环依赖链中其他 Bean 可能提前拿到它的“半成品引用”。如果这个 Bean 需要 AOP 代理Spring 会提前生成代理吗答案是会。在AbstractAutoProxyCreator里有一个getEarlyBeanReference方法Override public Object getEarlyBeanReference(Object bean, String beanName) { Object cacheKey getCacheKey(bean.getClass(), beanName); this.earlyProxyReferences.put(cacheKey, bean); return wrapIfNecessary(bean, beanName, cacheKey); }看到这里你应该明白循环依赖场景下早期暴露的可能就是代理对象而这个代理对象是“提前代理”的。之后postProcessAfterInitialization阶段会遇到earlyProxyReferences里的记录直接跳过避免重复代理。这个机制能工作但有个前提提前代理时那些后续才会被注册的 Advisor 还没有生效。也就是说如果某个切面是在循环依赖阶段之后才注册的它可能不会被织入到提前生成的代理里。这属于高级场景排查时如果发现“事务代理有了但业务切面没生效”可以往这个方向查。5.3 代理类失败的常见原因代理创建失败或切面不生效通常集中在几个原因目标类或方法是final的CGLIB 无法继承或覆写。切点表达式写错最常见就是execution(* com.example.service.*.*(..))里的包名层级搞错或者参数个数写错。Advisor 没有被 Spring 扫描到比如切面类没有被 Spring 管理没加Component或没有被配置扫描到。方法不是public的JDK 动态代理只代理接口方法CGLIB 默认无法代理private方法。代理的是类而不是接口但方法上加了final关键字。排查时我一般先看控制台有没有BeanCreationException或CglibSubclassingMessage相关日志再用一个最简单的Around配合execution(* com.example..*.*(..))打印日志确认切面本身能触发再一步步缩小范围。我强烈建议在开发环境开启 Spring AOP 的 debug 日志logging.level.org.springframework.aopDEBUG logging.level.org.springframework.beans.factoryDEBUG这样能看到类似Creating implicit proxy for bean xxx with 1 common interceptors的日志能直接告诉我们代理是否创建成功、拦截器有多少个。分享一个我从生产里攒出来的心得排查 AOP 失效问题永远先确认代理对象到底长什么样。只要你能确认拿到的对象是代理问题大概率出在切点表达式如果拿到的对象是原始对象问题大概率出在框架配置或自调用。6. 一份源码学习路线建议讲完原理和坑最后分享下我当时啃源码的方法。AOP 这块代码量大、类多没有方法论确实容易迷失。6.1 最小化断点调试法不要从一开始就全源码断点。我推荐沿着一条最小链路打断点跑一个最简单的 AOP 示例。第一步准备好一个最小可复现工程一个接口UserService一个实现类UserServiceImpl一个Aspect类里面写一个Around和一个Before一个Configuration配置类显式加EnableAspectJAutoProxy一个启动类从容器里取出UserService调用createUser方法第二步在下面这些位置打断点按顺序看AspectJAutoProxyRegistrar.registerBeanDefinitions—— 看注册入口AbstractAutoProxyCreator.postProcessAfterInitialization—— 看代理触发点AbstractAdvisorAutoProxyCreator.findEligibleAdvisors—— 看切面筛选DefaultAopProxyFactory.createAopProxy—— 看代理类型选择JdkDynamicAopProxy.invoke或CglibAopProxy.DynamicAdvisedInterceptor.intercept—— 看方法拦截ReflectiveMethodInvocation.proceed—— 看拦截器链每到一个断点先看调用栈能帮你理清是谁调了谁每次F6单步跳过时留意 IDEA 的变量面板里各个对象的状态尤其是chain列表里有几个拦截器、类型分别是什么。这个阶段能跑通一遍后再去看Transactional这类注解如何把TransactionInterceptor织入切面。6.2 从手写 AOP 到理解 Spring 的升华如果你有时间我建议自己写一个极简 AOP 框架不用解析复杂表达式只做三件事定义一个MyBefore注解和MyAround注解写一个AnnotationBeanPostProcessor扫描带MyAspect的类在postProcessAfterInitialization里创建 JDK 动态代理当你真正写出这段代码你会发现 Spring AOP 的所有核心决策都融在你的几十行代码里。然后你再回去看AnnotationAwareAspectJAutoProxyCreator就不再是“一堆陌生类”而是“一个成熟框架里更完备的实现”。我当年手写完之后对几个原本模糊的概念瞬间就通了为什么Aspect类本身不会被代理因为基础设施类要排除为什么Around里必须pjp.proceed()才能调用目标方法为什么代理创建发生在postProcessAfterInitialization而不是postProcessBeforeInstantiation所谓源码分析不是把代码读一遍而是把一个复杂系统的设计意图理解透。Spring AOP 这套东西从设计到实现都是“代理模式 责任链模式 后处理器扩展点”的组合拳搞清楚这三板斧你甚至不需要背源码细节也能在面试里从容回答。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进