Spring 是如何解决循环依赖的?

4 阅读7分钟

循环依赖是 Spring 面试里非常经典的一道题。
如果只回答“Spring 用三级缓存解决循环依赖”,通常不够。面试官更想听到的是:

  • 什么是循环依赖
  • 为什么会产生
  • Spring 为什么能解决
  • 为什么要三级缓存
  • 哪些情况能解决,哪些情况不能解决

下面按完整逻辑展开。

一、什么是循环依赖

循环依赖指的是多个 Bean 之间互相依赖,形成闭环。

最简单的例子:

class A {
    @Autowired
    private B b;
}

class B {
    @Autowired
    private A a;
}

这里:

  • A 依赖 B
  • B 又依赖 A

这就是最典型的循环依赖。


二、循环依赖为什么会出问题

假设 Spring 要创建 A:

第一步,先实例化 A。
第二步,发现 A 依赖 B,于是去创建 B。
第三步,创建 B 时又发现 B 依赖 A,于是又想回过头创建 A。
这时就会出现“创建 A -> 创建 B -> 又要创建 A”的死循环。

如果没有额外机制,程序就无法完成对象创建。


三、Spring 能解决哪种循环依赖

Spring 并不是所有循环依赖都能解决,它主要能解决的是:

单例 Bean + setter/字段注入 的循环依赖

例如:

  • 单例 A 依赖单例 B
  • 单例 B 依赖单例 A
  • 依赖通过 @Autowired 字段注入或 setter 注入完成

这种情况,Spring 通常可以解决。

但下面这些情况通常解决不了:

1. 构造器循环依赖

例如:

class A {
    public A(B b) {}
}

class B {
    public B(A a) {}
}

因为构造器注入要求对象在创建时就拿到完整依赖,而此时对方对象连“半成品”都还没有,所以无法提前暴露,最终会失败。

2. prototype Bean 循环依赖

prototype Bean 不会进入单例缓存体系,Spring 无法借助缓存暴露早期对象,因此一般也解决不了。


四、Spring 解决循环依赖的核心思想

Spring 解决循环依赖的关键思想是:

提前暴露一个“未完全初始化”的对象引用,让其他 Bean 先用上,等双方都创建完成后,再补全初始化。

这个思想的核心实现,就是 三级缓存


五、三级缓存分别是什么

Spring 单例 Bean 的循环依赖解决,主要依赖三个缓存:

1. 一级缓存:singletonObjects

存放的是 完全初始化完成的单例对象

这是正式可用的 Bean 缓存。


2. 二级缓存:earlySingletonObjects

存放的是 提前暴露的早期对象,也可以理解为“半成品对象”。

这些对象通常已经实例化了,但还没有走完整个初始化流程。


3. 三级缓存:singletonFactories

存放的是 对象工厂 ObjectFactory

它的作用是:
当确实需要早期引用时,通过工厂创建对象的早期引用。这个早期引用有可能是原始对象,也有可能是代理对象。


六、循环依赖的解决流程

下面用 A 和 B 相互依赖来说明。

第一步:创建 A

Spring 先实例化 A。
注意,这时候只是“实例化”,属性还没有注入。

第二步:把 A 的 ObjectFactory 放入三级缓存

Spring 不会马上把完整 A 放进一级缓存,因为它还没初始化完。
但为了防止后续循环依赖,Spring 会先把一个能够返回 A 早期引用的工厂放到三级缓存中。

第三步:A 需要注入 B,于是开始创建 B

Spring 发现 A 依赖 B,于是进入 B 的创建流程。

第四步:创建 B 时发现它依赖 A

这时 Spring 去容器中找 A:

  • 一级缓存里没有,因为 A 还没创建完成
  • 然后看二级缓存
  • 如果二级缓存也没有,就去三级缓存找 A 对应的 ObjectFactory

第五步:从三级缓存拿到 A 的早期引用

Spring 通过 ObjectFactory 获取 A 的早期引用,并把它放入二级缓存,同时从三级缓存移除。

此时 B 就可以先拿到 A 的引用,完成自己的依赖注入。

第六步:B 完成初始化,进入一级缓存

B 创建完成后,放入一级缓存。

第七步:回到 A,完成对 B 的注入

此时 A 再注入 B,B 已经是完整对象了,因此 A 也可以继续完成初始化。

第八步:A 初始化完成,进入一级缓存

A 完整创建好后,Spring 会把 A 放入一级缓存,并清除二级缓存中的早期引用。

这样,A 和 B 都成功创建,循环依赖被解决。


七、为什么必须是三级缓存,二级缓存不够吗

这是高频追问。

很多人会觉得:

  • 一级缓存存完整对象
  • 二级缓存存半成品对象

那为什么还要三级缓存?

关键原因是:
Spring 不只是要解决循环依赖,还要兼容 AOP 代理。

如果没有三级缓存,只有二级缓存,那么提前暴露出去的就只能是原始半成品对象。
但如果这个 Bean 后续需要被 AOP 代理,其他依赖它的 Bean 注入到的就是原始对象,而不是代理对象,这就会出问题。

三级缓存存的是 ObjectFactory,它不是直接存对象,而是提供一个“获取早期引用”的工厂。
这样 Spring 在需要时可以决定:

  • 返回原始对象
  • 或者返回提前创建的代理对象

因此三级缓存的意义在于:

延迟决定到底暴露原始对象还是代理对象,从而兼容 AOP。

所以结论是:

  • 二级缓存解决不了代理对象提前暴露的问题
  • 三级缓存是为了解决循环依赖与 AOP 代理共存的问题

八、为什么构造器循环依赖解决不了

构造器注入时,对象要先拿到完整依赖才能被创建出来。
例如创建 A,必须先有 B;创建 B,又必须先有 A。

问题在于:

  • setter/字段注入时,对象可以先实例化,再注入属性
  • 构造器注入时,对象连实例化都做不到

而 Spring 的三级缓存机制,前提是“对象已经先实例化出来了”,才能提前暴露早期引用。
构造器循环依赖在实例化阶段就卡死了,因此解决不了。


九、循环依赖与 AOP 的关系

Spring 解决循环依赖时,之所以设计成三级缓存,很大程度上就是为了兼容 AOP。

因为一个 Bean 在生命周期后期可能会被代理。
如果依赖方提前拿到的是原始对象,而容器最终放进去的是代理对象,那么容器中同一个 Bean 就会出现两个版本:

  • 一个是依赖方持有的原始对象
  • 一个是容器最终使用的代理对象

这会导致行为不一致,比如事务、日志、权限等增强失效。

因此 Spring 在提前暴露对象时,必须通过三级缓存中的 ObjectFactory 来决定是否返回代理对象,这就是三级缓存存在的重要原因。


十、总结

Spring 主要通过三级缓存解决单例 Bean 的属性注入循环依赖。具体来说,一级缓存存放完整初始化好的单例对象,二级缓存存放提前暴露的早期对象,三级缓存存放能够生成早期引用的对象工厂。当 Bean A 创建过程中依赖 Bean B,而 B 又依赖 A 时,Spring 会先实例化 A,并将 A 的 ObjectFactory 放入三级缓存;当 B 需要注入 A 时,就可以从三级缓存获取 A 的早期引用,先完成 B 的创建,之后再回过头完成 A 的创建。之所以要三级缓存而不是二级缓存,是因为 Spring 还要兼容 AOP 代理,三级缓存可以通过 ObjectFactory 决定提前暴露的是原始对象还是代理对象。需要注意的是,Spring 主要只能解决单例 Bean 的 setter 或字段注入循环依赖,构造器循环依赖和 prototype Bean 循环依赖通常无法解决。