深入了解Spring循环依赖本质

news2024/12/28 21:01:44

说明:

  1. 本文基于Spring-Framework 5.1.x版本讲解

  2. 建议读者对创建对象部分源码有一定了解

概述

这篇讲讲Spring循环依赖的问题,网上讲循环依赖的帖子太多太多了,相信很多人也多多少少了解一点,那我还是把这个问题自己梳理一遍,主要是基于以下出发点:

1. Spring到底如何解决的循环依赖问题,有没有’黑科技‘;

2. 有时项目会因为循环依赖问题导致启动失败,由于不了解其机制,排查耗费时间

3. 网上众说风云,没有形成自己的思考

还有其他文章说Spring使用三级缓存是为了解决循环依赖问题,为了解决代理下的循环依赖问题? 那废话不多说,直接开始吧

循环依赖简介

循环依赖的含义: BeanA依赖BeanB,BeanB又依赖BeanA , 如下图

这就是循环依赖, 我们来分析下会有什么问题?

1. 实例化BeanA

2. BeanA在属性注入阶段从容器里面找BeanB

3. 如果BeanB已经在容器里面创建好,那万事大吉,没循环依赖的问题

4. 如果BeanB还没有在容器里面创建好,这时候Spring会创建BeanB

5. 创建BeanB的时候又发现依赖BeanA,但此时BeanA也没有创建完,在没有开启循环依赖开关的情况下就会报错:Requested bean is currently in creation: Is there an unresolvable circular reference?

从软件代码分层结构来讲,如果分层合理,这种情况一般可以避免掉,但是避免不了同一个层次中的Bean互相引用,好那既然循环依赖肯定会出现,我们自己先来思考下,如果是我们自己写一个IOC容器,这个问题如何解决?

如何解决循环依赖?

从上面的4步可以看出来,问题所在就是BeanB在创建过程中,无法找到正在创建中的BeanA,那我们是不是可以找一个地方把正在创建的BeanA(为了行文方便,把Bean正在创建中的状态称为半状态)先给保存起来,等BeanB用到的时候赋值给它不就行了,这时候步骤如下:

1. 实例化BeanA

2. BeanA在属性注入之前先把自己放到一个Map里面(此时BeanA为半状态)

3. BeanA在属性注入阶段从容器里面找BeanB

4. 如果BeanB还没有在容器里面创建好,这时候Spring会创建BeanB

5. 创建BeanB的时候又发现依赖BeanA,由于BeanA已经在Map里面了(虽然是半状态,但不影响最终使用,反正现在又不暴露给用户使用) ,所以注入成功,BeanB创建完成

6. 由于BeanB已创建完成,意味着BeanA注入BeanB成功,此时从Map中移除BeanA的半状态

7. 容器初始化完成

到这里有什么大的问题没有? 其实是没有的,Bean确实会创建成功 , 容器可以正常启动完成。 那我们在来看下启用动态代理情况下,使用一个Map(一级缓存)会不会有问题? 为了说明简单,我们只生成BeanA的代理对象BeanA_Proxy,BeanB无需创建:

1. 实例化BeanA,并生成BeanA的代理对象BeanA_Proxy ,此时上下文中有BeanA、BeanA_Proxy两个对象

2. BeanA在属性注入之前先把代理对象BeanA_Proxy放到Map里面

3. BeanA在属性注入阶段从容器里面找BeanB

4. 如果BeanB还没有在容器里面创建好,这时候Spring会创建BeanB

5. 创建BeanB的时候又发现依赖BeanA,由于BeanA的代理对象BeanA_Proxy已经在Map里面了,所以把BeanA_Proxy注入BeanB,此时BeanB创建完成

6. 由于BeanB已创建完成,意味着BeanA注入BeanB成功,此时从Map中移除BeanA_Proxy

7. 容器把BeanA_Proxy暴露给用户使用,并初始化完成

从上面可以看出,即使使用代理的情况下,使用一个Map(一级缓存)来解决循环依赖问题也是可以的。

为什么是三级缓存?

从上面可以看出,把半状态的Bean或者代理对象无脑的放入一级缓存之后,确实可以解决循环依赖的问题,那为什么Spring要使用三级缓存来解决这个问题呢? 让我们来回忆下Bean的整个生命周期: 1. 实例化Bean 2. 属性注入 3. 初始化Bean 。 这是创建一个Bean必经的几个步骤,而我们上面是为了解决循环依赖问题而强行加了一个往一级缓存里面放对象的步骤,而对于不存在循环依赖的对象,这一步无疑是多余的;还有另外一个问题: 把半状态的Bean和创建完成的Bean对象放入同一个缓存里面也不太好管理,违背单一职能原则

