Spring 循环依赖处理之三级缓存设计

news/2024/4/24 13:20:38/文章来源:https://blog.csdn.net/echizao1839/article/details/130267183

一、思考

1、Spring是如何解决循环依赖问题的?

2、为什么要使用三级缓存?二级缓存能否解决问题?

3、提前暴露对象暴露的是什么?

4、主要源码

二、循环依赖

1、介绍

 如上图,创建A之前需要先创建B,创建B之前需要先创建A,造成循环依赖。

由于A没创建完成,所以B再创建的时候再容器中获取不到A对象。

如何解决这个问题呢?

我们把创建中的对象叫半成品,创建完了的叫成本。

处于半成品状态的对象能否直接使用?不能使用。如果此时并不是暴露给外部使用,而是内部程序的调用呢?

当需要暴露给外部调用的时候,如果完成了赋值操作就不会有问题了。

如果你持有了某一个对象的引用,能否在后续步骤的时候进行赋值操作?可以。

本质是半成品状态的对象可以在中间过程中使用,实例化和初始化分开执行。

 2、关键源码实现

1、创建bean

一级缓存保存BeanName 和创建bean实例之间的关系。

二级缓存保存BeanName 和创建bean实例之间的关系,与singletonFactories的不同之处在于,当一个单例bean被放到这里之后,那就可以通过getBean方法获取到,可以方便进行循环依赖的检测。

三级缓存用于保存BeanName 和创建bean的工厂之间的关系。

ObjectFactory是一个函数式接口,在java中,方法的参数在传递的时候都是值传递,也就是说必须要传入一个具体的数据值才可以,在1.8之后,可以将lambda表达式作为一个参数传递到方法中,而不必是具体的值,那么在方法定义的时候参数的类型可以是函数式接口,在具体的方法调用的时候,lambda表达式并不会立刻执行,而是在方法中调用getObject方法才会去执行。

创建bean的关键方法

第五个是创建bean实例,最后一个是给bean实例赋值。

 源码跟踪,从spring boot 入口跟进,进入方法有很多,我这里从spring boot 启动跟进去

开始获取a实例

关键方法五执行完成后只是完成了a实例化,还没进行初始化,现在属于半成品,这里面开始进行a的创建

往下走找到此方法

进去此方法完成后三级缓存保存了次bean,此时a再三级缓存中

 2、赋值

回到上面继续往下

次方法内完成属性赋值(非容器内属性,容器内的 aware 是单独的)

进入populateBean方法,看最后一行 

PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);

这里面保存b依赖的对象的kv  k是b  v是b对象【因为此时还没有创建b所以是默认RuntimeBeanReference】

 进入applyPropertyValues方法进行值处理

因为此时还没有b所以回尝试去构造b

Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);

进入方法内 上面说到没有b默认是RuntimeBeanReference

 进入方法内

这是一个大递归尝试创建b

 这里又会重复以上步骤,又会执行到关键方法五,不过此时创建的是b对象了,三级缓存中也会添加b进去,此时 

需要注意的是这一步走到applyPropertyValues方法拿到的PropertyValues pvs 中的value 仍然是RuntimeBeanReference,

执行到最后进入

新的一轮再次开始,此时拿到的name是a(b依赖a,给b赋值的是a属性),因为此时a还在三级缓存中,在二级缓存中的半成品才会暴露出来,所以这获取不到a,再次执行到关键方法二,doGetBean();

执行到关键行 

//这里会发现a已经在三级缓存中了

Object sharedInstance = getSingleton(beanName);

 分别从一二三,三级缓存中取

此时因为三级缓存中有了

所以从三级缓存中拿出来,放进二级缓存

singletonObject = singletonFactory.getObject();

这里因为传入的是内部类的实现,调用getObject();才会进行执行

此时的情况如下

此时由于已经拿到了a对象 doGetBean 就不需要调用 createBean创建对象,

继续执行到这里,就可以直接从二级缓存中获取到a对象了

