Spring AOP 与循环依赖:揭秘提前暴露代理的底层原理

引言

在Spring开发中,AOP和循环依赖是两个我们经常打交道的话题。通常的认知是,Spring会在一个Bean完全初始化(属性填充、init方法执行完毕)之后,才为其创建AOP代理。但当一个需要被代理的Bean,恰好又陷入了循环依赖,情况就变得棘手起来:Spring必须在这个Bean尚未”完工”时,就将它暴露给依赖方。

这就带来一个很自然的问题:一个尚未完全初始化的Bean,如何能以它最终的代理形态被提前暴露?这样做不会有状态不一致的风险吗?本文将以开发者的视角,深入Spring内部,看看它是如何通过精妙的三级缓存设计,解决这个看似矛盾的问题的。


1. 循环依赖的解决机制:三级缓存

要理解循环依赖的解决方案,核心就是要弄懂Spring的三级缓存。这三级缓存,本质上是Spring在Bean生命周期中,为了管理不同状态的Bean实例而设的三层存储空间:

  1. 一级缓存(singletonObjects):一个Map,存放的是完全初始化好的单例Bean。可以把它看作是”成品仓”,里面的Bean随时可以取用。
  2. 二级缓存(earlySingletonObjects):也是一个Map,存放的是提前暴露的单例Bean的早期引用。这些Bean已经实例化,但可能还没完成属性注入和初始化。它们是”半成品”,用于解开循环依赖。
  3. 三级缓存(singletonFactories):还是一个Map,但它存放的不是Bean,而是创建Bean的工厂ObjectFactory)。这是解决AOP循环依赖的关键,当Bean需要被代理时,这个工厂负责创建出代理对象,而不是原始对象。

当Bean A依赖Bean B,同时Bean B又依赖Bean A时,这个机制就开始运转:

  • Spring在创建Bean A时,首先会实例化A,然后将一个能够创建A的早期引用(可能是代理)的ObjectFactory放入三级缓存。
  • 接着Spring为A填充属性,发现它依赖B,于是去创建B。
  • 在创建B的过程中,Spring发现B又依赖A,此时需要获取A。
  • Spring依次检查一级、二级缓存,都找不到A。最后在三级缓存中找到了A的ObjectFactory
  • 通过这个工厂,Spring创建出A的早期引用(如果需要AOP,此时就会生成代理对象),并将其放入二级缓存,然后从三级缓存中移除工厂。这个早期引用被注入到B中,B顺利完成初始化,并被放入一级缓存。
  • 最后,Spring回到A的创建流程,将已经完成的B注入A,A也顺利完成初始化,最终被放入一级缓存。

2. 流程可视化

为了更直观地理解上述过程,尤其是AOP代理的创建时机,下面的流程图展示了完整的交互:


3. 为什么需要提前生成代理?

我们已经清楚了三级缓存的流程,但一个关键问题是:为什么必须在这么早的阶段就创建代理对象?

原因很直接:为了保证依赖注入的一致性,防止AOP失效。

设想一下,如果Spring在解决B对A的依赖时,从缓存里取出了一个原始的、未被代理的A对象并注入给了B,那么B就持有了一个指向原始A对象的引用。即使后续A对象走完了所有流程并被成功代理,B对此也一无所知。当B调用A的方法时,它会直接访问原始对象,从而完美绕过AOP代理,导致事务、日志等切面功能全部失效。

因此,Spring必须在依赖注入发生时,就确保注入的是正确的对象——如果这个Bean未来需要被代理,那么此时注入的就必须是代理对象。三级缓存中的ObjectFactory就承担了这个职责,它在提供早期引用时,会检查并应用AOP,返回一个代理对象。


4. 提前生成代理的潜在问题与解决方案

这种”提前暴露”的机制虽然巧妙,但作为开发者,我们很自然会关心它是否存在风险。