所以这里我们要解决2个问题:

1. 解决循环依赖的代码(也就说生成半状态对象的过程)不能放到创建Bean的主流程中;

2. 半状态的对象需要与创建完成的对象所在的容器隔离开。

为了解决问题一我们可以考虑在检测到有循环依赖的时候才把半状态对象暴露给BeanB使用,就是说说BeanB发现BeanA正在创建的时候,在把半状态BeanA赋值给BeanB。 但是问题又来了:这个半对象BeanA哪来的? 谁创建出来的? 这无疑变成了蛋和鸡的问题, 所以我们还是要在刚开始创建BeanA的时候以最小的代价(这里指执行时间最短) 把半对象给保存起来,BeanB检测到循环依赖的时候,在把BeanA给取出来。那Spring其实就是使用另一个Map保存匿名函数的方式来解决这个问题的, 因为你要考虑直接暴露BeanA的代价(比如说暴露给BeanB是一个代理对象,那要考虑创建BeanA代理对象的执行成本)。 这里我们简单贴下Spring的源码:

/** * 实际生成Bean的核心方法 */protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException { 
// 省略上下文中其他不重要的代码 
// 这里注册一个匿名函数,把匿名函数放到一个临时缓存里面(其实就是所谓的三级缓存) addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));} 
/** * 作用: 获取提前要暴露给其他Bean的引用 * 这个方法只有在Spring检测到有循环引用的情况下才会调进来 */protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; 
// 获取要提前暴露的对象,这里有可能产生代理对象以及其他非入参Bean的对象 exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); } } return exposedObject;}

好,看到这里其实我们已经顺便把上述第二个问题也一起解决掉了,因为保存匿名函数的Map和最终保存对象实例的Map不是同一个,而且保存匿名函数的Map在解决完循环依赖问题之后会清空掉,这样我们就可以以延迟加载的方式解决掉循环依赖的问题。 那在BeanA会产生代理的情况下会不会有问题??? 请你自己思考下

但这里还有一个问题,考虑下面一种场景:

​BeanA与BeanB、BeanC的关系是互相引用, 套用我们上面的理论, 在创建BeanA的主流程中我们只是插入了一个获取半状态对象的匿名函数,而不是要暴露给外部的最终对象,当BeanC也需要注入BeanA的时候,还是要执行一次匿名函数来获取最终暴露的对象,这里有两个问题:

1. 匿名函数重复执行,其实是没有必要的。

2. 更为严重的是,有可能每次调用函数返回的是不同的对象,这样会导致注入给BeanB和BeanC的对象不一致,这就是大问题了。 (那有人会问如果保证获取最终暴露对象的接口SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference中返回同一个对象不就行了? 站在解决问题的角度确实是这样的,但是站在容器的角度来讲就不一样了,接口只是一个拓展点,他的执行逻辑是不可控的,所以还是要在容器级别来解决这个问题)

为了解决以上两个问题,Spring采用了第三个缓存,把已经暴露出去的对象给缓存起来,这样问题就完美解决以上两个问题。

getEarlyBeanReference

在上面我们已经解释了为什么要用三级缓存来解决循环依赖的问题,我们在简单说下SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference接口,其实这个接口没有什么高深的地方,只是为了获取循环依赖下需要提前暴露给其他Bean的对象,这里要注意一下:仅仅在检测到有循环依赖的情况下才会调进来,我们看下接口定义

/** * Extension of the {@link InstantiationAwareBeanPostProcessor} interface, * adding a callback for predicting the eventual type of a processed bean. * * <p><b>NOTE:</b> This interface is a special purpose interface, mainly for * internal use within the framework. In general, application-provided * post-processors should simply implement the plain {@link BeanPostProcessor} * interface or derive from the {@link InstantiationAwareBeanPostProcessorAdapter} * class. New methods might be added to this interface even in point releases. * * @author Juergen Hoeller * @since 2.0.3 * @see InstantiationAwareBeanPostProcessorAdapter */public interface SmartInstantiationAwareBeanPostProcessor extends InstantiationAwareBeanPostProcessor { default Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { return bean; }}

从类注释中”This interface is a special purpose interface, mainly for internal use within the framework“可以看到,这个接口仅仅是Spring内部为了解决某些问题提供的接口,并不希望暴露给上层用户使用,所以我们在实际工作中一般用不到的

但是我们要特别注意一个坑: 我们知道在Bean后置处理接口BeanPostProcessor中也有可能返回代理对象,如果这里返回的对象和循环依赖暴露出去的对象不一致的话就会报以下错误:

Error creating bean with name 'serviceA': Bean with name 'serviceA' has been injected into other beans [serviceB] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'serviceA': Bean with name 'serviceA' has been injected into other beans [serviceB] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example. at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:622) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:514) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:321) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:319) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:866) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:878) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550) at test.circle_ref.CircleRefTests.run(CircleRefTests.java:13)

