Spring是如何解决循环依赖的

Spring是如何解决循环依赖的

循环依赖在Spring中主要有三种情况:

  • 构造器注入时产生的循环依赖问题
  • 多例模式下通过setter方法注入产生的循环依赖问题
  • 单例模式下通过setter方法注入产生的循环依赖问题

1. Spring的三级缓存

产生循环依赖的三种情况里,只有单例模式下通过setter方法注入产生的循环依赖问题被Spring解决了,其他两种情况下都会抛出异常。

Spring 创建一个 Bean 流程通常分为三个大步:

  1. 实例化 (Instantiation):调用构造函数,在堆内存中开辟空间,此时对象已产生(但属性是空的)。
  2. 属性填充 (Population):注入依赖(Setter 注入或字段注入在此阶段)。
  3. 初始化 (Initialization):执行 init-method@PostConstruct

Spring维护了三个重要的缓存:

  • 一级缓存singletonObjects:存放完全初始化好的,可用的Bean实例
  • 二级缓存earlySingletonObjects:存放的是需要提前暴露的Bean对象的原始引用 或 代理对应引用,此时的Bean已经实例化,但属性尚未填充,初始化方法尚未执行
  • 三级缓存singletonFactories:存放的是Bean的ObjectFactory工厂对象,这是解决循环依赖问题和AOP代理协同工作的关键。当对象实例化后,Spring会创建一个对应对象的ObjectFactory,并放进三级缓存里

假设存在两个相互依赖的单例Bean:BeanA和BeanB。当Spring容器启动时,它会按照以下流程处理:

  1. 创建BeanA的实例并提前暴露工厂:

    Spring会首先调用BeanA的构造函数进行实例化,此时得到一个原始对象(尚未填充属性)。

    紧接着,Spring会将一个特殊的ObjectFactory工厂对象放入第三级缓存中。这个工厂的使命是,当其他的Bean需要引用BeanA时,它能动态返回当前这个半成品BeanA(可能是原始对象,也可能是为应对AOP而提前生成的代理对象)

    此时BeanA的状态是“已实例化但并未初始化”。

  2. 填充BeanA的属性时触发BeanB的创建:

    Spring开始为BeanA注入属性,发现它依赖于BeanB。

    于是容器转向创建BeanB,同样先调用其构造函数实例化,并将BeanB对应的ObjectFactory工厂存入三级缓存

    至此,三级缓存中同时存在BeanA和BeanB的工厂,它们都代表尚未完成初始化的半成品。

  3. BeanB属性注入时发现循环依赖:

    当Spring尝试填充BeanB的属性时,检测到它需要注入BeanA。

    此时容器启动依赖查找:

    • 在一级缓存(存放完整Bean)中未找到BeanA
    • 在二级缓存(存放已暴露的早期引用)中同样未命中
    • 最终在三级缓存中定位到BeanA的工厂

    Spring立即调用该工厂的getObject()方法。这个方法会执行关键决策:若BeanA需要AOP代理,则动态生成代理对象(即使BeanA还未初始化)。若无需代理,则直接返回原始对象

    得到的这个早期引用(可能是代理)被放入二级缓存,同时从三级缓存清理工厂条目。

    最后,Spring将这个早期引用注入到BeanB的属性中。

  4. 完成BeanB的生命周期:

    BeanB获得所有依赖后,Spring执行其初始化方法,将其转换为可用的Bean。

    随后,BeanB被提升至一级缓存,二级和三级缓存中关于BeanB的临时条目均被清除。

    此时,BeanB已准备就绪,可被其他对象使用。

  5. 回溯完成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
2
3
public A(@Lazy B b) {
this.b = b;
}

原理:当加上 @Lazy 时,Spring 不会立即去寻找真正的 B,而是给 A 注入一个 B 的代理对象 。 因为代理对象是可以立即生成的,A 的构造函数就能顺利完成执行,A 成功实例化。等到 A 真正调用 b.method() 时,代理对象才会去容器里找真正的 B。这相当于人为地把“依赖解析”推迟到了“使用时”,从而打破了死循环

3. 为什么Spring用三级缓存而不是二级缓存解决循环依赖问题?

Spring用三级缓存解决循环依赖问题,核心是为了正确处理AOP代理的Bean

image-20260310185453818

三级缓存中的ObjecFactory就是解决这个问题的关键。它不是直接缓存对象,而是存了一个能生产对象的工厂

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


Spring是如何解决循环依赖的
http://example.com/2026/03/10/Spring是如何解决循环依赖的/
作者
Kon4tsu
发布于
2026年3月10日
许可协议