01-Spring解决循环依赖的方法_spring cloud 循环依赖-程序员宅基地

技术标签: spring  Springboot+Springcloud  

所谓Spring的循环依赖,指的是这样一种场景:

当我们注入一个对象A时,需要注入对象A中标记了某些注解的属性,这些属性也就是对象A的依赖,把对象A中的依赖都初始化完成,对象A才算是创建成功。那么,如果对象A中有个属性是对象B,而且对象B中有个属性是对象A,那么对象A和对象B就算是循环依赖,如果不加处理,就会出现:创建对象A-->处理A的依赖B-->创建对象B-->处理B的对象A-->创建对象A-->处理A的依赖B-->创建对象B......这样无限的循环下去。

这事显然不靠谱。

Spring处理循环依赖的基本思路是这样的:

虽说要初始化一个Bean,必须要注入Bean里的依赖,才算初始化成功,但并不要求此时依赖的依赖也都注入成功,只要依赖对象的构造方法执行完了,这个依赖对象就算存在了,注入就算成功了,至于依赖的依赖,以后再初始化也来得及(参考Java的内存模型)。

因此,我们初始化一个Bean时,先调用Bean的构造方法,这个对象就在内存中存在了(对象里面的依赖还没有被注入),然后把这个对象保存下来,当循环依赖产生时,直接拿到之前保存的对象,于是循环依赖就被终止了,依赖注入也就顺利完成了。

举个例子:

假设对象A中有属性是对象B,对象B中也有属性是对象A,即A和B循环依赖。

创建对象A,调用A的构造,并把A保存下来。
然后准备注入对象A中的依赖,发现对象A依赖对象B,那么开始创建对象B。
调用B的构造,并把B保存下来。
然后准备注入B的构造,发现B依赖对象A,对象A之前已经创建了,直接获取A并把A注入B(注意此时的对象A还没有完全注入成功,对象A中的对象B还没有注入),于是B创建成功。
把创建成功的B注入A,于是A也创建成功了。
于是循环依赖就被解决了。

下面从Spring源码的角度看一下,具体是个什么逻辑。

在注入一个对象的过程中,调用了这样一个方法:

Object sharedInstance = this.getSingleton(beanName);
这段代码在AbstractBeanFactory类的doGetBean()方法中。

这里得到的Object就是试图是要创建的对象,beanName就是要创建的对象的类名,这里getSingleton()方法的代码如下:

@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}
这个方法是Spring解决循环依赖的关键方法,在这个方法中,使用了三层列表来查询的方式,这三层列表分别是:

singletonObjects

earlySingletonObjects

singletonFactories

这个方法中用到的几个判断逻辑,体现了Spring解决循环依赖的思路,不过实际上对象被放入这三层的顺序是和方法查询的循序相反的,也就是说,在循环依赖出现时,对象往往会先进入singletonFactories,然后earlySingletonObjects,然后singletonObjects。

下面看一下这个方法的代码逻辑:

1,

Object singletonObject = this.singletonObjects.get(beanName);
方法首先从singletonObjects中获取对象,当Spring准备新建一个对象时,singletonObjects列表中是没有这个对象的,然后进入下一步。

2,

if (singletonObject == null && isSingletonCurrentlyInCreation(beanName))
除了判断null之外,有一个isSingletonCurrentlyInCreation的判断,实际上当Spring初始化了一个依赖注入的对象,但还没注入对象属性的时候,Spring会把这个bean加入singletonsCurrentlyInCreation这个set中,也就是把这个对象标记为正在创建的状态,这样,如果Spring发现要创建的bean在singletonObjects中没有,但在singletonsCurrentlyInCreation中有,基本上就可以认定为循环依赖了(在创建bean的过程中发现又要创建这个bean,说明bean的某个依赖又依赖了这个bean,即循环依赖)。

