Spring通过三级缓存机制解决单例Bean的循环依赖问题,核心在于提前暴露“半成品”对象。当Bean A依赖Bean B,而Bean B又依赖A时,Spring在A实例化后将其ObjectFactory放入三级缓存(singletonFactories),B在创建过程中通过该工厂获取A的原始或代理实例,完成自身初始化并放入一级缓存(singletonObjects),随后A再注入已初始化的B,最终双方都完成创建。此机制依赖Bean生命周期的分阶段处理:实例化→放入三级缓存→属性填充→初始化→升级至一级缓存。二级缓存(earlySingletonObjects)用于存放从三级缓存中取出的早期暴露对象,避免重复创建代理。该方案仅适用于单例作用域下的setter或字段注入,无法解决构造器注入的循环依赖,因为构造器要求所有依赖在实例化时即就绪,无法提前暴露半成品对象。构造器注入导致的循环依赖会直接抛出BeanCurrentlyInCreationException,这实则是设计警示,提示模块耦合过紧。尽管Spring能处理setter循环依赖,但应视为代码“异味”:它增加理解与测试难度,可能导致@PostConstruct中访问未初始化Bean引发运行时异常,且不利于重构。最佳实践包括:优先使用构造器注入以暴露设计问题;拆分职责、引入接口或门面模式解耦;采用事件驱动
Spring解决循环依赖的核心在于其三级缓存机制,结合提前暴露的单例对象,打破了对象创建的僵局。简单来说,当一个Bean A依赖Bean B,同时Bean B又依赖Bean A时,Spring会在Bean A实例化但未完全初始化(属性填充)时,将其“半成品”状态提前放入一个缓存中,供Bean B引用,从而允许Bean B完成初始化,最终Bean A也能顺利完成初始化。
要深入理解Spring如何优雅地处理循环依赖,我们得把目光投向它内部的Bean生命周期管理和那套精妙的“三级缓存”机制。这东西听起来有点玄乎,但实际上它解决的是一个经典的两难问题:当两个或多个Bean互相引用时,到底谁先完全准备好?
想象一下,我们有两个单例Bean:
ServiceA依赖
ServiceB,而
ServiceB又依赖
ServiceA。
ServiceA: Spring容器开始创建
ServiceA。它首先调用构造器实例化
ServiceA,此时
ServiceA只是一个“裸”对象,其依赖的
ServiceB还没有被注入。
ServiceA(一级缓存 -> 三级缓存): 实例化后的
ServiceA会被立即放入一个三级缓存(
singletonFactories,存储的是一个
ObjectFactory,可以生产未完全初始化的Bean)。这个
ObjectFactory至关重要,它封装了获取
ServiceA原始对象,并可能进行AOP代理的逻辑。
ServiceA属性,发现
ServiceB依赖: Spring尝试为
ServiceA注入属性,发现它依赖
ServiceB。
ServiceB: 容器转而去创建
ServiceB。
ServiceB:
ServiceB被实例化。
ServiceB: 实例化后的
ServiceB同样被放入三级缓存。
ServiceB属性,发现
ServiceA依赖: Spring尝试为
ServiceB注入属性,发现它依赖
ServiceA。
ServiceA: 此时,容器不会重新创建
ServiceA,而是检查它的一级、二级、三级缓存。它会在三级缓存中找到那个可以生成
ServiceA的
ObjectFactory。通过这个工厂,它获取到
ServiceA的“半成品”实例(原始的、未完全初始化的
ServiceA对象,如果需要AOP代理,此时也会进行代理)。
ServiceB完成初始化:
ServiceB成功获取到
ServiceA的引用,并完成自己的属性注入和初始化。此时,
ServiceB是一个完全可用的对象,并被放入一级缓存(
singletonObjects)。
ServiceA完成初始化:
ServiceB完成后,Spring回到
ServiceA的初始化流程。
ServiceA现在可以顺利获取到已经完全初始化的
ServiceB对象,完成自己的属性注入和初始化。最终,
ServiceA也被放入一级缓存。
这三级缓存具体是什么?
singletonObjects): 存放已经完全初始化并可用的单例Bean。
earlySingletonObjects): 存放已经实例化但尚未完全初始化(属性填充、AOP代理等)的单例Bean。当一个Bean被其他Bean提前引用时,它会从三级
缓存提升到二级缓存。singletonFactories): 存放一个
ObjectFactory,这个工厂可以生产出原始的、未完全初始化的Bean实例。它的存在是为了处理AOP代理。如果Bean需要被代理,那么在其他Bean引用它时,应该引用的是代理对象而不是原始对象。这个
ObjectFactory就负责在需要时生成代理对象。
这种机制巧妙地利用了Bean的生命周期阶段,在Bean实例化后、属性注入前,就将其“半成品”暴露出来,从而打破了循环引用的死锁。但需要注意的是,这种机制主要针对单例Bean的setter注入和字段注入有效。对于构造器注入的循环依赖,Spring是无法解决的,因为它无法在构造器执行完成前就暴露一个“半成品”对象。
这是一个很实际的问题,尤其是在提倡“构造器注入优先”的当下。答案其实很简单,也很直接:Spring的循环依赖解决机制,也就是我们前面提到的三级缓存,其核心在于Bean的“提前暴露”。这个“提前暴露”发生在一个Bean被实例化之后,但在其所有依赖被注入之前。
当使用构造器注入时,一个Bean的实例化过程本身就需要它的所有依赖都准备就绪。换句话说,
ServiceA的构造器需要
ServiceB的实例才能完成,而
ServiceB的构造器又需要
ServiceA的实例。这就形成了一个鸡生蛋、蛋生鸡的死循环,没有任何一个Bean可以在另一个Bean被完全实例化之前,提供一个“半成品”供对方引用。
举个例子:
@Service
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(ServiceB serviceB) { // 构造器需要ServiceB
this.serviceB = serviceB;
}
}
@Service
public class ServiceB {
private final ServiceA serviceA;
public ServiceB(ServiceA serviceA) { // 构造器需要ServiceA
this.serviceA = serviceA;
}
}Spring在尝试创建
ServiceA时,会发现需要
ServiceB。它会暂停
ServiceA的创建去创建
ServiceB。创建
ServiceB时,又发现需要
ServiceA。此时,
ServiceA甚至还没有完成实例化,更别提被放入任何缓存了。它卡在了构造器这一步,根本没有机会走到“提前暴露”的阶段。所以,Spring会直接抛出
BeanCurrentlyInCreationException或类似的异常,明确告诉你存在循环依赖。
这并不是Spring的缺陷,而是构造器注入本身的特性所决定的。它强制要求所有依赖在对象构造时就已存在,这使得它在处理循环依赖时变得无能为力。因此,在设计系统时,如果发现构造器注入导致循环依赖,这通常是一个代码设计上的“异味”,暗示着模块之间的职责划分可能不够清晰,或者耦合过于紧密,需要重新审视。
虽然Spring能够优雅地解决大多数单例Bean的setter/field注入循环依赖,但这并不意味着我们应该忽视它。循环依赖,即使被框架解决了,也常常是代码设计中潜在问题的信号。
潜在问题:
@PostConstruct方法中执行了依赖于其他Bean的逻辑,而那个Bean此时还处于“半成品”状态,就可能导致意想不到的
NullPointerException或其他运行时错误。因为提前暴露的Bean可能还没有完全初始化。
最佳实践:
ObjectProvider/
Provider: 在极少数情况下,如果循环依赖确实无法避免,并且是setter注入,可以考虑使用
@Lazy注解来延迟Bean的初始化,或者注入
ObjectProvider(Spring)或
Provider(JSR-330)来按需获取Bean实例,而不是在启动时就完全注入。但这通常是权宜之计,而不是首选方案。
@PostConstruct中访问循环依赖的Bean: 确保在
@PostConstruct方法中访问的任何Bean都已完全初始化。如果存在循环依赖,