Spring 框架通过三级缓存机制优雅地解决了单例 bean 的循环依赖问题。下面我将详细解释这个过程。
一、循环依赖的本质问题
当两个或多个 bean 相互依赖时,可能形成循环依赖:
java
class A {
@Autowired private B b;
}
class B {
@Autowired private A a;
}
若按常规创建流程:
- 创建 A 时发现需要 B,暂停 A 的创建去创建 B;
- 创建 B 时发现需要 A,但 A 尚未完成初始化,导致死锁。
二、Spring 的三级缓存机制
Spring 通过三个 Map 实现缓存(位于DefaultSingletonBeanRegistry
类):
java
// 一级缓存:存储完全初始化好的单例bean(已创建、已注入、已初始化)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:存储提前暴露的单例bean(已创建,但未完成属性注入和初始化)
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// 三级缓存:存储ObjectFactory(用于生成早期暴露对象的工厂)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
三、三级缓存如何解决循环依赖?
核心流程(以 A→B→A 循环为例):
- 创建 A:
-
- A 开始创建,首先将 A 的
ObjectFactory
放入三级缓存(此时 A 刚实例化,未注入属性)。
- A 开始创建,首先将 A 的
- java
- A 需要 B:
-
- A 在属性注入阶段发现依赖 B,暂停 A 的创建,开始创建 B。
- 创建 B:
-
- B 实例化后,将 B 的
ObjectFactory
放入三级缓存。 - B 在属性注入阶段发现依赖 A,从缓存中获取 A。
- B 实例化后,将 B 的
- B 获取 A:
-
- B 首先从一级缓存查 A,未找到;
- 再从二级缓存查 A,未找到;
- 最后从三级缓存获取 A 的
ObjectFactory
,通过工厂获取 A 的早期引用(未完成初始化的 A)。 - 将 A 的早期引用从三级缓存移至二级缓存。
- B 完成创建:
-
- B 成功注入 A 的早期引用,完成自身初始化,放入一级缓存。
- A 继续创建:
-
- A 获取已完成创建的 B,完成自身初始化,放入一级缓存。
关键代码(从缓存获取 bean):
java
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1. 从一级缓存获取
Object singletonObject = this.singletonObjects.get(beanName);
// 2. 若bean正在创建
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 3. 从二级缓存获取
singletonObject = this.earlySingletonObjects.get(beanName);
// 4. 若二级缓存没有且允许早期引用
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// 双重检查
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
// 5. 从三级缓存获取ObjectFactory
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 6. 通过工厂获取早期引用
singletonObject = singletonFactory.getObject();
// 7. 将早期引用放入二级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
// 8. 从三级缓存移除
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
四、为什么需要三级缓存?二级不够吗?
必须使用三级缓存的核心原因:AOP 代理。
若 A 是代理对象(如使用 @Transactional):
- 二级缓存直接存储 A 的原始实例,B 注入的将是 A 的原始对象,而非代理对象。
- 三级缓存通过
ObjectFactory
在获取早期引用时动态生成代理对象(若有 AOP),保证 B 注入的是 A 的代理对象。
示例代码(添加 AOP 代理逻辑):
java
// 创建bean实例后,添加ObjectFactory到三级缓存
addSingletonFactory(beanName, () -> {
// 若需要代理,在此生成代理对象
return getEarlyBeanReference(beanName, mbd, bean);
});
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
// SmartInstantiationAwareBeanPostProcessor会在此生成代理对象
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp =
(SmartInstantiationAwareBeanPostProcessor) bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}
五、循环依赖的限制与注意事项
- 仅支持单例模式:原型(prototype)作用域的 bean 不支持循环依赖,因为每次请求都会创建新实例,无法缓存。
- 构造器注入无法解决:若依赖通过构造器注入,实例化时就需要依赖对象,此时缓存尚未建立。
- 依赖必须是提前暴露的:若 bean 的生命周期中存在某些步骤阻止其提前暴露(如 @PostConstruct 中抛出异常),循环依赖可能失败。
六、总结
Spring 的三级缓存机制通过提前暴露未完成初始化的 bean,结合动态生成代理对象的能力,巧妙解决了单例 bean 的循环依赖问题。核心流程可概括为:
- 三级缓存分工:
-
- 一级缓存:存储完整的 bean。
- 二级缓存:存储早期暴露的原始 bean 或代理 bean。
- 三级缓存:存储生成早期 bean 的工厂,支持 AOP 动态代理。
- 执行流程:
-
- 创建 bean 时先将工厂放入三级缓存。
- 发生循环依赖时,通过工厂从三级缓存获取早期引用,放入二级缓存。
- bean 完全初始化后,从二级缓存移除,放入一级缓存。
理解三级缓存机制,对掌握 Spring 的 bean 生命周期和依赖注入原理至关重要。‘
是的,你理解得非常准确!Spring 的三级缓存机制核心就是解决循环依赖场景下,如何让 A 和 B 获取到对方的 “早期引用”(未完全初始化的 bean),从而打破死锁。
关键逻辑再梳理
- 传统方式的问题:
-
- 若 A 依赖 B,B 依赖 A,按常规流程:
-
-
- A 创建到一半等待 B → B 创建到一半等待 A → 死循环。
-
- 三级缓存的解决方案:
-
- 提前暴露机制:当 A 刚实例化(未注入属性)时,就将其 “早期引用” 通过
ObjectFactory
放入三级缓存。 - 动态代理支持:若 A 需要 AOP 代理,三级缓存的
ObjectFactory
会在被调用时动态生成代理对象(而非原始实例)。 - 依赖注入顺序:
- 提前暴露机制:当 A 刚实例化(未注入属性)时,就将其 “早期引用” 通过
-
-
- B 在创建时发现依赖 A,直接从三级缓存获取 A 的早期引用(可能是代理对象)。
- B 完成创建后,A 再注入已完成的 B,最终 A 也完成创建。
-
防止 “依赖已初始化完毕的 bean” 的关键点
- 允许 “半成品” bean:Spring 打破了 “必须完全初始化后才能被引用” 的限制,允许 bean 在未完成属性注入和初始化时就被其他 bean 引用。
- 保证最终一致性:虽然引用的是早期 bean,但 Spring 通过后续步骤确保所有属性最终正确注入,所有初始化方法(如
@PostConstruct
)最终被调用。
为什么需要三级缓存?
若只有二级缓存(直接存早期 bean):
- 当 A 需要代理时,B 会注入 A 的原始实例(而非代理对象),导致 AOP 失效。
- 三级缓存通过
ObjectFactory
延迟生成代理对象,确保 B 注入的是经过 AOP 增强的代理 A。
总结
三级缓存机制的本质是:在不破坏单例模式和 AOP 的前提下,通过 “提前暴露未完成初始化的 bean” 和 “动态生成代理对象”,让循环依赖的 bean 能够互相引用对方的早期状态,最终完成完整初始化。
三、三级缓存如何解决?
三级缓存通过ObjectFactory
延迟生成代理对象,关键在 **“按需生成代理”**:
java
// 三级缓存的正确实现(简化伪代码)
// 1. 创建A时,放入三级缓存的是工厂,而非原始实例
singletonFactories.put(beanName, () -> {
// 关键点:在这里判断是否需要代理,若需要则生成代理对象
return getEarlyBeanReference(beanName, mbd, bean);
});
// 2. 当B需要A时,从三级缓存获取工厂并调用
Object a = singletonFactory.getObject(); // 此时才触发代理生成逻辑
核心流程详解:
- A 实例化后:将
ObjectFactory
放入三级缓存,工厂内部逻辑包含 “是否需要代理” 的判断。 - B 创建时依赖 A:
-
- 从三级缓存获取
ObjectFactory
。 - 调用工厂的
getObject()
方法,触发getEarlyBeanReference()
。 - 在此方法中,Spring 通过
SmartInstantiationAwareBeanPostProcessor
检查 A 是否需要代理:
- 从三级缓存获取
-
-
- 若需要,则生成代理对象并返回;
- 若不需要,则返回原始实例。
-
- 代理对象存入二级缓存:生成的代理对象被放入二级缓存,后续其他 bean 再获取 A 时,直接从二级缓存拿到正确的代理对象。
四、对比示意图
二级缓存方案(失败)
plaintext
A实例化 → 原始A存入二级缓存 → B创建时获取原始A → B完成 → A继续初始化 → A被代理增强
↑ |
└───────────────────────────────────────────┘
B依赖的是原始A,而非代理A!
三级缓存方案(成功)
plaintext
A实例化 → A的ObjectFactory存入三级缓存 → B创建时获取A的工厂 → 工厂生成代理A → 代理A存入二级缓存
↑ |
↓ ↓
A继续初始化 → 最终A(代理)存入一级缓存 ←─── B完成(依赖的是代理A)
五、关键区别总结
缓存级别 |
存储内容 |
解决的问题 |
二级缓存 |
原始 bean 实例(提前暴露) |
无法处理 AOP 代理,导致循环依赖中注入原始对象 |
三级缓存 |
ObjectFactory(延迟生成代理) |
通过延迟代理生成,确保循环依赖中注入正确的代理对象 |
六、为什么代理必须延迟生成?
- 代理生成条件:需要等 bean 实例化完成,且所有
BeanPostProcessor
处理后,才能确定是否需要代理。 - 循环依赖的矛盾:在 A 还未完成初始化时,B 就需要引用 A,此时代理可能尚未生成。
- 三级缓存的巧妙之处:通过
ObjectFactory
将代理生成逻辑延迟到 “真正被引用时”,而非 “实例化后立即生成”。
七、验证代码示例
假设 A 是被 @Transactional 注解的 Service:
java
@Service
public class A {
@Autowired private B b;
@Transactional
public void doSomething() {
// 事务方法
}
}
@Service
public class B {
@Autowired private A a;
public void callA() {
a.doSomething(); // 此处必须调用代理A,才能触发事务
}
}
若使用二级缓存:
- B 中的 a 会是原始实例 → 调用
doSomething()
时无事务管理。
使用三级缓存:
- B 中的 a 是代理实例 → 调用
doSomething()
时触发事务拦截器。
总结
三级缓存的核心价值在于将 “代理生成” 和 “实例化” 解耦,通过延迟执行代理逻辑,确保在循环依赖场景下,早期暴露的 bean 依然是经过 AOP 增强的代理对象,而非原始实例。这是 Spring 解决循环依赖与 AOP 冲突的关键设计。