从逻辑上来讲报错也是合理的,因为既然提前暴露出去的Bean,像是一种承诺,后续就不能修改了,修改意味着Spring容器中Bean的’状态‘是不一致的。 所以在有循环依赖的情况下,一定要保证getEarlyBeanReference和BeanPostProcessor 返回的Bean是同一个。

小结

本篇文章介绍了Spring为什么用三级缓存才解决循环依赖问题,并且介绍了SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference接口的含义以及在实际情况中可能遇到的坑,那我们总结下Spring的三级缓存的作用:

一级缓存: 保存已经创建完的Bean对象,我们常用的BeanFactory#getBean方法就是从这里获取;

二级缓存: 缓存在循环依赖中暴露给其他Bean的半状态对象,防止注入对象不一致的问题;

三级缓存: 缓存获取提前暴露的Bean的匿名函数 ,为的是以最小的代价减少对Spring创建对象主干流程的影响

关于循环依赖的问题就介绍到这,不得不说Spring考虑问题真是的非常全面,设计相当合理。如有疑问,欢迎交流

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

kubernetes,service详解下

kubernetes&#xff0c;service详解下 HeadLiness类型的Service 在某些场景中&#xff0c;开发人员可能不想使用Service提供的负载均衡功能&#xff0c;而希望自己来控制负载均衡策略&#xff0c;针对这种情况&#xff0c;kubernetes提供了HeadLiness Service&#xff0c;这类…

内存分段与内存分页:逻辑地址、物理地址、线性地址、虚拟地址

这篇文章也是我自己的博客网站的里的文章&#xff0c;我觉得这篇文章还是我觉得知识含量比较高的文章&#xff0c;所以特地把它发出来看看。 这篇文章写于我在写自己的操作系统JackOS的时候系统梳理了一下CPU访问内存的各种方式&#xff0c;写完这篇文章之后&#xff0c;我对C…

Kafka高级特性解析之生产者

1、消息发送 1.1、数据生产流程解析 Producer创建时&#xff0c;会创建一个Sender线程并设置为守护线程。生产消息时&#xff0c;内部其实是异步流程&#xff1b;生产的消息先经过拦截器->序列化器->分区器&#xff0c;然后将消息缓存在缓冲区&#xff08;该缓冲区也是在…

Docker桌面版安装与使用(windows)

目录一、Docker概念二、下载安装三、docker镜像安装与操作四、制作自己的python镜像容器五、目录挂载六、多容器通信七、Docker-Compose管理多个容器运行八、发布和部署九、备份数据迁移一、Docker概念 1、Docker 是一个应用打包、分发、部署的工具2、镜像Image、容器Containe…

Windows OpenGL 图像绿幕抠图

目录 一.OpenGL 图像绿幕抠图 1.原始图片2.效果演示 二.OpenGL 图像绿幕抠图源码下载三.猜你喜欢 零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 >> OpenGL ES 基础 零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 >> OpenGL ES 特效 零基础 Open…

[问题解决方案](多人共同合并场景)git已merge到master分支代码且被同事代码覆盖如何回退

git已merge到master分支代码如何回退&#xff08;多人共同合并&#xff09;场景已经被同事代码覆盖的解决方案&#xff08;无需强制合并权限&#xff09;代码revert后又需要重新启用怎么办如果是未受保护分支代码的回退且只有你一人合并的代码 可以直接使用下面的命令即可如果只…

【Unity3D日常开发】Unity3D中实现不规则Button按钮的精准响应

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客QQ群&#xff1a;1040082875 大家好&#xff0c;我是佛系工程师☆恬静的小魔龙☆&#xff0c;不定时更新Unity开发技巧&#xff0c;觉得有用记得一键三连哦。 一、前言 在使用Unity3D开发中&#xff0c;可…

全志V853平台Camera模块开发框架详解

Camera 本章节介绍V853平台 Camera 模块的开发。 V853支持并口CSI、MIPI&#xff0c;使用VIN camera驱动框架。 Camera通路框架 VIN支持灵活配置单/双路输入双ISP多通路输出的规格 引入media框架实现pipeline管理 将libisp移植到用户空间解决GPL问题 将统计buffer独立为v…

