Spring 通过三级缓存机制解决单例 Bean 的循环依赖问题,其核心思想是提前暴露未完全初始化的 Bean 引用。以下是详细流程和原理:
1. 循环依赖的场景
假设两个 Bean 相互依赖:
BeanA
依赖BeanB
BeanB
依赖BeanA
如果没有特殊处理,Spring 会在创建 BeanA
时发现需要 BeanB
,而创建 BeanB
时又需要 BeanA
,导致死循环。
2. 三级缓存的作用
Spring 使用三个 Map(缓存)管理 Bean 的中间状态:
- 一级缓存
singletonObjects
存放完全初始化好的单例 Bean,可直接使用。 - 二级缓存
earlySingletonObjects
存放早期暴露的 Bean 对象(已实例化但未完成属性填充和初始化)。 - 三级缓存
singletonFactories
存放 Bean 工厂对象(ObjectFactory
),用于生成早期 Bean 的引用(可能包含代理对象)。
3. 解决循环依赖的流程
以 BeanA
和 BeanB
相互依赖为例:
步骤 1:创建 BeanA
- 实例化
BeanA
调用构造函数创建BeanA
的实例(此时属性未填充)。 - 暴露早期引用
将BeanA
的工厂对象(ObjectFactory
)放入三级缓存。 - 填充属性
BeanB
Spring 发现BeanA
依赖BeanB
,触发BeanB
的创建。
步骤 2:创建 BeanB
- 实例化
BeanB
调用构造函数创建BeanB
的实例。 - 暴露早期引用
将BeanB
的工厂对象放入三级缓存。 - 填充属性
BeanA
Spring 发现BeanB
依赖BeanA
,尝试从缓存中获取BeanA
:- 一级缓存:无。
- 二级缓存:无。
- 三级缓存:找到
BeanA
的工厂对象,调用getObject()
生成早期引用,并将BeanA
移至二级缓存。
- 完成
BeanB
的初始化
将BeanB
的完整对象放入一级缓存。
步骤 3:回到 BeanA
的初始化
- 获取
BeanB
实例
BeanA
通过已初始化的BeanB
完成属性注入。 - 完成
BeanA
的初始化
将BeanA
放入一级缓存,并从二级缓存中移除。
4. 关键设计点
为什么需要三级缓存?
- 分离职责:
三级缓存通过ObjectFactory
延迟生成 Bean 的早期引用,确保 AOP 代理仅在需要时创建(例如,若BeanA
需要代理,工厂会返回代理对象,避免重复创建)。 - 避免重复代理:
如果只有二级缓存,可能在多个地方重复生成代理对象,破坏单例。
为何构造函数注入无法解决循环依赖?
- 实例化前无法暴露引用:
构造函数注入发生在实例化阶段,此时 Bean 尚未创建,无法提前暴露引用到三级缓存,导致 Spring 抛出BeanCurrentlyInCreationException
。
5. 限制条件
- 仅支持单例 Bean:
原型(Prototype)作用域的 Bean 每次创建新对象,无法通过缓存解决循环依赖。 - 依赖的注入方式必须为 Setter 或字段注入:
构造函数注入无法解决循环依赖。
6. 总结
Spring 通过三级缓存提前暴露未初始化的 Bean 引用,结合延迟代理生成,解决了单例 Bean 的循环依赖问题。其核心是空间换时间,在保证单例和 AOP 一致性的前提下,避免死循环。理解这一机制有助于在开发中避免循环依赖陷阱,或在必要时调整依赖注入方式。