Spring解决循环依赖的原理
通过将自己注入自己,使用代理对象调用add方法解决了事务失效问题,但是这样不会产生循环依赖吗?
在OrdersCreateServiceImpl 中注入的是OrdersCreateServiceImpl 的代理对象,并不是OrdersCreateServiceImpl 本身实例,构不成循环依赖。即使向OrdersCreateServiceImpl 注入的是本身实例也不会报错,Spring通过三级缓存解决循环依赖,会先向成员变量注入一个半成品实例,而后再完成初始化。
Spring通过三级缓存对Bean延迟初始化解决循环依赖。
具体如下:
-
singletonObjects缓存:这是 Spring 容器用来缓存完全初始化好的单例 bean 实例的缓存。
-
earlySingletonObjects缓存:这个缓存是用来保存被实例化但还未完全初始化的 bean (半成品)的引用。
-
singletonFactories缓存:这个缓存保存的是用于创建 bean 实例的 ObjectFactory,用于支持循环依赖的延迟初始化。
Spring 通过这三级缓存的组合,来确保在循环依赖情况下,能够正常初始化 bean。当一个 bean 在初始化过程中需要依赖另一个还未初始化的 bean 时,Spring 会调用相应的 对象工厂来获取对应的 bean 半成品实例,这样就实现了循环依赖的延迟初始化。一旦 bean 初始化完成,它就会被移动到正式的单例缓存中。
对于通过构造方法注入导致循环依赖的在其中一个类的构造方法中使用@Lazy注解注入一个代理对象即可解决。
1. 三级缓存的工作机制
在 Spring 中,Bean 的创建过程分为实例化、填充属性(依赖注入)、初始化三个阶段。为了避免循环依赖的问题,Spring 设计了三级缓存来管理 Bean 的生命周期,确保在某些依赖未完全初始化时,能提供 Bean 的早期引用以打破依赖闭环。
Spring 的三级缓存包括:
1.1 一级缓存 (singletonObjects
)
- 描述: 这是 Spring 中的正式单例缓存,存放的是完全初始化完成的 Bean 实例。
- 目的: 一旦 Bean 完全初始化,它就会被放入这个缓存,并从二级缓存和三级缓存中移除。
1.2 二级缓存 (earlySingletonObjects
)
- 描述: 存放的是那些已经实例化,但尚未进行属性填充和初始化的 Bean 实例(“半成品”)。
- 目的: 当 Spring 需要早期引用时,直接从这个缓存中获取,还没完全初始化,但可以使用。
1.3 三级缓存 (singletonFactories
)
- 描述: 这里保存的是一个 ObjectFactory,用于在需要时生成一个 Bean 的早期引用(通常是通过 AOP 代理的方式)。
- 目的: 当一个 Bean 正在实例化过程中,而另一个 Bean 需要它时,Spring 会通过工厂(
ObjectFactory
)来生成这个 Bean 的早期引用,支持延迟初始化。
2. 循环依赖的处理流程
假设在一个场景中有两个 Bean A
和 B
,它们通过属性互相依赖,即 A
依赖 B
,B
依赖 A
。Spring 通过以下步骤解决这种循环依赖:
2.1 实例化 Bean A
- Spring 首先实例化 Bean
A
(构造方法调用),但还没有进行属性注入和初始化操作。 - 在此时,Spring 会将
A
的早期引用放入 三级缓存singletonFactories
中。
2.2 实例化 Bean B
- 当 Spring 尝试实例化
A
时,发现A
依赖于B
,于是开始实例化B
。 - Spring 同样会把
B
的早期引用放入三级缓存,并开始初始化它。
2.3 解决 Bean A 的依赖
- 在填充
B
的依赖时,发现B
需要A
,此时 Spring 发现A
的实例还未完全初始化,但A
已经存在于三级缓存中。 - Spring 从 三级缓存 中获取
A
的早期引用(这个引用还没有完全初始化,只是一个代理或者半成品对象),并注入到B
中。
2.4 完成初始化
- 现在
B
已经实例化并注入了A
,然后继续完成B
的剩余初始化(比如调用@PostConstruct
或者初始化方法)。 B
完全初始化后,Spring 将B
的实例放入 一级缓存 中,并从 二级缓存 和 三级缓存 中移除。- 接着,Spring 回到
A
,现在B
已经完全初始化,并注入到了A
中,A
可以继续完成自己的初始化。 - 最终,
A
也被放入 一级缓存 中。
通过这种方式,Spring 使用 三级缓存 来解决在 Bean 初始化过程中的循环依赖问题。
3. 构造器注入的循环依赖问题
- 问题: 上述三级缓存机制能够处理 setter 注入 的循环依赖,但对于 构造器注入 的循环依赖,Spring 无法通过缓存机制来解决。
- 原因: 在构造器注入中,Bean 的所有依赖必须在构造器中完全提供,无法等到 Bean 部分构造完成后再注入其他依赖。
- 解决方法:
-
通过
@Lazy
注解来延迟注入 Bean。在构造器注入中,@Lazy
会使依赖项在使用时才被注入,而不是在构造时就立即初始化,这样可以打破循环依赖。
-
@Service
public class A {
private final B b;
@Autowired
public A(@Lazy B b) { // 使用 @Lazy 延迟注入
this.b = b;
}
}
@Service
public class B {
private final A a;
@Autowired
public B(A a) {
this.a = a;
}
}