Spring是如何解决循环依赖的
Spring是如何解决循环依赖的
循环依赖在Spring中主要有三种情况:
- 构造器注入时产生的循环依赖问题
- 多例模式下通过setter方法注入产生的循环依赖问题
- 单例模式下通过setter方法注入产生的循环依赖问题
1. Spring的三级缓存
产生循环依赖的三种情况里,只有单例模式下通过setter方法注入产生的循环依赖问题被Spring解决了,其他两种情况下都会抛出异常。
Spring 创建一个 Bean 流程通常分为三个大步:
- 实例化 (Instantiation):调用构造函数,在堆内存中开辟空间,此时对象已产生(但属性是空的)。
- 属性填充 (Population):注入依赖(Setter 注入或字段注入在此阶段)。
- 初始化 (Initialization):执行
init-method或@PostConstruct。
Spring维护了三个重要的缓存:
- 一级缓存singletonObjects:存放完全初始化好的,可用的Bean实例
- 二级缓存earlySingletonObjects:存放的是需要提前暴露的Bean对象的原始引用 或 代理对应引用,此时的Bean已经实例化,但属性尚未填充,初始化方法尚未执行
- 三级缓存singletonFactories:存放的是Bean的ObjectFactory工厂对象,这是解决循环依赖问题和AOP代理协同工作的关键。当对象实例化后,Spring会创建一个对应对象的ObjectFactory,并放进三级缓存里
假设存在两个相互依赖的单例Bean:BeanA和BeanB。当Spring容器启动时,它会按照以下流程处理:
创建BeanA的实例并提前暴露工厂:
Spring会首先调用BeanA的构造函数进行实例化,此时得到一个原始对象(尚未填充属性)。
紧接着,Spring会将一个特殊的ObjectFactory工厂对象放入第三级缓存中。这个工厂的使命是,当其他的Bean需要引用BeanA时,它能动态返回当前这个半成品BeanA(可能是原始对象,也可能是为应对AOP而提前生成的代理对象)。
此时BeanA的状态是“已实例化但并未初始化”。
填充BeanA的属性时触发BeanB的创建:
Spring开始为BeanA注入属性,发现它依赖于BeanB。
于是容器转向创建BeanB,同样先调用其构造函数实例化,并将BeanB对应的ObjectFactory工厂存入三级缓存
至此,三级缓存中同时存在BeanA和BeanB的工厂,它们都代表尚未完成初始化的半成品。
BeanB属性注入时发现循环依赖:
当Spring尝试填充BeanB的属性时,检测到它需要注入BeanA。
此时容器启动依赖查找:
- 在一级缓存(存放完整Bean)中未找到BeanA
- 在二级缓存(存放已暴露的早期引用)中同样未命中
- 最终在三级缓存中定位到BeanA的工厂
Spring立即调用该工厂的getObject()方法。这个方法会执行关键决策:若BeanA需要AOP代理,则动态生成代理对象(即使BeanA还未初始化)。若无需代理,则直接返回原始对象。
得到的这个早期引用(可能是代理)被放入二级缓存,同时从三级缓存清理工厂条目。
最后,Spring将这个早期引用注入到BeanB的属性中。
完成BeanB的生命周期:
BeanB获得所有依赖后,Spring执行其初始化方法,将其转换为可用的Bean。
随后,BeanB被提升至一级缓存,二级和三级缓存中关于BeanB的临时条目均被清除。
此时,BeanB已准备就绪,可被其他对象使用。
回溯完成BeanA的创建:
随着BeanB创建完成,流程回溯到最初中断的BeanA属性注入环节。Spring将已完备的BeanB实例注入BeanA,接着执行BeanA的初始化方法。
这里有个精妙细节:若之前BeanA生成过早期代理,Spring会直接复用二级缓存中的代理对象作为最终Bean,而非重复创建。
最终,完全初始化的BeanA(可能是原始对象或代理)入驻一级缓存,其早期引用从二级缓存移除。
至此循环闭环完成,两个Bean均可用。
2. 为什么Spring的三级缓存策略仅适用于setter注入的方式,对于构造器注入无效?
Setter 注入的逻辑
在 Setter 注入中,Spring 会先执行第一步(实例化)。一旦第一步完成,这个“半成品”对象的引用就已经产生了。Spring 此时会将这个对象的 ObjectFactory 放入三级缓存。 如果此时发生了循环依赖,另一个 Bean 可以直接从缓存中拿到这个“半成品”的引用,从而完成注入。
构造器注入的逻辑
在构造器注入中,实例化和属性填充是同时发生的。 Spring 必须先拿到构造函数所需的所有参数,才能调用构造函数。如果 A 的构造函数需要 B,Spring 就会去创建 B;如果 B 的构造函数又需要 A,Spring 又要去创建 A。 关键点在于: 此时 A 还没有完成实例化,内存中甚至还没有 A 的引用。既然连对象都没创建出来,Spring 就没办法把 A 放入三级缓存。
如果非要用构造器注入处理循环依赖怎么办?
@Lazy注解
1 | |
原理:当加上 @Lazy 时,Spring 不会立即去寻找真正的 B,而是给 A 注入一个 B 的代理对象 。 因为代理对象是可以立即生成的,A 的构造函数就能顺利完成执行,A 成功实例化。等到 A 真正调用 b.method() 时,代理对象才会去容器里找真正的 B。这相当于人为地把“依赖解析”推迟到了“使用时”,从而打破了死循环。
3. 为什么Spring用三级缓存而不是二级缓存解决循环依赖问题?
Spring用三级缓存解决循环依赖问题,核心是为了正确处理AOP代理的Bean

三级缓存中的ObjecFactory就是解决这个问题的关键。它不是直接缓存对象,而是存了一个能生产对象的工厂
当发生循环依赖时,调用这个工厂的getObject()方法,这时Spring会智能判断:如果这个Bean最终需要代理,就提前生成代理对象并存入二级缓存,如果不需要代理,就返回原始对象。这样一来,B注入的就是A的最终形态(可能是代理对象),后续A初始化完成后也不再会创建新代理,保证了对象全局唯一