举个例子:对象A和对象B循环依赖,那么初始化对象A之后(执行了构造方法),要把A放入singletonsCurrentlyInCreation,对象A依赖了对象B,那么就要再初始化对象B,如果这个对象B又依赖了对象A,也就是形成了循环依赖,那么当我们注入对象B中的属性A时,进入这个代码逻辑,就会发现,我们要注入的对象A已经在singletonsCurrentlyInCreation中了,后面的逻辑就该处理这种循环依赖了。

3,

singletonObject = this.earlySingletonObjects.get(beanName);
这里引入了earlySingletonObjects列表,这是个为了循环依赖而存在的列表,从名字就可以看到,是个预创建的对象列表,刚刚创建的对象在这个列表里一般也没有。

4,

if (singletonObject == null && allowEarlyReference) {
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
earlySingletonObjects也没有则从singletonFactories中获取,前面说到singletonFactories是对象保存的第一步,实际上对象初始化后,可能还没有注入对象的依赖,就把对象放入了这个列表。

如果是循环依赖,此时的singletonFactories中一般是会存在目标对象的,举个例子:对象A和对象B循环依赖,那么初始化了对象A(执行了构造方法),还没有注入对象A的依赖时,就会把A放入singletonFactories,然后开始注入A的依赖,发现A依赖B,那么需要构对象B,构造过程也是执行了B的构造后就把B放到singletonFactories,然后开始注入B的依赖,发现B依赖A,在第二步中提到,此时A已经在singletonsCurrentlyInCreation列表里了,所以会进入此段代码逻辑,而且此时时对象A在singletonFactories中确实存在,因为这已经是第二次试图创建对象A了。

5,

if (singletonFactory != null) {
    singletonObject = singletonFactory.getObject();
    this.earlySingletonObjects.put(beanName, singletonObject);
    this.singletonFactories.remove(beanName);
}
代码到这里基本已经确定我们要创建的这个对象已经发生循环依赖了,然后Spring进行了这样的操作,把这个对象加入到earlySingletonObjects中,然后把该对象从singletonFactories中删掉。

6,其实上面5步已经执行完了该方法的代码,这里加的第6步是为了解释循环依赖的结果。在这个方法的代码之后,会把bean完整的进行初始化和依赖的注入,在完成了bean的初始化后,后面代码逻辑中会调用一个这样的方法:

getSingleton(String beanName, ObjectFactory<?> singletonFactory)
这个方法中有个小小的子方法addSingleton(),他的代码是这样的:

protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        this.singletonObjects.put(beanName, singletonObject);
        this.singletonFactories.remove(beanName);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
    }
}
这个方法处理的是已经注入完依赖的bean,把bean放入singletonObjects中,并把bean从earlySingletonObjects和singletonFactories中删除,这个方法和上面分析的方法组成了Spring处理循环依赖的逻辑。

综上,Spring处理循环依赖的流程大概就是以下这样,假设对象A和对象B循环依赖:

步骤    操作    三层列表中的内容
1    开始初始化对象A    
singletonFactories:

earlySingletonObjects:

singletonObjects:

2    调用A的构造,把A放入singletonFactories    
singletonFactories:A

earlySingletonObjects:

singletonObjects:

3    开始注入A的依赖,发现A依赖对象B    
singletonFactories:A

earlySingletonObjects:

singletonObjects:

4    开始初始化对象B    
singletonFactories:A,B

earlySingletonObjects:

singletonObjects:

5    调用B的构造,把B放入singletonFactories    
singletonFactories:A,B

earlySingletonObjects:

singletonObjects:

6    开始注入B的依赖,发现B依赖对象A    
singletonFactories:A,B

earlySingletonObjects:

singletonObjects:

7    
开始初始化对象A,发现A在singletonFactories里有,则直接获取A,

把A放入earlySingletonObjects,把A从singletonFactories删除

singletonFactories:B

earlySingletonObjects:A

singletonObjects:

8    对象B的依赖注入完成    
singletonFactories:B

earlySingletonObjects:A

singletonObjects:

9    
对象B创建完成,把B放入singletonObjects,

把B从earlySingletonObjects和singletonFactories中删除

singletonFactories:

earlySingletonObjects:A

singletonObjects:B

10    对象B注入给A,继续注入A的其他依赖,直到A注入完成    
singletonFactories:

