【CVTE 一面凉经Ⅰ】循环依赖如何解决

目录

  • [一.🦁 开始前的废话](#一.🦁 开始前的废话)
  • [二. 🦁 什么是循环依赖?](#二. 🦁 什么是循环依赖?)
  • [三. 🦁Spring 容器解决循环依赖的原理是什么?](#三. 🦁Spring 容器解决循环依赖的原理是什么?)
  • [五. 🦁 三级缓存解决循环依赖的原理](#五. 🦁 三级缓存解决循环依赖的原理)
  • [六. 🦁 由有参构造方法注入属性的循环依赖如何解决?](#六. 🦁 由有参构造方法注入属性的循环依赖如何解决?)
  • [七.🦁 ENDing](#七.🦁 ENDing)

一.🦁 开始前的废话

最近一个小伙伴"李四"面 C 厂后台开发凉凉了,原因是面试官就他简历的微服务项目问了个很常见的面试题,他没答上来!现在咱们来看看面试官和李华的对话:

面试官问道:"你的项目如果遇到循环依赖了咋办?"

李四见状惊喜万分,笑着回答,确实遇到过,官方推荐我在 yaml 文件添加这段代码来解决:

yml 复制代码
spring:
  main:
    allow-circular-references: true

面试官:"嗯呢,这个可以!这段代码什么意思?那如果是通过构造函数导入的实例,还能使用这段代码解决嘛?"

李四:我 😥...

面试官:"好的,没关系。回家等消息叭!"


经过面试官二连问,李华败下阵来。那我们现在来讨论一下这个循环依赖应该怎么解决!从源头开始剖析。

二. 🦁 什么是循环依赖?

其实循环依赖是指在我们 Coding 的过程中,由于不好的设计,导致两个或以上的 Bean 相互依赖导致形成了一个闭环 (lion 依赖 tiger,反过来 tiger 也在依赖 lion)。在 Springboot 2.6 以前,spring 容器是可以在实例化 Bean 的过程中,是可以自动解决一部分循环依赖的问题(依靠三级缓存),由于这个方案导致了越来越多的 Coder 老是滥用,导致代码质量越来越差,所以从 SpringBoot 2.6 起,就默认把这种方案给禁用了。如果你的项目里还存在循环依赖,SpringBoot 将拒绝启动!

java 复制代码
***************************
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 容器的三个缓存分别如下:

java 复制代码
// 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:

java 复制代码
@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;
    }
}

启动容器就会发现如下报错:

java 复制代码
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 注解则会解决这个循环依赖问题。如下:

java 复制代码
@Component
public class A{
    private B b;

    public A(@Lazy B b) {
        this.b = b;
    }
}

此时则会正常启动了!!!

那么这其中的原理是什么呢?请看下回分解!

七.🦁 ENDing

错过的题目,自己要学会成长,下次再也不错啦!


🦁 其它优质专栏推荐 🦁

🌟《Java核心系列(修炼内功,无上心法)》: 主要是JDK源码的核心讲解,几乎每篇文章都过万字,让你详细掌握每一个知识点!
🌟 《springBoot 源码剥析核心系列》一些场景的Springboot源码剥析以及常用Springboot相关知识点解读

欢迎加入狮子的社区 :『Lion-编程进阶之路』,日常收录优质好文

更多文章可持续关注上方🦁的博客,2023咱们顶峰相见!

相关推荐
Cosmoshhhyyy19 分钟前
LeetCode:2270. 分割数组的方案数(遍历 Java)
java·算法·leetcode
zhulangfly20 分钟前
【Java设计模式-4】策略模式,消灭if/else迷宫的利器
java·设计模式·策略模式
夕阳之后的黑夜1 小时前
SpringBoot + 九天大模型(文生图接口)
java·spring boot·后端·ai作画
造梦师阿鹏1 小时前
Spring Web 嵌套对象校验失效
spring boot·spring valid·spring校验
芝士就是力量啊 ೄ೨1 小时前
Kotlin 循环语句详解
android·java·开发语言·kotlin
QQ27437851091 小时前
django基于Python对西安市旅游景点的分析与研究
java·后端·python·django
会code的厨子1 小时前
Spring底层核心原理解析
java·spring
苹果酱05672 小时前
Redis之数据结构
java·spring boot·毕业设计·layui·课程设计
造梦师阿鹏2 小时前
【SpringBoot】用一个常见错误说一下@RequestParam属性
java·spring boot·后端·spring
袁庭新2 小时前
IntelliJ IDEA中Maven项目的配置、创建与导入全攻略
java·intellij-idea·袁庭新·maven工具·idea如何配置maven·maven如何使用