1. 什么是循环依赖?
循环依赖是指两个或多个 Bean 之间相互依赖,形成一个闭环。例如:
-
A 依赖 B,B 也依赖 A:
A -> B -> A
-
A 依赖 B,B 依赖 C,C 又依赖 A:
A -> B -> C -> A
2. Spring 支持和不支持的循环依赖类型:
- 支持:
- Setter 方法注入的循环依赖。
- 字段注入(Field Injection)的循环依赖。
- 不支持:
- 构造器注入(Constructor Injection)的循环依赖。
prototype
作用域的循环依赖(任何注入方式都不支持)。
3. Spring 解决循环依赖的核心:三级缓存
Spring 使用“三级缓存”来解决 Setter 方法注入和字段注入的循环依赖。这三级缓存是 DefaultSingletonBeanRegistry
类中的三个 Map
:
-
一级缓存:
singletonObjects
(单例对象缓存)- 存放已经经历了完整生命周期的 Bean 实例,即完全初始化好的单例 Bean。
- 从该缓存中获取的 Bean 可以直接使用。
-
二级缓存:
earlySingletonObjects
(早期单例对象缓存)- 存放早期暴露的 Bean 实例,这些 Bean 实例已经被创建,但尚未完成属性注入和初始化。
- 为了解决循环依赖,Spring 会在 Bean 实例化后、初始化前,将其放入二级缓存,提前暴露一个不完整的 Bean 实例。
-
三级缓存:
singletonFactories
(单例工厂缓存)- 存放创建 Bean 实例的
ObjectFactory
(一个函数式接口,用于创建对象)。 - 当一个 Bean 被放入三级缓存时,它并不直接暴露 Bean 实例,而是暴露一个
ObjectFactory
,通过ObjectFactory.getObject()
可以获取到 Bean 实例。 ObjectFactory
的作用是:- 延迟创建: 直到真正需要 Bean 实例时才调用
getObject()
方法创建。 - 解决 AOP 代理问题: 如果 Bean 需要被 AOP 代理,
ObjectFactory
可以返回代理对象,而不是原始对象。
- 延迟创建: 直到真正需要 Bean 实例时才调用
- 存放创建 Bean 实例的
4. 循环依赖的解决过程(以 Setter 方法注入为例):
假设有两个类 A 和 B,它们通过 Setter 方法相互依赖:
public class A {private B b;public void setB(B b) {this.b = b;}
}public class B {private A a;public void setA(A a) {this.a = a;}
}
Spring 解决 A 和 B 循环依赖的过程如下:
-
创建 A 实例:
- Spring 尝试获取 A,发现 A 不存在,开始创建 A。
- 通过反射实例化 A(调用 A 的无参构造方法),得到一个 A 的原始对象。
- 将 A 的
ObjectFactory
放入三级缓存(singletonFactories.put(beanName, () -> getEarlyBeanReference(beanName, mbd, bean))
)。 - 将一个早期的 A 实例(尚未进行属性注入)放入二级缓存(
earlySingletonObjects.put(beanName, bean)
)。 - 开始对A进行属性注入
-
A 注入 B:
- Spring 发现 A 依赖 B,尝试获取 B。
- 发现 B 不存在,开始创建 B。
- 流程与创建 A 类似
-
创建 B 实例:
- 通过反射实例化 B,得到一个 B 的原始对象。
- 将 B 的 ObjectFactory 放入三级缓存。
- 将一个早期的 B 实例放入二级缓存。
- 开始对B进行属性注入
-
B 注入 A:
- Spring 发现 B 依赖 A,尝试获取 A。
- 由于 A 已经在创建过程中,Spring 会依次从一级缓存、二级缓存、三级缓存中查找 A。
- 在二级缓存中找到早期的 A 实例(虽然 A 还没有完成初始化,但 B 可以先持有 A 的引用)。
- 将早期的 A 实例注入到 B 中。
-
B 完成初始化:
- B 完成属性注入和初始化,从二级缓存和三级缓存中移除,放入一级缓存(
singletonObjects
)。
- B 完成属性注入和初始化,从二级缓存和三级缓存中移除,放入一级缓存(
-
A 继续初始化:
- A 继续进行属性注入,从一级缓存中获取到已经初始化完成的 B 实例。
- A 完成初始化,从二级缓存和三级缓存中移除,放入一级缓存。
循环依赖解决的关键:
- 提前暴露: 在 Bean 实例化后、初始化前,就将其放入二级缓存,提前暴露一个不完整的 Bean 实例。
- 延迟创建: 使用
ObjectFactory
延迟创建 Bean 实例,直到真正需要时才创建。 - 三级缓存: 通过三级缓存,Spring 可以在 Bean 创建的不同阶段获取到 Bean 的引用,从而解决循环依赖。
5. 为什么构造器注入无法解决循环依赖?
因为构造器注入要求在创建对象时就必须传入所有依赖,如果存在循环依赖,A 的构造方法需要 B 的实例,B 的构造方法又需要 A 的实例,这会导致无限递归,无法创建任何一个对象。
6. 为什么 Prototype 作用域无法解决循环依赖?
prototype
作用域的 Bean 每次获取都会创建一个新的实例,Spring 不会对 prototype
Bean 进行缓存,因此无法通过提前暴露的方式解决循环依赖。
总结:
Spring 通过“三级缓存”机制,巧妙地解决了 Setter 方法注入和字段注入的循环依赖问题。