人人都会AI编程

5.3 Spring AOP 代理对象创建流程

更新时间:2026-07-11

理解了 AOP 的“切面织入”概念之后,有一个问题自然浮现:Spring 在什么时候、用什么方式为我们的 Bean 生成代理对象? 这个过程的透明性正是 Spring AOP 实用性的关键。当你在 Bean 上标注 @Transactional,或在配置中声明切面时,容器就默默地执行了一套完整的代理创建流程。本节将把这条链路拆解开来,让你既能在面试中从容作答,也能在出现 AOP 失效问题时快速定位。

5.3.1 代理创建的入口:BeanPostProcessor

Spring IoC 容器在初始化每个 Bean 时,会按照固定的生命周期依次调用注册的所有 BeanPostProcessor。AOP 代理的创建,正是利用了其中一个至关重要的后处理器——AbstractAutoProxyCreator

它的工作时机在 Bean 完成属性填充、初始化回调(@PostConstruct 等)之后,此时原始的 Bean 实例已经基本可用,AbstractAutoProxyCreator 会拦截这个 Bean,根据条件决定是否将其包装为代理。

这里有一个非常实用的点:如果你发现某个 Bean 的 AOP 通知没有生效,首先检查它是否被 Spring 容器管理,其次检查 Bean 初始化过程是否提前返回了原始对象,导致 BeanPostProcessor 没机会创建代理。例如,在自己调用 new 对象或过早的 @PostConstruct 中调用同类方法,都可能避开代理。

5.3.2 分步解析:从筛选到生成代理

整个代理创建流程可以概括为以下五个核心步骤,每一步都由 AbstractAutoProxyCreator 的相关方法驱动。

1. 查找所有候选增强器 (Advisors)

首先,容器会从当前的 ApplicationContext 中收集所有实现了 Advisor 接口的 Bean,包括:

  • @Aspect 注解解析出来的切面 Bean 所包含的通知方法(每个通知方法都会被包装为一个 Advisor);
  • 直接注册为 Bean 的拦截器或增强器(如 TransactionInterceptor 之于事务)。

这一步的关键方法是 shouldSkipgetAdvicesAndAdvisorsForBean,它会保证既不漏掉任何增强逻辑,又能利用缓存避免重复解析。

2. 筛选适用于当前 Bean 的 Advisor

收集到全量 Advisor 后,容器要判断哪些增强切实需要应用到当前正在处理的 Bean 上。筛选的依据就是切点表达式(Pointcut)。每个 Advisor 都关联着一个切点(比如 execution( com.example.service...*(..))),Spring 会使用这个切点对 Bean 的类和方法进行匹配。

  • 类级别匹配:先判断当前 Bean 的类是否落在切点关注的范围内,快速排除大部分不相关的增强。
  • 方法级别匹配:如果类匹配成功,再对 Bean 中的每个方法进行精确匹配,找出哪些方法需要织入通知。

这一步完成后,你会得到一个 specificInterceptors 列表——其中每一个都是将要在目标方法上执行的增强。

3. 决定是否需要创建代理

如果经过筛选,没有任何 Advisor 匹配当前 Bean,AbstractAutoProxyCreator 会直接返回原始 Bean 实例,不做任何代理包装。这也就是“按需代理”的体现,避免无谓的性能开销。

如果存在匹配的 Advisor,Spring 会进入下一步,创建代理对象。

4. 创建代理工厂,设置目标对象与增强

Spring 会实例化一个 ProxyFactory,并将以下信息配置进去:

  • 被代理的目标对象(即原始 Bean)
  • 已筛选出的所有 Advisor
  • 其他配置(如是否暴露代理、是否冻结等)

然后由 ProxyFactory 决定具体的代理创建策略。默认情况下,它会检查目标类是否实现接口来决定采用哪种代理方式。

5. 根据策略生成代理对象

