目录
- 一.🦁 开始前的废话
- 二. 🦁 什么是循环依赖?
- 三. 🦁Spring 容器解决循环依赖的原理是什么?
- 五. 🦁 三级缓存解决循环依赖的原理
- 六. 🦁 由有参构造方法注入属性的循环依赖如何解决?
- 七.🦁 ENDing
一.🦁 开始前的废话
最近一个小伙伴"李四"面 C 厂后台开发凉凉了,原因是面试官就他简历的微服务项目问了个很常见的面试题,他没答上来!现在咱们来看看面试官和李华的对话:
面试官问道:“你的项目如果遇到循环依赖了咋办?”
李四见状惊喜万分,笑着回答,确实遇到过,官方推荐我在 yaml 文件添加这段代码来解决:
spring:
main:
allow-circular-references: true
面试官:“嗯呢,这个可以!这段代码什么意思?那如果是通过构造函数导入的实例,还能使用这段代码解决嘛?”
李四:我 😥…
面试官:“好的,没关系。回家等消息叭!”
经过面试官二连问,李华败下阵来。那我们现在来讨论一下这个循环依赖应该怎么解决!从源头开始剖析。
二. 🦁 什么是循环依赖?
其实循环依赖是指在我们 Coding 的过程中,由于不好的设计,导致两个或以上的 Bean 相互依赖导致形成了一个闭环(lion 依赖 tiger,反过来 tiger 也在依赖 lion)。在 Springboot 2.6 以前,spring 容器是可以在实例化 Bean 的过程中,是可以自动解决一部分循环依赖的问题(依靠三级缓存),由于这个方案导致了越来越多的 Coder 老是滥用,导致代码质量越来越差,所以从 SpringBoot 2.6 起,就默认把这种方案给禁用了。如果你的项目里还存在循环依赖,SpringBoot 将拒绝启动
!
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| tiger defined in file [/Users/study/personal-projects/easy-web/target/classes/com/log/web/controller/Lion.class]
↑ ↓
| lion (field private com.log.web.controller.Tiger com.log.web.service.Tiger.class)
└─────┘
需要我们在 yaml 或者 properties 文件配置参数来临时开启循环依赖(开启方式就是上面那段代码)。
三. 🦁Spring 容器解决循环依赖的原理是什么?
前面我们说了 Spring 容器可以处理部分循环依赖问题,只不过是高版本的 Spring 需要手动开启。那么 Spring 是如何解决这个问题的呢?
其实 Spring 是依靠三级缓存的方式来处理循环依赖的!
那么它是如何检测存在循环依赖的呢?
其实也比较简单,就是容器在创建 实例A 的过程中,会给它贴一个正在创建的标签,说明它正在创建了,然后递归去创建其依赖的 实例B,当 实例B 也依赖 A 时,并且发现其正处于创建中的状态,那么就说明存在循环依赖了。
那么三级缓存是如何来解决它呢?
首先,我们可以认为实例化一个对象可以简单分为两步:为这个对象填充所需要的属性,而填充对象的属性的方式又有两种:
Spring 容器解决循环依赖可以理解为是在一个闭环中,先将第一个实例A实例化并提前暴露出来,这样闭环上依赖A的B就可以创建完成,那么依此类推,依赖 B 的 C 也可以创建出来了,从而递归可以顺利进行下去,当跳出最后一层递归后,A依赖的D也创建出来了,再将D注入到A上,整个闭环的实例就完成创建了。
-
实例化该对象
-
为这个对象填充所需要的属性,而填充对象的属性的方式又有两种:
- 有参构造方法直接在对象创建时填充进去(Spring 无法自动处理这种方式创建实例的循环依赖);
- 通过 set() 方法填充对象属性,而三级缓存仅对这种方法有效。
Spring 容器解决循环依赖可以理解为是在一个闭环中,先将第一个实例A实例化并提前暴露出来,这样闭环上依赖A的B就可以创建完成,那么依此类推,依赖 B 的 C 也可以创建出来了,从而递归可以顺利进行下去,当跳出最后一层递归后,A依赖的D也创建出来了,再将D注入到A上,整个闭环的实例就完成创建了。
那么为什么说只有对于 set() 方法填充对象属性的方式,Spring容器才能解决呢?原因很简单(以B依赖A,A依赖B为例),A类 通过有参构造的方式在创建实例并同时将属性注入,那么在这个过程中(注意:此时A并没有完成实例创建),它会去寻找它依赖的对象B,此时B类也开始创建实例,但是由于A依赖B,并且A类并没有完成实例创建,所以二者就处在这种尴尬状态,导致最后容器报错!
然而,若是通过 set() 方法填充对象属性的方式,那么此时实例A已经创建完成,只是还没有注入对应的属性(这个我们后文暂且叫装配Bean吧,因为所谓的实例本质上就是一个Bean对象)而已。这就是为什么 Spring 容器为什么只能解决 set() 方法装配 Bean对象的循环依赖。
五. 🦁 三级缓存解决循环依赖的原理
我们前面提了好多次三级缓存!那么三级缓存到底是什么?它是如何解决循环依赖的?
Spring 容器的三个缓存分别如下:
// 1级缓存:存放实例化+属性注入+初始化+代理(如果有代理)后的单例bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 2级缓存:存放实例化+代理(如果有代理)后的单例bean
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// 3级缓存:存放封装了单例bean(实例化的)的对象工厂
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
我们都知道 Spring 容器通过 IOC 创建单例对象,这个对象创建完成后最终都是存放在 singletonObjects 一级缓存里面的,但并不是所有的实例一开始创建就存进一级缓存!创建的时候需要放进不同的缓存。(具体的不讨论,我们现在来看一下循环依赖过程中,三级缓存是如何工作的!)
以 实例A 与 实例B 相互依赖为例子:
首先 Spring 容器先创建 A实例,实例化完成后将其封装为 ObjectFactory 对象先存入到三级缓存 singletonFactories 中;然后容器对 A 做进一步的装配,装配的时候发现 A 依赖 B ,所以 Spring 去三级缓存中寻找 B,发现其还没有创建,所以会先创建 B实例,B 创建完成后被封装为 ObjectFactory 对象被存入三级缓存 singletonObjects ,同时也会先进行装配,其间 Spring 也发现了 B 依赖于 A ,所以会回到三级缓存中寻找 A实例,终于在第三级缓存中发现了被封装为 ObjectFactory 对象的 A,将其取出来通过 getObject() 方法得到 A,拿到 A 后不再将其放回三级缓存,而是存进二级缓存 earlySingletonObjects 中,而三级缓存中的 ObjectFactoryA 也会被移除,这个过程相当于是 A 从三级缓存——>二级缓存。同时也将 A 填充给 B,至此B 完成装配,从三级缓存——>二级缓存,Spring 容器不会忘记还在"嗷嗷待哺"的 实例A,回过头去一级缓存找到 B将其填充给 A,至此 A 完成装配,从二级缓存——>一级缓存,创建结束,循环依赖完美解决。
六. 🦁 由有参构造方法注入属性的循环依赖如何解决?
我们通过一个案例来说明:实现两个相互依赖的类 A B:
@Component
public class A{
private B b;
public A(B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
public B(A a) {
this.a = a;
}
}
启动容器就会发现如下报错:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| b defined in file [.../target/classes/com/demo/service/B.class]
↑ ↓
| a defined in file [.../target/classes/com/demo/service/A.class]
└─────┘
我们通过在 A/B 类上的构造函数添加 @Lazy 注解则会解决这个循环依赖问题。如下:
@Component
public class A{
private B b;
public A(@Lazy B b) {
this.b = b;
}
}
此时则会正常启动了!!!
那么这其中的原理是什么呢?请看下回分解!
七.🦁 ENDing
错过的题目,自己要学会成长,下次再也不错啦!
🦁 其它优质专栏推荐 🦁
🌟《Java核心系列(修炼内功,无上心法)》: 主要是JDK源码的核心讲解,几乎每篇文章都过万字,让你详细掌握每一个知识点!
🌟 《springBoot 源码剥析核心系列》:一些场景的Springboot源码剥析以及常用Springboot相关知识点解读
欢迎加入狮子的社区:『Lion-编程进阶之路』,日常收录优质好文
更多文章可持续关注上方🦁的博客,2023咱们顶峰相见!