earlySingletonObjects:A

singletonObjects:B

11    
对象A创建完成,把A放入singletonObjects,

把A从earlySingletonObjects和singletonFactories中删除

singletonFactories:

earlySingletonObjects: 

singletonObjects:A,B

12    循环依赖处理结束,A和B都初始化和注入完成    
singletonFactories:

earlySingletonObjects:

singletonObjects:A,B

以上,希望我说清楚了。

另外,源码分析部分截取自这篇文章:

https://blog.csdn.net/lkforce/article/details/95456154

这篇文章是SpingBoot启动流程的源码分析。
————————————————
版权声明:本文为CSDN博主「lkforce」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lkforce/article/details/97183065

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/q287573145/article/details/121581179

智能推荐

基于ECS和NAS搭建个人网盘!创建属于你的私人空间!_在线建立私人空间的方法-程序员宅基地

文章浏览阅读791次。基于ECS和NAS搭建个人网盘体验地址:https://developer.aliyun.com/adc/scenario/bd0643a87c3e4cde8b488a56850df181场景体验目标本场景将提供一台配置了CentOS 7.7的ECS实例(云服务器)和创建好的NAS文件存储系统。通过本教程的操作,您可以基于已有的环境快速搭建一个个人网盘。体验此场景后,可以掌握的知识有:NAS文件存储系统基本知识Apache + PHP语言环境的安装和部署挂载NAS文件存储系统_在线建立私人空间的方法

python网课_网络课程python-程序员宅基地

文章浏览阅读5.2k次,点赞2次,收藏20次。在家是不是看网课看的比较烦躁?我就是,在家看学习通课程看的烦躁,想着用代码刷课程,写了一天终于写出来了(狗头开心)。这里对于selenium库就不详细解释,包括chrome浏览器的配置等问题应该能百度到。不多说直接上代码。等下再粗略解释一下每部分。每一部分要详细解释,篇幅太长了,现在是深夜,熬不住了。from selenium import webdriverimport timefrom..._网络课程python

教会你怎么安装和使用 Visio 哦 ~ ~-程序员宅基地

文章浏览阅读3.8k次,点赞7次,收藏42次。前言很多学生会在编程、设计或其他时候可能使用到【制图工具】。最好用的莫不过【微软的Visio】了。但是很多学生不会使用Visio,甚至不会安装Visio。 > Visio 这么好的工具,一定要会用。 今天,作者写一份教程,教会大家了解如何安装和使用 ==Visio==。 在正式教程之前,我们先介绍 Visio。 Visio 是offic..._visio安装教程

Nginx ./configure详解_nginx中./configure-程序员宅基地

文章浏览阅读601次。转载至https://blog.csdn.net/zhangman0702/article/details/93628954在"./configure"配置中,"–with"表示启用模块,也就是说这些模块在编译时不会自动构建"–without"表示禁用模块,也就是说这些模块在编译时会自动构建,若你想Nginx轻量级运行,可以去除一些不必要的模块。[root@localhost nginx-1.14.0]# ./configure --help => 查看安装配置项--hel_nginx中./configure

DNS正向解析、反向解析、双向解析基本配置_dns可以双向查询吗-程序员宅基地

文章浏览阅读1.8k次,点赞3次,收藏10次。概述DNS服务器里面有两个区域,即“正向查找区域”和“反向查找区域”,正向查找区域就是通常所访问的域名去解析地址,反向查找区域即是IP反向解析,它可以作用是通过查找IP地址的PTR记录来得到该IP地址指向的域名。要成功得到域名就必须有该IP地址的PTR记录。PTR记录是邮件交换记录的一种,邮件交换记录中有A记录和PTR记录,A记录解析域名到地址,PTR记录解析地址得到对应的域名。DNS正向解析..._dns可以双向查询吗

MATLAB Runtime 安装方法_could not find version 9.13 of the matlab runtimea-程序员宅基地

