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

Spring AOP 与循环依赖:揭秘提前暴露代理的底层原理
XR引言
在Spring开发中,AOP和循环依赖是两个我们经常打交道的话题。通常的认知是,Spring会在一个Bean完全初始化(属性填充、init方法执行完毕)之后,才为其创建AOP代理。但当一个需要被代理的Bean,恰好又陷入了循环依赖,情况就变得棘手起来:Spring必须在这个Bean尚未”完工”时,就将它暴露给依赖方。
这就带来一个很自然的问题:一个尚未完全初始化的Bean,如何能以它最终的代理形态被提前暴露?这样做不会有状态不一致的风险吗?本文将以开发者的视角,深入Spring内部,看看它是如何通过精妙的三级缓存设计,解决这个看似矛盾的问题的。
1. 循环依赖的解决机制:三级缓存
要理解循环依赖的解决方案,核心就是要弄懂Spring的三级缓存。这三级缓存,本质上是Spring在Bean生命周期中,为了管理不同状态的Bean实例而设的三层存储空间:
- 一级缓存(singletonObjects):一个Map,存放的是完全初始化好的单例Bean。可以把它看作是”成品仓”,里面的Bean随时可以取用。
- 二级缓存(earlySingletonObjects):也是一个Map,存放的是提前暴露的单例Bean的早期引用。这些Bean已经实例化,但可能还没完成属性注入和初始化。它们是”半成品”,用于解开循环依赖。
- 三级缓存(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代理的创建时机,下面的流程图展示了完整的交互:
sequenceDiagram
participant Client as 客户端
participant Spring as Spring容器
participant L3Cache as "三级缓存 (singletonFactories)"
participant L2Cache as "二级缓存 (earlySingletonObjects)"
participant L1Cache as "一级缓存 (singletonObjects)"
participant InstanceA as "Bean A 实例"
participant InstanceB as "Bean B 实例"
Client->>+Spring: getBean("a")
Spring->>Spring: 1. 创建Bean A
Spring->>InstanceA: new ServiceA()
Spring->>L3Cache: 2. 放入A的ObjectFactory
Spring->>Spring: 3. 填充A的属性 (发现依赖B)
Spring->>+Spring: 4. getBean("b")
Spring->>InstanceB: new ServiceB()
Spring->>L3Cache: 5. 放入B的ObjectFactory
Spring->>Spring: 6. 填充B的属性 (发现依赖A)
Spring->>Spring: 7. 尝试获取Bean A (为B注入)
Spring->>L1Cache: 检查"a" (未命中)
Spring->>L2Cache: 检查"a" (未命中)
Spring->>L3Cache: 检查"a" (命中, 获取ObjectFactory)
Spring->>Spring: 8. 调用A的ObjectFactory.getObject()
Note right of Spring: 此刻通过getEarlyBeanReference<br>为A创建代理对象
Spring->>L2Cache: 9. 将A的早期引用(代理)放入二级缓存
Spring->>L3Cache: 从三级缓存移除A的Factory
Spring-->>InstanceB: 10. 将A的早期引用注入B
Spring->>Spring: 11. 完成B的初始化
Spring->>L1Cache: 12. 放入B的完整实例
Spring-->>Spring: 返回B的实例
Spring->>InstanceA: 13. 将B实例注入A
Spring->>Spring: 14. 完成A的初始化
Spring->>L1Cache: 15. 放入A的完整实例(代理)
Spring-->>Client: 返回A的实例(代理)
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在这里的设计,体现了一种非常务实的设计哲学:在保证核心功能的前提下,做出聪明的权衡。
- 利用代理的转发机制:
代理对象的核心是”转发”,而非”存储状态”。这为”先创建代理,后完善对象”的异步操作提供了理论基础。只要保证在方法被真正调用时,目标对象是完整的即可。 - 明确的边界条件:
这个机制并非万能。Spring官方也指出,应当避免在初始化方法(如@PostConstruct)中,调用本类中需要被AOP拦截的方法。因为在那个时间点,代理可能尚未完全应用到当前Bean的自我引用上,导致调用绕过代理。
6. 总结
现在我们再来梳理一下。当Spring”打破常规”提前暴露代理时,背后是一套严谨的机制在支撑:
- 循环依赖靠三级缓存解耦:这套缓存机制确保了Bean之间即使相互依赖,也能顺利完成装配。
- AOP有效性靠提前代理:在三级缓存的
ObjectFactory中提前生成代理,保证了注入到其他Bean中的引用是正确的代理对象,从而让AOP功能不失效。 - 数据一致性靠引用转发:代理对象通过持有对目标对象的引用,确保了任何时候的调用都能访问到目标对象的最新状态,避免了数据不一致的问题。
总而言之,这套方案是Spring在框架的健壮性和功能的完备性之间,做出的一个非常精彩的工程决策,体现了设计的灵活性和实用性。
7. 设计启示:从Spring身上学到的
从Spring处理循环依赖的方式中,我们作为开发者可以得到一些启发:
- 问题驱动,务实取舍:面对复杂问题(如循环依赖),不拘泥于单一原则,优先保证核心功能可用,再通过精巧的设计规避潜在风险。
- 分层与延迟:通过分层(三级缓存)和延迟计算(
ObjectFactory),将复杂问题分解,在真正需要时才执行关键步骤(如创建代理),降低了耦合。 - 对用户透明:尽管内部机制复杂,但对于开发者而言,使用
@Autowired和AOP注解的体验是无缝的。一个优秀的框架,就应该将复杂性留在内部。
结论
Spring 在解决循环依赖时”违背”延迟生成代理的原则,实则是通过三级缓存和代理对象的设计,在特定场景下做出的合理权衡。这样既支持了循环依赖,又通过技术手段(如目标对象引用委托)避免了状态不一致问题,我感觉体现了Spring框架设计的灵活性。