此时b获取到a对象之后,就可以进行赋值了,由于a已经存在所以这里获取到的不在是RuntimeBeanReference而是a的实例,注意此时是从二级缓存中获取到的,a属于半成品。

有了a对象之后就可以直接对b进行赋值了 applyPropertyValues方法中赋值

执行完成之后b里面的a属性就有值了 ,此时b是成品如下

 

b执行完了之后,需要进行缓存再次进行缓存。因为此时b已经是成品所以直接放进一级缓存。

getSingleton

 以上创建b对象的原因是为了给a对象赋值,此时b已经变成成品了。

此时给a再次赋值的时候b已经获取到了

返回

此时resolveValue也有了,是一个b对象

 此时就可以给a赋值了

此时 a也创建完了

 a执行完了之后,需要进行缓存再次进行缓存。因为此时a已经是成品所以直接放进一级缓存。

getSingleton

 此时a和b都创建完了,但是b是被迫创建的。

再次回到这循环,上面所有的只是循环了a,此时进行b的循环,由于a循环的时候,b已经创建好了,所以本次循环很顺利的完成了。 

此时正式创建完成。

 三、思考

1、三个map结构分别存储什么对象

一级缓存:成品对象
二级缓存:半成品对象
三级缓存:lambda表达式

2、三个map结构在查找对象的时候顺序是什么样的?

1,2,3

3、如果只有一个map结构,能否解决循环依赖的问题?

理论上来说是可以的,但是实际操作的时候会有问题,一级缓存和二级缓存的区分点在于存储的对象类型不一样,当只有一个map的时候,就意味着成品对象和半成品对象放到了一起,半成品对象是不能够直接暴露给外部使用的,因为会有空指针的问题,所以如果非要用一个map存储就意味着要添加标识,来标注是成品对象还是本成品对象,如果按照这样的方式来设计代码,会很不优雅,所以可以直接用两个map解决这个问题,不需要一个。

4、如果只有两个map,可以解决循环依赖问题吗

可以,但是有前提条件:在对象创建过程中不能有代理对象

当存在代理对象的时候,抛出一下异常。

Exception in thread "main"org.springframework.beans.factory.BeanCurrentlylnCreationException:Error creating bean with name 'a': Bean with name 'a' has been injected into other beans (b] in its rawversion 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 'getBeanNamesForlype' with the'allowEagerInit' flag turned off for example.线程“main”org.springframework.beans.factory.BeanCurrentlynCreateException中出现异常:
创建名为“a”的bean时出错:名为“a”的bean已作为循环引用的一部分注入到原始版本中的其他bean(b]中,但最终已被包装。
这意味着所述其他bean不使用bean的最终版本。
这通常是过度渴望类型匹配的结果——例如,考虑在关闭“allowEagleInit”标志的情况下使用“getBeanNamesForlype”。

5、为什么必须要使用三级缓存来解决循环依赖问题?为什么三级缓存可以解决带有代理对象的循环依赖问题?

1,同一个容器中,能否出现同名的不同对象?

不能

2,如果出现了同名的不同对象,应该怎么办?刚开始创建出原始对象,后续创建出了代理对象
如果在创建过程中出现了同名的不同对象,那么后面创建的对象要覆盖前面创建的对象

在这里会被覆盖



3,那么为什么要使用lambda表达式这样的方式,为什么要加入三级缓存呢?

对象的属性赋值是在哪个方法里完成的?populateBean方法
代理对象的创建是在哪个方法里完成的?BeanPostProcessor的后置处理方法里完成的
他们两个方法谁先执行,谁后执行呢?先执行populateBean,然后执行后置处理方法

也就是说,在进行对象的属性赋值的时候,代理对象还没有创建出来,那么属性的赋值只能赋原始对象,而在后续的步骤中又创建出了代理对象,此时的代理对象会有赋值的过程吗?不会,所以会出现一个错误
this means that said other beans do not use the final version of the bean
就是说赋值是原始对象,而最终留下来的是代理对象,所以导致没有使用最终版本的bean对象

