目录
3.【举个例子】那三级缓存是怎么解决上述代码例子中的A、B互相依赖呢?
一、什么是循环依赖?
循环依赖发生在两个或多个 Bean 之间相互依赖的情况下。
例如,假设我们有两个 Bean:
A
和B
,其中A
依赖B
,而B
又依赖A
,这就形成了循环依赖。如下所示://在这种情况下,Spring 在创建 Bean A 时需要注入 B, //而创建 B 时又需要注入 A,这就导致了一个无法打破的循环。 @Component public class A { @Autowired private B b; } @Component public class B { @Autowired private A a; }
有一点“死锁”的味道。
二、关于传说中的三级缓存
1.基本概念:
三级缓存通过在 Bean 的创建过程中提前暴露未完全初始化的 Bean 的引用,来解决单例 Bean 的循环依赖问题。
大白话:“管你初始化没初始化好,先拉出来遛遛~”,就是把还没准备好的对象,先拉出来用用,在用的过程中该对象也会逐步配置完全。
2.三级缓存是哪三级?
· 三级缓存singletonFactories:
存储的是用于创建单例 Bean 实例的工厂对象。
在 Bean 创建的早期阶段,Spring 会将一个工厂对象放入三级缓存,以便在需要时生成该 Bean 的早期引用。这是为了在需要解决循环依赖时,可以通过这个工厂对象获取到 Bean 的引用。创建的 Bean 引用随后可以被放入二级缓存。
· 二级缓存 earlySingletonObjects:
用于存储早期暴露的单例 Bean。这些 Bean 已经实例化但还未完成依赖注入和初始化过程。
当一个 Bean 在创建过程中需要提前暴露自身(通常是为了解决循环依赖),Spring 会将其放入二级缓存中,以便其他 Bean 可以引用这个未完全初始化的 Bean。
· 一级缓存:singletonObjects
这是最主要的缓存,用于存储完全初始化好的单例 Bean 对象。
当一个 Bean 完全初始化完成后,Spring 会将它放入这个一级缓存中。在后续获取该 Bean 时,Spring 直接从这里获取已经初始化好的实例。
3.【举个例子】那三级缓存是怎么解决上述代码例子中的A、B互相依赖呢?
详细过程:(理解用)
a. 创建 A 的实例
- 当 Spring 容器开始创建 A 时,它会先调用 A 的构造函数来实例化 A 对象。
- 这个时候,A 还没有完成依赖注入(即 B 还没有注入到 A 中),只是创建了 A 的一个原始实例。
b. 将 A 的早期引用放入三级缓存
- Spring 识别到可能存在循环依赖问题,因此它会将 A 的一个工厂对象(ObjectFactory<A>)放入三级缓存中 (singletonFactories)。
- 这个工厂对象用于在后续依赖注入过程中,提供 A 的早期引用(即尚未完全初始化的 A 实例)。
c. 创建 B 的实例
- 紧接着,Spring 需要创建 B,于是调用 B 的构造函数来实例化 B 对象。
- 在 B 的实例化过程中,Spring 发现 B 依赖于 A(因为 B 中有 @Autowired 注解的 A)。
d. 从三级缓存中获取 A 的早期引用
- Spring 检查三级缓存,发现 A 的工厂对象已存在。它从三级缓存中获取 A 的早期引用,并将这个引用放入二级缓存 (earlySingletonObjects) 中,同时从三级缓存中移除该工厂对象。
- 此时,B 就可以引用 A 的这个早期引用,完成 B 的依赖注入。
e. 完成 B 的创建
- B 完成依赖注入和初始化后,Spring 将 B 放入一级缓存 (singletonObjects) 中,表示 B 已经完全创建并准备好被使用。
f. 回到 A,完成依赖注
- 现在,B 已经创建完毕,回到 A,继续完成 A 的依赖注入过程。这时 B 已经存在于一级缓存中,可以直接注入到 A 中。
- 最后,A 也完成了初始化,并被放入一级缓存。
简约版:(理解后看这个就行)
1. 创建 A 实例 → A 放入三级缓存 → 开始创建 B
2. 创建 B 实例 → B 发现依赖 A → 从三级缓存获取 A 的早期引用 → A 的早期引用放入二级缓存
3. B 完成创建 → B 放入一级缓存
4. 回到 A → 从一级缓存获取 B → 完成 A 的创建 → A 放入一级缓存
三、关于三级缓存的适用范围
适用的循环依赖范围:
Spring 三级缓存机制主要用于解决单例(Singleton)作用域的 Bean 循环依赖问题。
细心的读者已经发现了,标题强调的是“单例Bean循环依赖”,且上述解决AB相互依赖的详细步骤中,关于A、B实例的创建的标红部分。
不适用的循环依赖范围:
1.原型(Prototype)作用域的循环依赖
对于原型作用域的 Bean,每次请求都会创建一个新的实例。因此,原型 Bean 没有全局缓存,也不会在创建过程中被缓存起来,没有缓存机制,Spring 无法像处理单例 Bean 那样通过三级缓存提前暴露 Bean 的引用来解决循环依赖。
如果原型 Bean 之间存在循环依赖,Spring 无法使用三级缓存解决这个问题,通常会抛出
BeanCurrentlyInCreationException
。
2.构造函数注入的循环依赖
当两个 Bean 通过构造函数相互依赖时,Spring 无法通过三级缓存来解决这种类型的循环依赖,因为即使是生成原始对象,也要通过构造函数
3.AOP 代理引发的复杂循环依赖
在使用 AOP时,Spring 可能会创建代理对象。如果 AOP 代理涉及的 Bean 存在循环依赖,AOP 代理对象和原始 Bean 之间的关系可能导致复杂的依赖链,这种依赖链可能无法通过三级缓存的简单缓存机制来解决
4.某些非单例作用域的自定义 Bean Scope
除了单例和原型作用域外,Spring 允许定义自定义的作用域。如果这种自定义作用域的生命周期和依赖处理方式与单例不同,三级缓存机制可能无法适用于这些自定义作用域的 Bean。
5.循环依赖与非自动装配的复杂场景
某些情况下,Bean 的依赖关系并非通过 Spring 自动装配管理,而是通过手动获取或其他复杂的方式管理。这种情况下,三级缓存机制可能无法适用。
四、那只用二级缓存行不行?
1.只用二级缓存:
细心的小伙伴就会问了,那上面的第三级缓存是不是可以砍掉,⼀级缓存保存完整的Bean,⼆级缓存保存不完整的Bean。是这样的道理,二级缓存也能满足循环依赖的注入。在Spring3.6之前,Spring是通过使⽤⼀级和⼆级缓存来解决单例Bean的循环依赖问题。
二级局限性:过早暴露Bean
仅使用二级缓存,在创建一个 Bean 时,需要立即将其尚未完全初始化的实例放入二级缓存中。这可能会导致半成品 Bean 被过早地使用,容易引发问题,尤其是在 Bean 的初始化步骤中可能进行一些关键操作(如依赖检查、代理生成等)。
2.三级缓存的优点:
从 3.6版本开始,通过引入三级缓存,Spring能够更全⾯地处理包括AOP在内的更复杂的循环依赖情况。 三级缓存中的 存储的是工厂对象 而不是 Bean 的实例,这样在需要时可以动态生成 Bean 的引用。
这种设计允许 Spring 在真正需要的时候才将早期引用暴露出来,而不是在 Bean 实例创建的最初阶段。这种延迟初始化方式可以避免很多潜在的并发问题和初始化过程中不一致的情况。