问题 1:提前暴露的”半成品”Bean状态可靠吗?

  • 风险:这个提前生成的代理对象,其内部包裹的目标对象在当时尚未完成属性注入和初始化(如@PostConstruct)。此时若有方法调用,会不会导致空指针或数据不一致?
  • 解决方案
    这得益于代理对象的工作模式。无论是JDK动态代理还是CGLIB,代理对象内部都只维护了一个对目标对象的引用。当外部通过代理调用方法时,代理对象会将调用实时转发给它持有的目标对象。在循环依赖的场景下,虽然代理暴露得很早,但真正的外部方法调用通常发生在所有Bean都初始化完成之后。届时,目标对象的状态已经完整,因此通过代理的调用是安全的。

问题 2:后来的BeanPostProcessor会不会失效?

  • 风险:如果代理对象过早生成,那排在后面的 BeanPostProcessor 对原始 Bean 的修改,还能否体现在代理对象上?
  • 解决方案
    Spring通过BeanPostProcessor的执行顺序来保证。负责AOP的AnnotationAwareAspectJAutoProxyCreator会在一个非常早的时间点(getEarlyBeanReference阶段)介入。一旦它生成了代理对象,这个代理的AOP逻辑就基本固定了。后续其他的BeanPostProcessor仍然可以对原始Bean的属性等进行修改,但无法改变已经织入的代理逻辑。

当然,这也意味着如果你的某个后置处理器需要影响到代理的生成逻辑,你需要确保它的执行顺序在AOP处理器之前。


5. Spring 的权衡:一种务实的设计哲学

Spring在这里的设计,体现了一种非常务实的设计哲学:在保证核心功能的前提下,做出聪明的权衡。

  1. 利用代理的转发机制
    代理对象的核心是”转发”,而非”存储状态”。这为”先创建代理,后完善对象”的异步操作提供了理论基础。只要保证在方法被真正调用时,目标对象是完整的即可。
  2. 明确的边界条件
    这个机制并非万能。Spring官方也指出,应当避免在初始化方法(如@PostConstruct)中,调用本类中需要被AOP拦截的方法。因为在那个时间点,代理可能尚未完全应用到当前Bean的自我引用上,导致调用绕过代理。

6. 总结

现在我们再来梳理一下。当Spring”打破常规”提前暴露代理时,背后是一套严谨的机制在支撑:

  1. 循环依赖靠三级缓存解耦:这套缓存机制确保了Bean之间即使相互依赖,也能顺利完成装配。
  2. AOP有效性靠提前代理:在三级缓存的ObjectFactory中提前生成代理,保证了注入到其他Bean中的引用是正确的代理对象,从而让AOP功能不失效。
  3. 数据一致性靠引用转发:代理对象通过持有对目标对象的引用,确保了任何时候的调用都能访问到目标对象的最新状态,避免了数据不一致的问题。

总而言之,这套方案是Spring在框架的健壮性和功能的完备性之间,做出的一个非常精彩的工程决策,体现了设计的灵活性和实用性。


7. 设计启示:从Spring身上学到的

从Spring处理循环依赖的方式中,我们作为开发者可以得到一些启发:

  1. 问题驱动,务实取舍:面对复杂问题(如循环依赖),不拘泥于单一原则,优先保证核心功能可用,再通过精巧的设计规避潜在风险。
  2. 分层与延迟:通过分层(三级缓存)和延迟计算(ObjectFactory),将复杂问题分解,在真正需要时才执行关键步骤(如创建代理),降低了耦合。
  3. 对用户透明:尽管内部机制复杂,但对于开发者而言,使用@Autowired和AOP注解的体验是无缝的。一个优秀的框架,就应该将复杂性留在内部。

结论

Spring 在解决循环依赖时”违背”延迟生成代理的原则,实则是通过三级缓存和代理对象的设计,在特定场景下做出的合理权衡。这样既支持了循环依赖,又通过技术手段(如目标对象引用委托)避免了状态不一致问题,我感觉体现了Spring框架设计的灵活性。