文章浏览阅读2.1w次,点赞6次,收藏70次。1. 问题描述在使用由Matlab GUI开发的工具软件时,常常会遇到如下问题:Could not find version 9.1 of the MATLAB Runtime. Attempting …2. 解决方案在官网下载对应版本的编译器:(示例为Runtime 9.1)将编译器安装到如图所示的路径下:. 重新启动软件即可..._could not find version 9.13 of the matlab runtimeattempting to load mclmcrrt

随便推点

【漏洞复现】Apache Flink 文件读取(CVE-2020-17519)_apache flink 目录遍历漏洞 cve-2020-17519-程序员宅基地

文章浏览阅读307次。0x01 漏洞描述ApacheFlink是一个开源的流处理框架,具有强大的流处理和批处理功能。Apache Flink 1.11.0(以及1.11.1和1.11.2中发布的)中引入的更改允许攻击者通过JobManager进程的REST接口读取JobManager本地文件系统中的任何文件。0x02 影响版本Flink 1.11.0、1.11.1、1.11.20x03 漏洞复现简单验证:读取该文件可直接判断是否存在漏洞http://192.168.1.1:8081/jobmanager/logs/_apache flink 目录遍历漏洞 cve-2020-17519

CSDN使用时遇到的问题_add to the trusted site list ie浏览器-程序员宅基地

文章浏览阅读97次。第一发布文章时,遇到的问题,发布时弹出 ‘请勿使用默认栏’,改下面这框里文字为标题就OK了_add to the trusted site list ie浏览器

差分进化算法优化(MATLAB)_dfo优化-程序员宅基地

文章浏览阅读156次。在初始种群中,所有蜜蜂都是侦查蜜蜂,它们从规定的搜索空间中选择位置。聚集蜜蜂在新位置周围的领域内搜索,侦查蜜蜂则返回搜索空间的一个新位置。其中一种基于差分进化的算法是差分蜂群优化算法(DFOA),它模拟蜜蜂群饲料搜索过程,使用差分策略来更新蜜蜂位置,以实现全局最优解搜索。在差分进化算法中,初始种群由 n 个个体组成,每个个体包含 D 个维度,可以表示为。因此,在搜索空间 [L,U] = [-2,-2] 至 [2,2] 内,Rosenbrock 函数的全局最小值为 4.2776e-06。U = [2,2];_dfo优化

Linux下printf输出彩色文字_linux打印彩色关键字-程序员宅基地

文章浏览阅读819次。Linux下printf输出彩色文字语法举例多行输出?应用规范最近学习了printf输出彩色字体的方法,来给大伙分享一下语法\033[ A1;A2;A3;…An m这里的An详情请看应用规范中的表格XD举例printf("\033[1;33;41mhello world\n");多行输出?你一定觉得这样就可以了,但是这其实是不对的,因为当你在下面再多输出一行你就会发现问题printf("\033[1;33;41mhello world\n");printf("hello worl_linux打印彩色关键字

Java春招面试复习:if-else代码优化的八种方案_map<?, function<?> action> actionmappings = new ha-程序员宅基地

文章浏览阅读279次。前言代码中如果if-else比较多,阅读起来比较困难,维护起来也比较困难,很容易出bug,接下来,本文将介绍优化if-else代码的八种方案。优化方案一:提前return,去除不必要的else如果if-else代码块包含return语句,可以考虑通过提前return,把多余else干掉,使代码更加优雅。优化前:if(condition){ //doSomething}else{ return ;}优化后:if(!condition){ return ;}//_map action> actionmappings = new hashmap<>()

SMI(MDC/MDIO)总线接口介绍_serial management interface-程序员宅基地

文章浏览阅读4.7k次,点赞2次,收藏16次。1. MDIO接口SMI:串行管理接口(Serial Management Interface),也被称作MII管理接口(MII Management Interface),包括MDC和MDIO两条信号线。MDIO是一个PHY的管理接口,用来读/写PHY的寄存器,以控制PHY的行为或获取PHY的状态,MDC为MDIO提供时钟。MDIO原本是为MII总线接口定义的,MII用于连接MAC和PH..._serial management interface

推荐文章

热门文章

相关标签