循环依赖与三级缓存
候选人小钱在面试字节P6时,面试官问道:
"Spring 是怎么解决循环依赖的?"
小钱说:"用三级缓存,singletonObjects、earlySingletonObjects、singletonFactories..."
面试官追问:"三级缓存分别存什么?为什么要三级,两级不够吗?"
小钱说:"singletonObjects 存完整对象,early 存早期对象..."
面试官继续问:"那 earlySingletonObjects 里的对象是什么时候放进去的?Bean 还没完成属性注入怎么就放进去了?"
小钱支支吾吾答不上来。
面试官:"setter 注入能解决循环依赖,那构造器注入呢?为什么不能解决?"
小钱彻底卡住。
【面试官心理】
这道题我用来筛选那些真正看过 Spring 源码的人。三级缓存是 Spring 最复杂也是最精妙的机制之一。知道名字的占 80%,能说出三级缓存作用的占 40%,能讲清为什么构造器注入无法解决的只有 15%。循环依赖是 Spring 源码中最容易露馅的地方。
一、什么是循环依赖 🔴
1.1 三种循环依赖
// 1. 字段注入循环依赖
@Service
public class A {
@Autowired private B b;
}
@Service
public class B {
@Autowired private A a;
}
// 2. setter 注入循环依赖
@Service
public class A {
private B b;
@Autowired
public void setB(B b) { this.b = b; }
}
@Service
public class B {
private A a;
@Autowired
public void setA(A a) { this.a = a; }
}
// 3. 构造器循环依赖(Spring 无法解决)
@Service
public class A {
private B b;
public A(B b) { this.b = b; }
}
@Service
public class B {
private A a;
public B(A a) { this.a = a; }
}
1.2 循环依赖的可解决范围
graph TD
A[Spring 循环依赖解决能力] --> B[setter 注入<br/>✅ 可解决]
A --> C[@Autowired/@Value 字段注入<br/>✅ 可解决]
A --> D[构造器注入<br/>❌ 无法解决]
A --> E[prototype Bean<br/>❌ 无法解决]
A --> F[depends-on 强制依赖<br/>❌ 无法解决]
二、三级缓存详解 🔴
2.1 三级缓存的数据结构
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry {
// 一级缓存:完全初始化好的 Bean(可以直接使用)
private final Map<String, Object> singletonObjects =
new ConcurrentHashMap<>(256);
// 二级缓存:提前曝光的 Bean(实例已创建,但属性未注入)
private final Map<String, Object> earlySingletonObjects =
new ConcurrentHashMap<>(256);
// 三级缓存:Bean 的早期工厂(用于创建早期引用的回调)
private final Map<String, ObjectFactory<?>> singletonFactories =
new ConcurrentHashMap<>(256);
// 正在创建中的 Bean 名称集合
private final Set<String> singletonsCurrentlyInCreation =
Collections.newSetFromMap(new ConcurrentHashMap<>(16));
}
2.2 getSingleton 核心逻辑
public Object getSingleton(String beanName) {
return getSingleton(beanName, true);
}
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1. 先查一级缓存:已完全初始化的单例
Object singletonObject = singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 2. 一级缓存没有,正在创建中,查二级缓存
synchronized (this.singletonObjects) {
singletonObject = earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 3. 二级缓存没有,查三级缓存并升级
ObjectFactory<?> singletonFactory = singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject(); // 调用回调
// 升级到二级缓存
earlySingletonObjects.put(beanName, singletonObject);
// 删除三级缓存
singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
2.3 为什么要三级?逐层拆解
假设只有一级缓存:
// ❌ 一级缓存的问题
Map<String, Object> singletonObjects = new HashMap<>();
// 线程 A 创建 A 属性注入时需要 B,去创建 B
// 线程 A 创建 B 属性注入时需要 A,此时去一级缓存找 A
// 问题:一级缓存里没有 A(A 还在创建中,没有完成)
// 结果:死循环 or NPE
假设只有两级缓存(一级 + 二级):
// ❌ 两级缓存的问题
Map<String, Object> singletonObjects = new HashMap<>();
Map<String, Object> earlySingletonObjects = new HashMap<>();
// A 属性注入时需要 B,去创建 B
// B 属性注入时需要 A,去一级缓存找 A → 没有(A 还在创建)
// 去二级缓存找 A → 没有(A 还没有创建!)
// 问题:二级缓存里也没有,因为 A 还没开始创建
三级缓存的解决方案:
// ✅ 三级缓存的关键:提前暴露工厂
// 1. A 开始创建,调用 addSingletonFactory
protected void addSingletonFactory(String beanName,
ObjectFactory<?> singletonFactory) {
if (!this.singletonObjects.containsKey(beanName)) {
// 把工厂放入三级缓存
this.singletonFactories.put(beanName, singletonFactory);
// 注意:此时一级和二级缓存都没有 A
}
}
// 2. A 实例化完成后,立即暴露工厂(此时 B 可以通过工厂获取 A 的早期引用)
// 在 doCreateBean 中:
boolean earlySingletonExposure = mbd.isSingleton()
&& this.allowCircularReferences
&& isSingletonCurrentlyInCreation(beanName);
if (earlySingletonExposure) {
addSingletonFactory(beanName,
() -> getEarlyBeanReference(beanName, mbd, bean)); // ← 三级缓存存的就是这个
}
// 3. B 在属性注入时发现需要 A,去调用 getSingleton("A")
// getSingleton 查一级 → 没有(因为 A 还没创建完)
// 查二级 → 没有
// 查三级 → 有!调用 singletonFactory.getObject() → getEarlyBeanReference("A")
// getEarlyBeanReference 返回 A 的早期引用(可能已经被代理)
// B 拿到 A 的早期引用,继续创建 → B 创建完成
// 回到 A,继续属性注入 → A 创建完成
2.4 为什么需要二级缓存?
二级缓存是为了避免重复调用 singletonFactory.getObject():
// 如果只有三级缓存:
// B 获取 A 的早期引用 → 调用 factory.getObject() → 创建代理
// C 获取 A 的早期引用 → 又调用 factory.getObject() → 又创建代理
// 同一个 Bean 被创建了两次代理!
// 有二级缓存后:
// B 获取 A 的早期引用 → 调用 factory.getObject() → 创建代理 → 存入二级缓存
// C 获取 A 的早期引用 → 查二级缓存 → 直接取已有的代理 → 不会重复创建
💡
getEarlyBeanReference 不仅是返回早期引用,它还负责创建代理。如果 Bean 被 AOP 切面覆盖,这里的工厂会返回一个代理对象而不是原始对象。这就是为什么循环依赖中的 Bean 能拿到代理对象的原因。
三、循环依赖解决流程图 🔴
sequenceDiagram
participant A as BeanA
participant Factories as 三级缓存
participant Early as 二级缓存
participant Complete as 一级缓存
participant B as BeanB
Note over A,B: 构造器注入:无法解决
Note over A,B: setter/字段注入:可以解决
rect rgb(200, 220, 240)
Note over A,B: setter 循环依赖解决过程
A->>+Factories: addSingletonFactory("A", factory)
Note right of A: A 实例化完成,暴露工厂
A->>+Factories: getSingleton("B")
Note right of A: A 需要注入 B,去获取 B
B->>+Factories: addSingletonFactory("B", factory)
Note right of B: B 实例化完成,暴露工厂
B->>+Factories: getSingleton("A")
Note right of B: B 需要注入 A,去获取 A
Factories-->>+B: factory.getObject() → A 早期引用
B->>+Early: earlySingletonObjects.put("A", A_ref)
Factories-->>+Early: singletonFactories.remove("A")
B->>+Complete: 一级缓存放入 B,B 创建完成
B-->>+A: B 实例注入 A
A->>+Complete: 一级缓存放入 A,A 创建完成
Complete->>Complete: A 和 B 都在一级缓存
end
四、构造器注入为什么无法解决 🔴
这是面试中最容易被追问的深水区:
// 构造器注入的循环依赖
@Service
public class A {
private B b;
public A(B b) { // 构造器需要 B
this.b = b;
}
}
@Service
public class B {
private A a;
public B(A a) { // 构造器需要 A
this.a = a;
}
}
原因:三级缓存解决循环依赖的前提是实例化可以先于属性注入完成。
graph TD
subgraph "setter 注入(可解决)"
A1[new A] --> A2[A 实例化完成]
A2 --> A3[暴露早期引用到三级缓存]
A3 --> A4[注入 B(B 需要 A)]
A4 --> A5[从三级缓存获取 A 早期引用]
A5 --> A6[B 创建完成,注入给 A]
A6 --> A7[A 创建完成]
end
subgraph "构造器注入(无法解决)"
B1[new A 需要 B] --> B2[创建 B 需要 A]
B2 --> B3[创建 A 需要 B]
B3 --> B4[创建 B 需要 A]
B4 --> B5[...无限递归]
end
关键点:在 addSingletonFactory 调用之前,Bean 必须已经实例化完成。构造器注入要求依赖在构造时就确定,而此时还没有暴露工厂,所以无法解决。
// doCreateBean 中暴露工厂的时机:
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
// ↑ 实例化完成后
boolean earlySingletonExposure = mbd.isSingleton()
&& this.allowCircularReferences
&& isSingletonCurrentlyInCreation(beanName);
if (earlySingletonExposure) {
addSingletonFactory(beanName, ...); // ← 这里才暴露工厂
}
// 但构造器注入的"实例化"发生在 new A(B b) 那一刻
// 此时 A 还没有进入 doCreateBean,还没有暴露工厂
// 所以 B 无法从三级缓存获取 A 的早期引用
五、❌ 错误示范
翻车点一:把二级缓存当成多余的
候选人原话:"二级缓存可以不要,有三级就够了。"
实际上二级缓存用于缓存代理对象,避免同一个 Bean 被重复代理。
翻车点二:认为所有循环依赖都能解决
候选人原话:"Spring 通过三级缓存可以解决所有循环依赖。"
错了!构造器注入、prototype Bean、depends-on 强制依赖都无法解决。
翻车点三:不知道早期引用可能是代理对象
候选人原话:"earlySingletonObjects 存的是原始对象。"
实际上通过 getEarlyBeanReference 返回的可能是代理对象(AOP 的工作)。
六、标准回答
P5 级别
Spring 通过三级缓存解决 setter 注入和字段注入的循环依赖。三级缓存分别是:singletonObjects(一级,完全初始化)、earlySingletonObjects(二级,提前曝光)、singletonFactories(三级,早期工厂)。构造器注入无法解决循环依赖,因为构造时就需要依赖对象,此时工厂还没暴露。
P6 级别
流程是:Bean 实例化完成后立即调用 addSingletonFactory 将工厂放入三级缓存。属性注入时遇到循环依赖,通过 getSingleton 依次查询三级缓存,最终从三级缓存获取早期引用。二级缓存用于避免重复调用工厂创建代理对象。关键在于 getEarlyBeanReference 方法——它不仅返回早期引用,还负责创建 AOP 代理,所以循环依赖中的 Bean 拿到的是代理对象。构造器注入无法解决是因为构造函数执行时 Bean 还未进入 doCreateBean,工厂尚未暴露。
P7 级别
三级缓存的设计本质上是"延迟代理创建"和"循环依赖解决"的权衡。一级缓存存完全体,二级缓存存过渡态,三级缓存存工厂。为什么要这样?因为 AOP 代理的创建时机很关键——如果 Bean 被切面覆盖,循环依赖中其他 Bean 需要拿到代理而不是原始对象。singletonFactory.getObject() 调用 getEarlyBeanReference,它会检查 Bean 是否需要被代理,需要则创建代理并返回。这解释了为什么 Spring AOP 在循环依赖场景下依然能正常工作。构造器注入无法解决的根因是:它要求依赖在构造时确定,此时连实例化都没完成,根本没有机会暴露工厂给其他 Bean 使用。
七、追问升级 🟡
追问1:prototype Bean 为什么不能解决循环依赖?
// prototype Bean 每次 getBean 都创建新实例,不走三级缓存
@Override
public Object getBean(String name, Object... args) {
for (String beanName : beanDefinitionNames) {
if (mbd.isPrototype()) {
// prototype 不注册到 singletonFactories
// 每次都是新创建,无法通过缓存解决
return createBean(beanName, mbd, args);
}
}
}
追问2:allowCircularReferences 是什么?
// 默认 allowCircularReferences = true
// 如果关闭,循环依赖会直接抛异常
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
if (!allowEarlyReference) {
throw new BeanCurrentlyInCreationException(beanName);
}
// ...
}
追问3:二级缓存升级后,三级缓存要不要清?
必须清!
// DefaultSingletonBeanRegistry.getSingleton
ObjectFactory<?> singletonFactory = singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
earlySingletonObjects.put(beanName, singletonObject); // 升级到二级
singletonFactories.remove(beanName); // 必须删除三级
// 否则下次还会调用工厂,创建重复代理
}
【面试官心理】
这道题我追问的深度取决于候选人的表现。如果前两问答得不错,我会追问"为什么需要二级缓存"和"getEarlyBeanReference 为什么要检查切面"。这两问能答好的,基本都是看过源码的候选人。