ProxyFactory 最终交出具体代理对象的是两个核心实现类:

  • JdkDynamicAopProxy:如果目标类实现了至少一个接口,且没有强制指定使用 CGLIB,则采用 JDK 动态代理。这种代理基于接口,只能拦截接口中定义的方法。
  • CglibAopProxy:如果目标类没有实现接口,或者显式配置了 proxy-target-class="true",则使用 CGLIB 通过字节码生成目标类的子类来实现代理。它可以拦截继承到的非 final 方法,但 final 方法无法被代理。

特别说明:Spring Boot 从 2.0 开始,对于 @Configuration 配置类默认强制使用 CGLIB,而在一般的 Service 上则遵循上述规则。在 AOP 场景下,你也可以通过 spring.aop.proxy-target-class=true 全局启用 CGLIB 代理。

生成的代理对象会被返回给容器,替换原始的 Bean 实例并注册在单例池中。此后,所有对这个 Bean 的引用(包括从容器获取和依赖注入)拿到的都是代理对象。

5.3.3 两种代理机制的选择与调试

理解 JDK 动态代理和 CGLIB 代理的差异,对排查问题和性能调优很有意义。

| 维度 | JDK 动态代理 | CGLIB 代理 |
|------|---------------|-------------|
| 要求 | 目标类必须实现接口 | 不要求接口,但不能是 final 类 |
| 拦截范围 | 只能拦截接口中的方法 | 可以拦截非 final 方法,包括实现接口的方法 |
| 性能 | 创建开销小,反射调用稍慢(现代 JVM 已优化) | 创建开销稍大,调用等同于子类方法,非常快 |
| 识别方式 | 代理类名前缀为 com.sun.proxy.$Proxy | 代理类名通常包含 $$EnhancerByCGLIB$$ |

实用调试技巧:当你怀疑某个 Bean 是否被成功代理时,可以在代码中输出它的 class.getName()。如果看到 JDK 代理或 CGLIB 类名,则表明代理已生效;如果仍是原始的 Service 类名,则说明 AOP 未被触发,需要检查切点表达式、Bean 的作用域以及是否被同类内部调用绕过。

5.3.4 代理对象的执行流程(简要预览)

代理对象最终要在方法调用时将请求“路由”到增强逻辑上。无论 JDK 还是 CGLIB 代理,真正的方法调用都会进到 AdvisedSupport 所维护的一个拦截器链。执行时,Spring AOP 会按顺序构造一串 MethodInterceptor 并封装为一个 ReflectiveMethodInvocation,然后依次调用每个拦截器,最后通过反射调用目标方法。

这个执行链的具体结构将会在下一节中展开,但对于代理创建流程,你只需要记住:代理对象封装了原始 Bean 和一组 Advisor,调用时的拦截顺序由 Advisor 的优先级和类型决定。

5.3.5 常见问题与实战提示

  • 同类的自我调用导致 AOP 失效:如果在同一个 Service 内,一个方法不通过代理直接调用 this.anotherMethod(),则 anotherMethod 上的通知不会被触发。修复方式是从容器中获取当前 Bean(例如通过 @Autowired 自注入或 AopContext.currentProxy())并使用代理调用,或将方法拆分到不同的 Bean 中。
  • 为什么有时候需要 @EnableAspectJAutoProxy(proxyTargetClass = true):在纯 Spring(非 Boot)环境下,要开启 CGLIB 代理需要显式设置。Spring Boot 中则可以通过 spring.aop.proxy-target-class=true 统一控制。
  • 代理创建顺序AbstractAutoProxyCreator 可能不止一个,比如事务和自定义切面同时存在时,容器的处理顺序是后处理器优先级高的先执行。这会导致多个代理层层包装——外层代理的方法调用进入内层代理,最终到达目标对象,理解这一点对多切面的执行顺序至关重要。

掌握了代理对象的创建流程,你就对 Spring AOP 的内部机制有了宏观而精准的认识。接下来,我们将深入代理调用链的细节,看看一条方法调用是如何一步步被通知包围并最终抵达实际业务逻辑的。