1. 什么是循环依赖?
循环依赖指的是两个或多个 bean 互相依赖,形成一个闭环。例如:
A
依赖于B
B
依赖于C
C
又依赖于A
这种关系导致在实例化时会出现死循环,无法完成对象的创建。
2. Spring 如何解决循环依赖?
Spring 主要通过三级缓存(三级 Map)机制来解决循环依赖问题。以下是三级缓存的具体机制:
- Singleton Objects 一级缓存:保存完全初始化好的单例 bean 对象。
- Early Singleton Objects 二级缓存:保存早期曝光的单例对象,这些对象只是实例化了,但还没有完成依赖注入。
- Singleton Factories 三级缓存:保存对象工厂,可以通过工厂获取早期的 bean 实例。
具体实现步骤:
- 创建 bean 实例:
- 当 Spring 容器遇到一个 bean 的创建请求时,首先会尝试从一级缓存
singletonObjects
中获取该 bean。如果获取不到,再继续向下寻找。
- 当 Spring 容器遇到一个 bean 的创建请求时,首先会尝试从一级缓存
- 存储 Bean 对象的引用:
- Spring 会首先创建 bean 的原始实例(通过构造函数或工厂方法进行实例化),但未进行依赖注入。这时将该实例的引用存储在三级缓存
singletonFactories
中,以便循环引用的情况下其他 bean 可以提前访问到这个未完成的 bean 实例。
- Spring 会首先创建 bean 的原始实例(通过构造函数或工厂方法进行实例化),但未进行依赖注入。这时将该实例的引用存储在三级缓存
- 依赖注入和初始化:
- 对 bean 进行属性依赖注入时,如果该 Bean 依赖了其他 bean,那么:
- 尝试从一级缓存
singletonObjects
获取依赖的 bean。 - 如果获取不到,则从二级缓存
earlySingletonObjects
获取。 - 如果仍获取不到,则通过三级缓存
singletonFactories
中保存的 ObjectFactory 创建依赖,并将其移到二级缓存earlySingletonObjects
,过程中可能还会涉及到代理处理(如 AOP)。
- 尝试从一级缓存
- 完成属性依赖注入后,进行初始化。如果一个 bean 依赖的其他 bean 还未完全初始化,Spring 可以在三级缓存中找到它们的早期引用,避免死循环。
- 对 bean 进行属性依赖注入时,如果该 Bean 依赖了其他 bean,那么:
- 完成 Bean 的创建:
- 在完成依赖注入和初始化后,将完全初始化的 bean 放入一级缓存
singletonObjects
,并将其从二级缓存和三级缓存中移除。
- 在完成依赖注入和初始化后,将完全初始化的 bean 放入一级缓存
3. 示例代码分析
假设我们有两个互相依赖的类:
@Component
public class A {
@Autowired
private B b;
public A() {
System.out.println("A instance created");
}
public void doSomething() {
b.doSomething();
}
}
@Component
public class B {
@Autowired
private A a;
public B() {
System.out.println("B instance created");
}
public void doSomething() {
a.doSomething();
}
}
在这种情况下,Spring 的三级缓存机制将会起作用:
-
A实例创建:
- Spring 首先创建
A
的实例。 - A 的构造函数执行,但此时
A
还未完成依赖注入,它会将A
的早期引用放入三级缓存。
- Spring 首先创建
-
B实例创建:
- 在
A
的依赖注入过程中,需要注入B
,此时 Spring 创建B
的实例。 - B 的构造函数执行,但此时
B
也还未完成依赖注入,它会将B
的早期引用放入三级缓存。
- 在
-
再次注入A:
- 在
B
的依赖注入过程中,需要注入A
。此时Spring 会尝试从三级缓存中获取A
的早期引用并注入B
。
- 在
-
完成依赖注入:
A
和B
的依赖注入完成后,Spring 将A
和B
放入一级缓存,并将它们从三级缓存中移除。
4. 注意事项
-
构造函数注入的限制:如果两个类通过构造函数互相注入,三级缓存机制将无法解决这种循环依赖,因为在构造函数调用时就需要实例化依赖的对象。这种情况下,需要考虑通过其他方式(比如使用
@PostConstruct
或者Setter
方法)来处理依赖注入。 -
非单例模式:Spring 无法解决
prototype
范围的 bean 的循环依赖问题。prototype
范围的 bean 每次获取都是新实例,无法利用缓存机制解决循环依赖。如果涉及prototype
范围的 bean,建议重构代码来避免循环依赖。