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

目录

  • 一.🦁 开始前的废话
  • 二. 🦁 什么是循环依赖?
  • 三. 🦁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咱们顶峰相见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/478488.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

微前端架构

介绍 微前端的概念是由ThoughtWorks在2016年提出的&#xff0c;它借鉴了微服务的架构理念&#xff0c;核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用&#xff0c;每个应用都可以独立开发、独立运行、独立部署&#xff0c;再将这些小型应用融合为一个完整的应用&am…

当内外网的域名相同时,如何在外网解析同域名的网址

当内部网络和外部网络存在相同的域名&#xff0c;并且希望内部用户通过内部DNS服务器解析到外部网络上的该域名对应的公网IP地址时&#xff0c;需要在内部DNS服务器上采取一些特殊配置策略来实现这一目标。以下是一种通用的解决方案&#xff1a; 条件转发&#xff08;Condition…

鸿蒙一次开发,多端部署(四)工程管理

DevEco Studio的基本使用&#xff0c;请参考DevEco Studio使用指南。本章主要介绍如何使用DevEco Studio进行多设备应用开发。 说明&#xff1a; 本章的内容基于DevEco Studio 3.1.1 Release版本进行介绍&#xff0c;如您使用DevEco Studio其它版本&#xff0c;可能存在文档与产…

COMPOSITE SLICE TRANSFORMER

Composite Slice Attention (CSA) 辅助信息 作者未提供代码

第十四届蓝桥杯JavaB组省赛真题 - 蜗牛

dp[i][0] 状态转移方程&#xff1a; 1. 从上一个竹竿的底部转移过来&#xff0c;即&#xff1a; dp[i][0]dp[i−1][0]x[i]−x[i−1]; 2. 从上一个竹竿的传送门转移过来&#xff0c;即&#xff1a; dp[i][0]dp[i−1][1]b[i]/1.3; dp[i][1] 状态转移方程&#xff1a; 1. 从上一…

Java集合框架-读书笔记

Java集合框架 数据结构是以某种形式将数据组织在一起的集合。数据结构不仅可以存储数据 并且可以对数据进行访问和处理操作。 eg&#xff1a;Arraylist是将数据保存在线性表的数据结构 其实java中还提供了一些第一数据进行操作和存储的数据结构 这些数据结构被称为 java集合…

如何使用phpStudy在Windows系统部署静态站点并实现无公网IP远程访问

文章目录 使用工具1. 本地搭建web网站1.1 下载phpstudy后解压并安装1.2 打开默认站点&#xff0c;测试1.3 下载静态演示站点1.4 打开站点根目录1.5 复制演示站点到站网根目录1.6 在浏览器中&#xff0c;查看演示效果。 2. 将本地web网站发布到公网2.1 安装cpolar内网穿透2.2 映…

并发编程之interrupt方法的详细解析

3.9 interrupt方法详解 Interrupt说明 interrupt的本质是将线程的打断标记设为true&#xff0c;并调用线程的三个parker对象&#xff08;C实现级别&#xff09;unpark该线程。 基于以上本质&#xff0c;有如下说明&#xff1a; 打断线程不等于中断线程&#xff0c;有以下两种…

hcia静态实验

题目&#xff1a; 要求&#xff1a; 1、R6为isp&#xff0c;接口ip均为公有地址&#xff0c;该设备只能配置ip地址&#xff0c;之后不能再对其进行任何其他配置 2、r1到r5为局域网&#xff0c;私有ip地址为192.168.1.0 24&#xff0c;合理分配 3、r1,r2,r4各有两个环回地址&am…

python基础——语句

一、条件语句 就是 if else 语句 &#xff01; 代表不等于 代表等于if 关键字&#xff0c;判断语句&#xff0c;有“如果”的意思&#xff0c;后面跟上判断语句else 常和“if” 连用&#xff0c;有“否则”的意思&#xff0c;后面直接跟上冒号 …

探秘空投女巫:区块链世界的神秘现象

