知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方请评论,我们一起交流!
Spring 解决循环依赖的核心机制:三级缓存
Spring 通过 三级缓存 和 提前暴露未完成初始化的 Bean 的机制解决循环依赖问题,具体流程如下:
1. 循环依赖的典型场景
假设存在两个 Bean 的相互依赖:
java
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
在初始化时,Spring 会尝试创建 A → 发现 A 依赖 B → 创建 B → 发现 B 依赖 A → 此时 A 尚未初始化完成,形成循环依赖。
2. 三级缓存的结构
Spring 通过三个缓存区管理 Bean 的创建状态:
缓存名称 | 存储内容 | 作用 |
---|---|---|
singletonObjects |
完全初始化完成的单例 Bean | 最终对外暴露的 Bean 实例 |
earlySingletonObjects |
提前暴露的未完成初始化的 Bean(半成品) | 解决循环依赖时,避免重复创建代理对象 |
singletonFactories |
Bean 的工厂对象(ObjectFactory ) |
生成早期 Bean 的引用(可能生成代理对象,如 AOP 场景) |
3. 循环依赖解决流程(以 A → B → A 为例)
步骤 1:创建 Bean A
-
实例化 A
调用 A 的构造函数,生成一个未设置属性的原始对象(此时a.b = null
)。 -
将 A 的工厂对象存入三级缓存
Spring 将 A 的工厂对象(用于生成早期引用)存入singletonFactories
:javaaddSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
步骤 2:填充 A 的属性(依赖 B)
- 发现 A 依赖 B
Spring 尝试从容器中获取 B,但 B 尚未创建,触发 B 的创建流程。
步骤 3:创建 Bean B
- 实例化 B
调用 B 的构造函数,生成原始对象(此时b.a = null
)。 - 将 B 的工厂对象存入三级缓存
类似 A 的流程,将 B 的工厂对象存入singletonFactories
。
步骤 4:填充 B 的属性(依赖 A)
-
发现 B 依赖 A
Spring 尝试获取 A:
- 从
singletonObjects
查找 → 不存在。 - 从
earlySingletonObjects
查找 → 不存在。 - 从
singletonFactories
获取 A 的工厂对象 → 生成 A 的早期引用(可能被 AOP 代理)。 - 将 A 的早期引用存入
earlySingletonObjects
,并从singletonFactories
移除。
- 从
-
将 A 的早期引用注入到 B
此时 B 的
a
属性指向 A 的早期引用(未完成初始化的对象)。
步骤 5:完成 B 的初始化
- 执行 B 的初始化方法 (如
@PostConstruct
、InitializingBean
)。 - 将 B 放入
singletonObjects
,并从earlySingletonObjects
和singletonFactories
中移除。
步骤 6:完成 A 的初始化
- 将 B 的完整实例注入到 A。
- 执行 A 的初始化方法。
- 将 A 放入
singletonObjects
,并从earlySingletonObjects
移除。
4. 关键实现细节
-
提前暴露对象
在 Bean 实例化后、属性填充前,将工厂对象存入三级缓存,允许其他 Bean 获取其早期引用。
-
处理代理对象
如果 Bean 需要被代理(如 AOP),
getEarlyBeanReference()
会通过SmartInstantiationAwareBeanPostProcessor
(如AbstractAutoProxyCreator
)生成代理对象,确保最终注入的是同一个代理实例。 -
循环依赖的检测
在创建 Bean 时,Spring 会将当前正在创建的 Bean 名称存储在
singletonsCurrentlyInCreation
集合中。若在依赖查找时发现该 Bean 已在创建中,则触发循环依赖处理逻辑。
5. 无法解决的循环依赖场景
-
构造器注入的循环依赖
若两个 Bean 均通过构造器注入对方,则无法解决:
javapublic class A { public A(B b) { /* ... */ } } public class B { public B(A a) { /* ... */ } }
原因:构造器注入需在实例化阶段完成,但此时 Bean 尚未放入三级缓存,无法提前暴露引用。
-
原型作用域(Prototype)的循环依赖
Spring 不会缓存原型 Bean,因此无法通过三级缓存解决循环依赖。
6. 源码核心逻辑
-
DefaultSingletonBeanRegistry
类
管理三级缓存的核心代码:javaprotected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return singletonObject; }
7. 最佳实践与设计建议
-
避免循环依赖
循环依赖通常是设计缺陷的标志,应通过重构代码(如引入中间层、使用事件驱动)消除。
-
优先使用 Setter 注入
若必须使用循环依赖,Setter 注入比字段注入更安全(显式管理依赖关系)。
-
监控与告警
通过 Spring 的
BeanCurrentlyInCreationException
日志发现潜在循环依赖问题。
总结
机制 | 实现要点 |
---|---|
三级缓存 | 通过 singletonFactories 提前暴露未完成初始化的 Bean,打破循环依赖链。 |
代理处理 | 结合 BeanPostProcessor 确保 AOP 代理对象的唯一性。 |
作用域限制 | 仅支持单例 Bean 的循环依赖,构造器注入和原型 Bean 无法解决。 |
设计意义 | 循环依赖应作为临时解决方案,长期需通过代码优化消除。 |