Java Spring循环依赖的三级缓存解决方案原理
Spring 框架通过其 IoC 容器管理 Bean 的生命周期,但在面对两个或多个 Bean 互相引用(例如 A 依赖 B,B 依赖 A)的“循环依赖”场景时,直接的实例化会导致无限递归,最终抛出 StackOverflowError。为了解决这一问题,Spring 设计了一套精妙的三级缓存机制。这套机制利用不同状态的缓存,在不改变 Bean 核心创建流程的前提下,打破了循环引用的死锁。
一、 理解三级缓存的结构
Spring 使用三个 Map 结构来存储不同状态下的 Bean 对象。这三级缓存各自承担不同的职责,确保对象在创建的各个阶段都能被正确引用。
| 级别 | 变量名 | 中文名称 | 存放内容 | 用途 |
|---|---|---|---|---|
| 一级缓存 | singletonObjects |
单例对象池 | 完全初始化好的 Bean | 存放已经完成实例化、属性填充和初始化的成品 Bean,可以直接供外部使用。 |
| 二级缓存 | earlySingletonObjects |
早期对象缓存 | 仅实例化(未填充属性)的 Bean | 存放提前暴露的 Bean(半成品),用于解决循环依赖中的引用问题。 |
| 三级缓存 | singletonFactories |
单例工厂缓存 | 创建 Bean 的 ObjectFactory(工厂对象) | 存放可以生成 Bean 的工厂(通常是 Lambda 表达式),主要用于在循环依赖发生时生成代理对象(AOP)。 |
二、 三级缓存解决循环依赖的流程
假设场景:Bean A 依赖于 Bean B,同时 Bean B 也依赖于 Bean A。
1. 开始创建 Bean A
Spring 容器调用 getBean(A) 开始创建 A。此时容器检查一级缓存,发现不存在。Spring 执行 A 的构造函数,完成对象的实例化(此时 A 只是一个空壳,属性 B 尚未注入)。紧接着,Spring 将一个可以生成 A 的 ObjectFactory 放入三级缓存 singletonFactories 中。
2. 填充 Bean A 的属性
Spring 开始填充 A 的属性,发现需要依赖 Bean B,于是调用 getBean(B)。
3. 开始创建 Bean B
容器检查一级缓存,B 不存在。Spring 执行 B 的构造函数完成实例化,同样将 B 的 ObjectFactory 放入三级缓存。
4. 填充 Bean B 的属性
Spring 开始填充 B 的属性,发现需要依赖 Bean A,于是再次调用 getBean(A)。
5. 查找 Bean A 的缓存
这是解决循环依赖的关键步骤。容器再次查找 Bean A:
- 检查一级缓存
singletonObjects:A 尚未完成创建,不存在。 - 检查二级缓存
earlySingletonObjects:不存在。 - 检查三级缓存
singletonFactories:找到了 A 的工厂对象。
6. 生成早期引用与缓存升级
容器调用三级缓存中 A 的工厂对象的 getObject() 方法。这一步非常关键,它会根据 A 是否配置了 AOP(面向切面编程)来决定返回原始对象还是代理对象。
- 获取到对象后,Spring 将其放入二级缓存
earlySingletonObjects中。 - 同时,从三级缓存中移除 A 的工厂对象。
此时,Bean B 获得了 Bean A 的早期引用(可能是代理对象)。
7. 完成 Bean B 的初始化
Bean B 拿到了 A 的引用,完成属性填充和初始化。Bean B 此时是一个完整的 Bean,被放入一级缓存 singletonObjects 中,并从二、三级缓存中清理(如果存在)。
8. 回溯完成 Bean A 的初始化
Bean B 创建完成并返回给 Bean A。Bean A 获得 B 的引用,完成自己的属性填充和初始化。最终,Bean A 也变成了一个完整的 Bean,被放入一级缓存 singletonObjects 中,并从二级缓存中移除。
三、 核心流程可视化
以下流程图展示了 A、B 互相依赖时,各级缓存的变化与对象流转过程:
四、 为什么必须使用三级缓存?
很多开发者会疑惑:两级缓存似乎足以解决循环引用(一级存成品,二级存半成品),为什么还需要第三级?
-
延迟代理对象的创建(核心原因)
Spring 的设计原则是:Bean 的代理对象(AOP)通常应该在初始化阶段最后一步创建。如果只有两级缓存,Spring 必须在实例化后立即判断是否需要代理,并提前创建代理对象放入二级缓存。这会迫使所有 Bean 的代理逻辑提前执行,无论是否发生循环依赖。
而使用三级缓存,存放的是工厂而不是具体的对象。只有当真正发生循环依赖(B 需要引用 A)时,才会调用工厂生成对象。如果 A 不被其他 Bean 循环引用,这个工厂永远不会被调用,A 的代理对象就可以按照正常流程在最后一步创建。这保证了 Spring 处理流程的优雅性和性能。 -
保持结构的一致性
三级缓存结构清晰地划分了“工厂”、“早期引用”和“成品”三个阶段,使得代码逻辑分离,易于扩展和维护。
五、 极简代码示例
为了更直观地理解,以下是模拟 Spring 核心逻辑的伪代码:
// 1. 容器获取 Bean A
Object beanA = getBean("A");
public Object getBean(String beanName) {
// 2. 尝试从一级缓存获取成品
Object singleton = getSingleton(beanName);
if (singleton != null) {
return singleton;
}
// 3. 如果未找到,创建 Bean 实例(实例化,非初始化)
Object beanInstance = createBeanInstance(beanName);
// 4. 将工厂放入三级缓存 (关键步骤)
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, beanInstance));
// 5. 填充属性(触发循环依赖 B -> A)
populateBean(beanName, beanInstance);
// 6. 初始化 Bean
initializeBean(beanName, beanInstance);
// 7. 加入一级缓存
addSingleton(beanName, beanInstance);
return beanInstance;
}
protected Object getSingleton(String beanName) {
// 查一级
Object singletonObject = singletonObjects.get(beanName);
if (singletonObject == null) {
// 查二级
singletonObject = earlySingletonObjects.get(beanName);
if (singletonObject == null) {
// 查三级
ObjectFactory<?> singletonFactory = singletonFactories.get(beanName);
if (singletonFactory != null) {
// 调用工厂获取对象(可能是代理)
singletonObject = singletonFactory.getObject();
// 升入二级,删三级
earlySingletonObjects.put(beanName, singletonObject);
singletonFactories.remove(beanName);
}
}
}
return singletonObject;
}
通过这套机制,Spring 巧妙地利用时间差和状态分离,在不牺牲 Bean 生命周期完整性的前提下,完美解决了单例 Bean 的循环依赖问题。

暂无评论,快来抢沙发吧!