Web大学生网页作业成品——抗击疫情网站设计与实现(HTML+CSS)

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

Vue3 样式绑定

Vue3 样式绑定1.Vue.js class2.class 属性绑定3.Vue.js style(内联样式)4.组件上使用 class 属性1.Vue.js class class 与 style 是 HTML 元素的属性&#xff0c;用于设置元素的样式&#xff0c;我们可以用 v-bind 来设置样式属性。 v-bind 在处理 class 和 style 时&#xf…

微信小程序反编译

本文转载于&#xff1a;https://www.cnblogs.com/one-seven/p/15524457.html 微信小程序反编译 微信文件保存位置\WeChat Files\Applet\小程序id_APP_.wxapkg 现在小程序是处于编码状态 github上下载一个python版的解密工具 https://github.com/superdashu/pc_wxapkg_decr…

【免杀前置课——Windows编程】十四、异步IO——什么是异步IO、API定位问题、APC调用队列

异步IO异步IO异步I/0注意事项:定位问题总解决方案APC调用队列异步IO 当我们读取一个文件时&#xff0c;一般情况下&#xff0c;线程是阻塞的&#xff0c;也就是说&#xff0c;当前线程在等待文件读取操作结束,这种方式叫同步IO。 Windows 在系统底层为用户实现了另外一种高效的…

【软考】-- 操作系统(下)

操作系统&#xff08;下&#xff09;第五节 文件管理&#x1f355;一、文件管理的基本概念1️⃣文件2️⃣文件目录3️⃣目录结构:&#x1f354;二、文件路径&#x1f35f;三、文件命名规则&#x1f32d;四、文件的基本操作&#x1f37f;五、文件类型与扩展名&#x1f9c2;六、系…

Docker中安装Kibana

Kibana是一个免费且开放的用户界面,能够让你对Elasticsearch 数据进行可视化,并让你在Elastic Stack中进行导航。你可以进行各种操作,从跟踪查询负载,到理解请求如何流经你的整个应用,都能轻松完成。 在Docker Hub中选择最新版本的Kibaba镜像(选择版本为8.5.1),如下图…

bigquant选股模型主要有哪些?

bigquant选股模型一般常见的有七种&#xff0c;即多因子模型、风格轮动模型、行业轮动模型、资金流模型、动量反转模型、一致预期模型、趋势追踪模型等方面。不过要想样样都学会精通也是需要花费时间&#xff0c;以及精力等&#xff0c;那么&#xff0c;小编就从最基本的多因子…

DPDK Mempool

mempool是DPDK提供的内存池&#xff0c;其用处有&#xff1a; 由于DPDK使用UIO让DMA将网卡中的数据直接拷贝至用户态&#xff0c;因此需要一块固定的区域提供给DMA重复利用内存&#xff0c;提高效率 结构 mempool的主要结构如下图所示。 mempool为每个注册的lcore都分配了一…

Node.js - nvm管理node.js版本

使用nvm来管理node.js的版本真的很方便&#xff0c;这样就可以根据自己的需要来回切换node.js版本&#xff01; 一、卸载本地安装的node.js版本 略 二、安装nvm管理工具 2.1、下载 https://github.com/coreybutler/nvm-windows/releases 2.2 安装 (1) 鼠标双击nvm-setup.exe文件…

【从零开始学习深度学习】7.自己动手实现softmax回归的训练与预测

基于上一篇文章读取fashion-minist数据集的基础&#xff0c;本文自己动手实现一个softmax模型对其进行训练与预测。 目录1. 自己动手实现softmax回归1.1 读取数据1.2 初始化模型参数1.3 实现softmax运算1.4 定义模型1.5 定义损失函数1.6 计算分类准确率1.7 训练模型1.8 预测完整…

面试碰壁15次!作为一个已经27岁的测试工程师,未来在何方....

3年测试经验原来什么都不是&#xff0c;只是给你的简历上画了一笔&#xff0c;一直觉得经验多&#xff0c;无论在哪都能找到满意的工作&#xff0c;但是现实却是给我打了一个大巴掌&#xff01;事后也不会给糖的那种... 先说一下自己的个人情况&#xff0c;普通二本计算机专业…

LabVIEW编程LabVIEW开发SMP10辐射表例程与相关资料

LabVIEW编程LabVIEW开发SMP10辐射表例程与相关资料 ​​SMP10辐射表是荷兰Kipp&Zonen公司的一种用于测量短波辐射的产品&#xff0c;配有只能型接口&#xff0c;能够提供标准输出&#xff0c;能耗低。 作为一款副基准总辐射表,SMP10结合了CMP 11的传感器技术、SMP 11的智…