如何解决?

将代理对象的创建过程提前执行,也就是说在进行对象赋值的时候,必须要唯一性的确定出到底是原始对象还是代理对象,这个方法是在getEarlyBeanReference方法里执行的,是在populateBean方法的调用逻辑里操作的


为什么要使用lambda表达式?

lambda表达式相当于是延迟执行,因为此方法并不会在方法调用的时候立刻执行,而是在对象必须要进行赋值的那一刻执行,也就是说在对象赋值的前一刻确定出了最终版本的bean对象。 

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

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

相关文章

一个关于Mybatis和spring的公共组件starter

utils-springboot-starter 介绍使用说明 介绍 一个关于Mybatis和spring的公共组件starter,目前包含以下功能: 接口请求日志SQL执行日志数据自动加解密数据自动脱敏服务治理方面: 接口限流接口熔断降级:CPU、内存、异常数、异常率…

win11 环境下streamlit使用pycharm debug

目录 1. pycharm中配置run 脚本2. streamlit3. 开始debug调试 1. pycharm中配置run 脚本 (一)点击 Edit Configurations,按图操作. 2. streamlit 1.streamlit 安装在 anaconda 的 base 环境(随意哈,安装哪里都可以&#xff0c…

问题定位及解决方案

1.视频沉浸页快速滑动后,必现不能向下划动 复现步骤: 进入视频沉浸页,快速向下划动,滑动到第一页最后一个时,不能再向下划动。 解决步骤: 1.确定请求API: mtop.aliexpress.ugc.feed.video.lis…

PE文件反编译为python脚本流程

1、查壳 DetectltEasy、PeiD查壳 2、脱壳 常见打包工具PyInstaller,脱壳方法 (1)用pyinstxtractor.py脱壳,用”python pyinstxtractor.py 1.exe“命令,生成“.exe文件名_extracted” (2)用…

深度学习技巧应用8-各种数据类型的加载与处理,并输入神经网络进行训练

大家好,我是微学AI,今天给大家介绍一下深度学习技巧应用8-各种数据类型的加载与处理,并输入神经网络进行训练。在模型训练中,大家往往对各种的数据类型比较难下手,对于非结构化数据已经复杂的数据的要进行特殊处理&…

LeetCode:6390. 滑动子数组的美丽值

🍎道阻且长,行则将至。🍓 🌻算法,不如说它是一种思考方式🍀 算法专栏: 👉🏻123 一、🌱6390. 滑动子数组的美丽值 题目描述:给你一个长度为 n 的整…

波形生成:均匀和非均匀时间向量

波形生成—— 脉冲、chirp、VCO、正弦函数、周期性/非周期性和调制信号 使用 chirp 生成线性、二次和对数 chirp。使用 square、rectpuls 和 sawtooth 创建方波、矩形波和三角形波。 如需了解此处未显示的其他无线波形生成功能,请参阅无线波形发生器 (Communicat…

完整的生产车间管理流程是怎样的?六大步骤分享

阅读本文您将了解:1.生产车间管理的特征;2.生产车间管理流程具体步骤;3.生产车间管理流程规范的重要性。 一、生产车间管理的特征 车间管理是指对车间所从事的各项生产经营活动进行计划、组织、指挥、协调和控制的一系列管理工作。生产车间…

巧用千寻位置GNSS软件| 电力线勘测如何实现?

正如大家所知,电力线勘测是在做电力线路设计之前对设计线路沿途自然环境进行勘察测量,最后把手簿测量数据在电脑端经过转换输出为电力软件专用格式数据的专用功能。 那么在千寻位置GNSS软件中该如何操作完成电力线的勘察测量呢? 点击【测量】…

SwiftUI 中 TabView 如何原生使用类 UIPageView 的翻页样式?

功能需求 我们知道 TabView 是 SwiftUI 中非常好用的布局组织容器,它可以分类组织视图并依次展示给用户。 从 SwiftUI 2.0 开始(iOS 14.0+),TabView 除了常规的以标签(Tab Label)样式显示外,还可以用类似 UIPageView 的样式分页原生显示视图,显得更加简洁: 如上图所…

AWT-对话框——Dialog以及其子类FileDialog

Dialog: Dialog时Window类的子类,时一个容器类,属于特殊组件。对话框是可以独立存在的顶级窗口,因此用法与普通窗口的用法几乎完全一样,但是使用对话框需要注意以下几点: 对话框通常依赖于其它窗口,就是通…

基于Java+SpringBoot+vue学生学习平台详细设计实现

基于JavaSpringBootvue学生学习平台详细设计实现 博主介绍:5年java开发经验,专注Java开发、定制、远程、指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末获取源码联系方式 文章目…

AI Stable Diffusion Prompt参数【二】之 生成效果查验

AI Stable Diffusion Prompt参数【二】之 生成效果查验 效果国漫风生成参数配置prompt:Negative prompt:Model:Steps:Sampler:CFG scale:Clip skip:Model hash:Hires upscale:Hires upscaler:Denoising strength: 全部效果 效果 国漫风生成参数配置 prompt&#xf…

拉格朗日粒子扩散模式FLEXPART

为了高效、精准地治理区域大气污染,需要弄清污染物的来源。拉格朗日粒子扩散模式FLEXPART通过计算点、线、面或体积源释放的大量粒子的轨迹,来描述示踪物在大气中长距离、中尺度的传输、扩散、干湿沉降和辐射衰减等过程。该模式既可以通过时间的前向运算…

汇编与内联 x86-64

机器字长 x86是32位系统 64是64位系统 这里的32和64,指的都是机器字长 机器字长是 能直接进行整数/位运算的大小指针的大小(索引内存的范围) 容易与机器字长混淆的概念:字 字 字存储字长 字是MDR寄存器的位数,代表每个主存存储体中的存储…

[HDU - 4578]Transformation(线段树+多重懒标记)

[HDU - 4578]Transformation(线段树多重懒标记) 一、问题二、分析1、节点定义2、pushup3、pushdown(1)每种标记如何下传?赋值乘法加法 (2)三种标记下传的优先级问题 三、代码 一、问题 二、分析…

C++数据结构:手撕AVL树

目录 一. 什么是AVL树 二. AVL树的节点定义 三. AVL树的插入操作 3.1 寻找插入位置 3.2 更新平衡因子 3.3 AVL树的旋转调整 3.4 AVL树插入操作的整体实现 四. AVL树的检验 附录:AVL树的实现完整代码 AVL树定义代码 -- AVLTree.h AVL树检验代码 -- test.…

MFC加载动态gif图片文件C++语言,基于MFC的动画播放控件

MFC加载动态gif图片,使用VS2015环境 一、将下载的PictureEx.h和PictureEx.cpp放在工程文件的目录下,动态gif图片放在工程文件的res文件夹下;(GIF动图下载 https://icons8.com/preloaders/en/search/move) &#xff08…

软考软件设计师 操作系统笔记

操作系统地位 程序顺序执行(进程管理) 程序顺序执行的特征,顺序性封闭性可再现性 前趋图 P1结束后 V操作 SS1 P2操作前先执行S S -1 此时S0 一个箭头对应一个信号量 程序并发执行和前驱图 找到输入i计算c输出p,如果找不到就…

“老司机”机器视觉工程师警告,硬件,软件,固件,程序使用新版本务必谨慎

做任何事情之前,程序先保存。没保存,真的会哭的。千万别保存在系统盘。​ 机器视觉最终的目的解决是什么问题?项目验收结束。 如果公司不知道或者希望去测试新的东西,要积极主动去使用,也会学到很多新的东西&#xff…