01.3 — 循环依赖与三级缓存
定位: Spring 如何用三级缓存解决循环依赖——以及为什么不用二级 面试高频度: ⭐⭐⭐⭐⭐ 考查方式: 三级缓存机制、延迟代理创建、构造器注入循环依赖
一、这是什么?为什么需要它?
循环依赖:A 依赖 B,B 又依赖 A。如果不做特殊处理,容器会陷入"创建 A -> 需要 B -> 创建 B -> 需要 A -> ..."的死循环。
Spring 通过三级缓存机制,在 Bean 未完成初始化时提前暴露半成品引用,打破循环。
二、原理拆解
2.1 三级缓存数据结构
java
// DefaultSingletonBeanRegistry 中的三个缓存
// 1. 一级缓存 — 完全初始化好的 Bean
Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 2. 二级缓存 — 提前暴露的半成品 Bean(尚未完成属性赋值和初始化)
Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 3. 三级缓存 — ObjectFactory 工厂
Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);2.2 三级缓存工作流程图
Bean A 开始创建
-> A 实例化
-> A 放入三级缓存 (singletonFactories)
-> A 注入依赖 B
-> Bean B 是否已创建? -> 否 -> 创建 Bean B
-> B 实例化
-> B 放入三级缓存
-> B 注入依赖 A
-> 从三级缓存获取 A 的 ObjectFactory
-> 调用 ObjectFactory -> 生成 A 的早期引用
-> 移除三级缓存 -> 放入二级缓存
-> 将早期引用注入 B
-> B 完成初始化 -> B 移入一级缓存
-> A 从二级缓存获取完整 B
-> A 完成初始化 -> A 移入一级缓存三、关键问题:为什么不是二级缓存?
| 方案 | 问题 |
|---|---|
| 只用两级缓存 | 如果直接用二级缓存存"半成品 Bean",当 AOP 需要创建代理时,必须在放入二级缓存前就创建代理。这导致所有 Bean 实例化后就创建代理,即使没有循环依赖也白白创建 |
| 三级缓存 | ObjectFactory 是延迟代理创建的关键。只有 Bean 真的被循环依赖引用时,才会触发工厂调用创建代理对象。无循环依赖则全程不创建代理 |
核心结论:三级缓存解决了"延迟代理创建"的问题——让普通 Bean 在最终阶段(postProcessAfterInitialization)才创建代理,仅当发生循环依赖时才提前创建。
局限
- 仅解决 singleton + 非构造器注入 的循环依赖
- 构造器注入 的循环依赖无法解决(实例化前就需要依赖)
- prototype 作用域的循环依赖也无法解决
2.3 @Lazy 代理原理
java
@Component
@Lazy // 延迟初始化
public class ExpensiveBean { ... }
@Component
public class Client {
@Autowired
@Lazy // 为这个依赖生成懒加载代理
private ExpensiveBean expensiveBean;
}工作原理:
- Spring 为 @Lazy 标注的依赖创建一个 CGLIB 代理对象
- 代理对象注入到目标 Bean 中
- 首次调用代理对象的方法时,才真正去容器中查找/创建目标 Bean
四、实战验证
java
// 验证三级缓存
// 运行以下配置观察三级缓存行为
@Configuration
public class CircularConfig {
@Bean
public DependencyA dependencyA() { return new DependencyA(dependencyB()); }
@Bean
public DependencyB dependencyB() { return new DependencyB(dependencyA()); }
// singleton + 构造器注入 -> BeanCurrentlyInCreationException!
}五、面试视角
| 追问 | 答案要点 |
|---|---|
| Spring 如何解决循环依赖? | 三级缓存机制,提前暴露半成品 Bean 的 ObjectFactory |
| 为什么不用二级缓存? | 二级缓存无法实现"延迟代理创建",所有 Bean 实例化后必须立即创建代理 |
| 构造器注入的循环依赖能解决吗? | 不能,因为实例化前就需要依赖,抛 BeanCurrentlyInCreationException |
| prototype 的循环依赖怎么办? | 无法解决,Spring 不缓存 prototype Bean |
| @Lazy 的实现原理? | CGLIB 生成代理对象,首次调用才真正创建目标 Bean |
📚 相关链接
- **Bean生命周期** — Bean 的完整生命周期中,循环依赖发生在属性赋值阶段
- ← 返回 **返回 IoC 容器索引**