在区块链世界中&#xff0c;充满了各种新奇的名词和概念&#xff0c;其中一个引人注目的现象就是“空投女巫”。这个神秘的名字让人不禁好奇&#xff0c;究竟是什么&#xff1f; 什么是空投女巫&#xff1f; 空投女巫是指那些在区块链项目中频繁参与空投活动&#xff0c;并且…

[Netty实践] 简单聊天实现(四):Server集群改造

目录 一、介绍 二、解决方案 三、server端改造 五、客户端改造 四、测试 五、拓展 一、介绍 本章是拓展内容&#xff0c;主要实现的是Server集群。 当系统的用户多了之后&#xff0c;单机Server资源有限&#xff0c;无法提供socket连接时&#xff0c;我们需要部署Serve…

[Linux]多线程(在Linux中的轻量级进程(LWP),怎么使用线程(接口))

目录 一、在Linux中的轻量级进程&#xff08;LWP&#xff09; 二、多线程的接口 1.创建线程&#xff08;pthread_create&#xff09; 2.线程ID&#xff08;pthread_self&#xff09; 3.线程终止 终止某个线程而不终止整个进程的三种方法&#xff1a; return pthread_…

Python Windows系统 虚拟环境使用

目录 1、安装 2、激活 3、停止 1、安装 1&#xff09;为项目新建一个目录&#xff08;比如&#xff1a;目录命名为learning_log&#xff09; 2&#xff09;在终端中切换到这个目录 3&#xff09;执行命令&#xff1a;python -m venv ll_env&#xff0c;即可创建一个名为ll…

基于ssm的医院住院管理系统论文

摘 要 随着时代的发展&#xff0c;医疗设备愈来愈完善&#xff0c;医院也变成人们生活中必不可少的场所。如今&#xff0c;已经2021年了&#xff0c;虽然医院的数量和设备愈加完善&#xff0c;但是老龄人口也越来越多。在如此大的人口压力下&#xff0c;医院住院就变成了一个问…

YOLOv2学习

YOLOv2学习 引入 Anchor boxes摘要数据集组合方法&#xff08;Dataset Combination Method&#xff09;联合训练算法&#xff08;Joint Training Algorithm&#xff09;改进Batch NormalizationHigh Resolution Classifier分类器预训练分辨率调整**Convolutional With Anchor B…

鸿蒙Harmony应用开发—ArkTS-全局UI方法(警告弹窗)

显示警告弹窗组件&#xff0c;可设置文本内容与响应回调。 说明&#xff1a; 从API Version 7开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 本模块功能依赖UI的执行上下文&#xff0c;不可在UI上下文不明确的地方使用&#xff0c;参见UI…

云手机为电商提供五大出海优势

出海电商行业中&#xff0c;各大电商平台的账号安全是每一个电商运营者的重中之重&#xff0c;账号安全是第一生产力&#xff0c;也是店铺运营的基础。因此多平台多账号的防关联管理工具成了所有电商大卖家的必备工具。云手机最核心的优势就是账户安全体系&#xff0c;本文将对…

网络仿真3-NS2协议修改和移植

Ns2实现原理 OTcl与C关联 执行路径&#xff1a;Tcl->Otcl->C 返回路径&#xff1a;C->Otcl->Tcl NS2协议修改和移植 NS2中的各种网络协议在底层通过C实现&#xff0c;在网络仿真过程中最终通过调用底层C代码实现网络行为、算法、功能等各种仿真 NS2协议修改&…

vue3+threejs新手从零开发卡牌游戏(三):尝试在场景中绘制一张卡牌

首先我们思考下&#xff0c;一张最简单的卡牌有哪些东西构成&#xff1a;卡牌样式和卡牌数据。一张卡牌有正面和背面&#xff0c;有名称、属性、种族、攻击力等数据&#xff0c;我们先不考虑数据&#xff0c;先尝试在场景中绘制一张卡牌出来。 一、寻找卡牌素材 为了简单我直…