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的构造器注入为什么会导致循环依赖异常?
这是一个很实际的问题,尤其是在提倡“构造器注入优先”的当下。答案其实很简单,也很直接: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能解决,循环依赖会带来哪些潜在问题和最佳实践?
虽然Spring能够优雅地解决大多数单例Bean的setter/field注入循环依赖,但这并不意味着我们应该忽视它。循环依赖,即使被框架解决了,也常常是代码设计中潜在问题的信号。
潜在问题:
- 理解难度增加: 当系统存在循环依赖时,理解各个模块之间的关系变得更加复杂。你很难清晰地画出依赖图,因为箭头总是绕来绕去,这会给新成员的加入和现有成员的维护带来不小的障碍。
- 测试困难: 单元测试或集成测试时,如果模块之间存在循环依赖,你可能需要同时初始化多个Bean才能进行测试,这增加了测试的复杂性。模拟(Mock)对象时也可能遇到麻烦。
-
运行时行为不可预测: 虽然Spring解决了循环依赖,但如果你在Bean的构造器或
@PostConstruct
方法中执行了依赖于其他Bean的逻辑,而那个Bean此时还处于“半成品”状态,就可能导致意想不到的NullPointerException
或其他运行时错误。因为提前暴露的Bean可能还没有完全初始化。 - 职责不清: 很多时候,循环依赖暗示着两个或多个类承担了过于相似或交叉的职责,导致它们彼此需要对方才能正常工作。这违反了“单一职责原则”。
- 难以重构: 紧密的循环依赖使得代码库像一个缠绕的线团,任何一个小的改动都可能牵一发而动全身,导致重构变得异常困难和风险高。
最佳实践:
- 优先使用构造器注入,并以此为契机发现循环依赖: 尽管构造器注入无法解决循环依赖,但这恰恰是它的优点。当它抛出循环依赖异常时,它是在“警告”你,你的设计可能存在问题。这提供了一个重构的机会。
- 重新审视模块职责: 如果发现循环依赖,尝试重新思考这些Bean的职责。能否将它们拆分成更小的、职责单一的组件?或者引入一个新的协调者(Facade)来管理它们?
- 引入接口抽象: 有时,循环依赖是因为两个具体实现类互相引用。引入接口可以帮助解耦,让一个类依赖于另一个类的接口,而不是具体实现。
- 事件驱动或消息队列: 对于某些业务场景,如果两个服务需要互相通知,可以考虑引入事件发布/订阅机制或消息队列。这样,服务A完成任务后发布一个事件,服务B订阅这个事件并作出响应,避免了直接的同步调用依赖。
-
延迟初始化或
ObjectProvider
/Provider
: 在极少数情况下,如果循环依赖确实无法避免,并且是setter注入,可以考虑使用@Lazy
注解来延迟Bean的初始化,或者注入ObjectProvider
(Spring)或Provider
(JSR-330)来按需获取Bean实例,而不是在启动时就完全注入。但这通常是权宜之计,而不是首选方案。 -
避免在
@PostConstruct
中访问循环依赖的Bean: 确保在@PostConstruct
方法中访问的任何Bean都已完全初始化。如果存在循环依赖,










