Spring 解决单例 Bean 循环依赖的核心机制是 “三级缓存 + 提前暴露未完成初始化的 Bean”,通过在 Bean 初始化过程中 “提前曝光” 半成品对象,打破 “互相等待对方完成” 的死循环。
先明确前提
Spring 只能解决 “单例 Bean 的属性注入循环依赖”(如 @Autowired
字段注入),无法解决 原型 Bean 或 构造器注入 的循环依赖(这两种情况会直接抛出异常)。
核心:三级缓存的作用
Spring 维护了三个缓存(本质是 Map),用于存储不同状态的 Bean:
缓存名称 | 作用(存储内容) |
---|---|
singletonObjects | 一级缓存:存储 完全初始化完成 的单例 Bean(可以直接使用的成品)。 |
earlySingletonObjects | 二级缓存:存储 提前暴露的半成品 Bean(已实例化,但未完成属性注入和初始化方法)。 |
singletonFactories | 三级缓存:存储 Bean 工厂对象(用于生成半成品 Bean 的工厂,含 AOP 代理逻辑)。 |
解决循环依赖的完整流程(举例:A 依赖 B,B 依赖 A)
假设我们有两个单例 Bean:
@Service
public class A {
@Autowired
private B b; // A 依赖 B
}
@Service
public class B {
@Autowired
private A a; // B 依赖 A
}
Spring 创建这两个 Bean 的过程如下,完美打破循环:
1. 开始创建 Bean A
- 步骤 1:实例化 A
Spring 先通过构造器创建 A 的对象(只完成 “实例化”,还没注入属性b
,也没执行初始化方法)。此时 A 是 “半成品”。 - 步骤 2:暴露 A 的工厂到三级缓存
实例化后,Spring 会创建一个 “Bean 工厂”(ObjectFactory
),并放入三级缓存singletonFactories
。这个工厂的作用是:当其他 Bean 需要依赖 A 时,能通过工厂获取 A 的半成品(如果 A 需要被代理,工厂会生成代理对象)。// 伪代码:放入三级缓存 singletonFactories.put("a", () -> createEarlyBeanReference(a));
- 步骤 3:A 需要注入 B
接下来 Spring 要给 A 注入属性b
,但此时一级缓存中没有 B(B 还没创建),所以 Spring 暂停 A 的初始化,转而开始创建 B。
2. 开始创建 Bean B
- 步骤 1:实例化 B
同样,先通过构造器创建 B 的对象(半成品,未注入属性a
)。 - 步骤 2:暴露 B 的工厂到三级缓存
把 B 的工厂放入singletonFactories
,方便其他 Bean 依赖 B 时获取。 - 步骤 3:B 需要注入 A
现在 B 要注入属性a
,Spring 会依次从缓存中找 A:- 一级缓存
singletonObjects
:没有(A 还没完成初始化)。 - 二级缓存
earlySingletonObjects
:没有(A 还没被提前暴露到这里)。 - 三级缓存
singletonFactories
:找到了 A 的工厂!
此时,Spring 会通过 A 的工厂获取 A 的半成品(如果 A 需要 AOP 代理,这里会生成代理对象),并把 A 从三级缓存移到二级缓存earlySingletonObjects
(标记为 “已提前暴露”)。
- 一级缓存
- 步骤 4:B 完成初始化
B 成功注入 A 的半成品后,继续执行其他初始化操作(如@PostConstruct
方法),最终成为 “成品”,放入一级缓存singletonObjects
,并从二、三级缓存中移除。
3. 回到 Bean A 的初始化
- B 已经是成品(在一级缓存中),所以 A 可以顺利注入 B。
- A 继续执行剩余初始化操作,最终成为成品,放入一级缓存
singletonObjects
,并从二、三级缓存中移除。
为什么需要三级缓存?
核心是为了处理 AOP 代理场景:
如果 A 被 @Transactional
等注解标记,需要生成代理对象(而非原始对象),那么 B 依赖的应该是 A 的代理对象,而不是原始对象。
三级缓存中的 “Bean 工厂” 会在获取 A 时,判断是否需要生成代理对象,确保依赖注入的是正确的代理对象。如果只有二级缓存,无法动态生成代理(只能存固定对象),会导致依赖注入错误。
无法解决的情况
- 原型 Bean 循环依赖:原型 Bean 每次获取都会重新创建,不会放入缓存,所以无法提前暴露,直接抛出
BeanCurrentlyInCreationException
。 - 构造器注入循环依赖:构造器注入在 “实例化阶段” 就需要依赖(如
A(B b)
和B(A a)
),而此时 Bean 还未实例化,无法提前暴露,会抛出异常(可通过@Lazy
注解延迟加载依赖解决)。
总结
Spring 解决单例 Bean 循环依赖的核心是:
在 Bean 实例化后、初始化前,通过三级缓存提前暴露半成品对象,让依赖方可以先获取到这个半成品,从而打破循环等待。三级缓存的设计还兼顾了 AOP 代理的动态生成,确保依赖注入的是正确的对象(原始对象或代理对象)。