深度解析源码,Spring 如何使用三级缓存解决循环依赖

目录

一. 前言

二. 基础知识

2.1. 什么是循环依赖?

2.2. 三级缓存

2.3. 原理执行流程

三. 源码解读

3.1. 代码入口

3.2. 第一层

3.3. 第二层

3.4. 第三层

3.5. 返回第二层

3.6. 返回第一层

四. 原理深度解读

4.1. 什么要有三级缓存?

4.2. 为什么三级缓存不直接存半成品的 A,而是要存一个代理工厂呢 ?

4.3. 能干掉第二级缓存么?

4.4. Spring 为什么不能解决构造器的循环依赖?

4.5. Spring 为什么不能解决 prototype 作用域循环依赖?

4.6. Spring 为什么不能解决多例的循环依赖?

五. 总结


一. 前言

    首先我们需要说明,Spring 只是解决了单例模式下属性依赖的循环问题。Spring 为了解决单例的循环依赖问题,使用了三级缓存。在创建 Bean 过程中贯穿着循环依赖问题,Spring 使用三级缓存解决循环依赖,这也是一个重要的知识点,所以我们下面就来看看 Spring 是如何使用三级缓存解决循环依赖的。

二. 基础知识

2.1. 什么是循环依赖?

    一个或多个对象之间存在直接或间接的依赖关系,这种依赖关系构成一个环形调用,有下面 3 种方式:

我们看一个简单的 Demo,对标“情况 2”:

@Service
public class Louzai1 {
    @Autowired
    private Louzai2 louzai2;

    public void test1() {
    }
}

@Service
public class Louzai2 {
    @Autowired
    private Louzai1 louzai1;

    public void test2() {
    }
}

这是一个经典的循环依赖,它能正常运行,后面我们会通过源码的角度,解读整体的执行流程。

2.2. 三级缓存

/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);
 
/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);

/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
  1. 第一层缓存(singletonObjects):单例对象缓存池,已经实例化并且属性赋值,这里的对象是成熟对象;
  2. 第二层缓存(earlySingletonObjects):单例对象缓存池,已经实例化但尚未属性赋值,这里的对象是半成品对象;
  3. 第三层缓存(singletonFactories):单例工厂的缓存,以便后面有机会创建代理对象。

这是最核心,我们直接上源码:

执行逻辑:

  1. 先从“第一级缓存”找对象,有就返回,没有就找“二级缓存”;
  2. 找“二级缓存”,有就返回,没有就找“三级缓存”;
  3. 找“三级缓存”,找到了,就获取对象,放到“二级缓存”,从“三级缓存”移除。

2.3. 原理执行流程

把“情况 2”执行的流程分解为下面 3 步,是不是和“套娃”很像 ?

整个执行逻辑如下:

  1. 在第一层中,先去获取 A 的 Bean,发现没有就准备去创建一个,然后将 A 的代理工厂放入“三级缓存”(这个 A 其实是一个半成品,还没有对里面的属性进行注入),但是 A 依赖 B 的创建,就必须先去创建 B;
  2. 在第二层中,准备创建 B,发现 B 又依赖 A,需要先去创建 A;
  3. 在第三层中,去创建 A,因为第一层已经创建了 A 的代理工厂,直接从“三级缓存”中拿到 A 的代理工厂,获取 A 的代理对象,放入“二级缓存”,并清除“三级缓存”;
  4. 回到第二层,现在有了 A 的代理对象,对 A 的依赖完美解决(这里的 A 仍然是个半成品),B 初始化成功;
  5. 回到第一层,现在 B 初始化成功,完成 A 对象的属性注入,然后再填充 A 的其它属性,以及 A 的其它步骤(包括 AOP),完成对 A 完整的初始化功能(这里的 A 才是完整的 Bean)。
  6. 将 A 放入“一级缓存”。

为什么要用三级缓存?我们先看源码执行流程,后面会给出答案。

三. 源码解读

注意:Spring 的版本是 5.2.15.RELEASE,否则代码可能不一样!

3.1. 代码入口

这里需要多跑几次,把前面的 beanName 跳过去,只看 louzai1。

3.2. 第一层

进入 doGetBean(),从 getSingleton() 没有找到对象,进入创建 Bean 的逻辑。

进入 doCreateBean() 后,调用 addSingletonFactory()。

往三级缓存 singletonFactories 塞入 louzai1 的工厂对象。

进入到 populateBean(),执行 postProcessProperties(),这里是一个策略模式,找到下图的策略对象。

正式进入该策略对应的方法。

下面都是为了获取 louzai1 的成员对象,然后进行注入。

进入 doResolveDependency(),找到 louzai1 依赖的对象名 louzai2。

需要获取 louzai2 的 Bean,是 AbstractBeanFactory 的方法。

正式获取 louzai2 的 Bean。

到这里,第一层套娃基本结束,因为 louzai1 依赖 louzai2,下面我们进入第二层套娃。

3.3. 第二层

    获取 louzai2 的 Bean,从 doGetBean(),到 doResolveDependency(),和第一层的逻辑完全一样,找到 louzai2 依赖的对象名 louzai1。

    前面的流程全部省略,直接到 doResolveDependency()。

正式获取 louzai1 的 Bean。

到这里,第二层套娃结束,因为 louzai2 依赖 louzai1,所以我们进入第三层套娃。

3.4. 第三层

    获取 louzai1 的 Bean,在第一层和第二层中,我们每次都会从 getSingleton() 获取对象,但是由于之前没有初始化 louzai1 和 louzai2 的三级缓存,所以获取对象为空。

    到了第三层,由于第三级缓存有 louzai1 数据,这里使用三级缓存中的工厂,为 louzai1 创建一个代理对象,塞入二级缓存。

这里就拿到了 louzai1 的代理对象,解决了 louzai2 的依赖关系,返回到第二层。

3.5. 返回第二层

    返回第二层后,louzai2 初始化结束,这里就结束了么?二级缓存的数据,啥时候会给到一级呢?

    甭着急,看这里,还记得在 doGetBean() 中,我们会通过 createBean() 创建一个 louzai2 的 Bean,当 louzai2 的 Bean 创建成功后,我们会执行 getSingleton(),它会对 louzai2 的结果进行处理。

我们进入 getSingleton(),会看到下面这个方法。

这里就是处理 louzai2 的一、二级缓存的逻辑,将二级缓存清除,放入一级缓存。

3.6. 返回第一层

同 3.5,louzai1 初始化完毕后,会把 louzai1 的二级缓存清除,将对象放入一级缓存。

到这里,所有的流程结束,我们返回 louzai1 对象。

四. 原理深度解读

4.1. 什么要有三级缓存?

    我们先说“一级缓存”的作用,变量命名为 singletonObjects,结构是 Map<String, Object>,它就是一个单例池,将初始化好的对象放到里面,给其它线程使用,如果没有第一级缓存,程序不能保证 Spring 的单例属性。

    “二级缓存”先放放,我们直接看“三级缓存”的作用,变量命名为 singletonFactories,结构是 Map<String, ObjectFactory<?>>,Map 的 Value 是一个对象的代理工厂,所以“三级缓存”的作用,其实就是用来存放对象的代理工厂。

    那这个对象的代理工厂有什么作用呢,我先给出答案,它的主要作用是存放半成品的单例 Bean,目的是为了“打破循环”,可能大家还是不太懂,这里我再稍微解释一下。

    我们回到文章开头的例子,创建 A 对象时,会把实例化的 A 对象存入“三级缓存”,这个 A 其实是个半成品,因为没有完成 A 的依赖属性 B 的注入,所以后面当初始化 B 时,B 又要去找 A,这时就需要从“三级缓存”中拿到这个半成品的 A(这里描述,其实也不完全准确,因为不是直接拿,为了让大家好理解,我就先这样描述),打破循环。

4.2. 为什么三级缓存不直接存半成品的 A,而是要存一个代理工厂呢 ?

    答案是因为 AOP。在解释这个问题前,我们看一下这个代理工厂的源码,让大家有一个更清晰的认识。

直接找到创建 A 对象时,把实例化的 A 对象存入“三级缓存”的代码,直接用前面的两幅截图:

下面我们主要看这个对象工厂是如何得到的,进入 getEarlyBeanReference() 方法。

最后一幅图太重要了,我们知道这个对象工厂的作用:

  1. 如果 A 有 AOP,就创建一个代理对象;
  2. 如果 A 没有 AOP,就返回原对象。

    那二级缓存的作用就清楚了,就是用来存放对象工厂生成的对象,这个对象可能是原对象,也可能是个代理对象

4.3. 能干掉第二级缓存么?

@Service
public class A {
    @Autowired
    private B b;

    @Autowired
    private C c;

    public void test1() {
    }
}

@Service
public class B {
    @Autowired
    private A a;

    public void test2() {
    }
}

@Service
public class C {
    @Autowired
    private A a;

    public void test3() {
    }
}

根据上面的套娃逻辑,A 需要找 B 和 C,但是 B 需要找 A,C 也需要找 A。

    假如 A 需要进行 AOP,因为代理对象每次都是生成不同的对象,如果干掉第二级缓存,只有第一、三级缓存:

  1. B 找到 A 时,直接通过三级缓存的工厂的代理对象,生成对象 A1。
  2. C 找到 A 时,直接通过三级缓存的工厂的代理对象,生成对象 A2。

    看到问题没?你通过 A 的工厂的代理对象,生成了两个不同的对象 A1 和 A2,所以为了避免这种问题的出现,我们搞个二级缓存,把 A1 存下来,下次再获取时,直接从二级缓存获取,无需再生成新的代理对象。

    所以,二级缓存的目的是为了避免因为 AOP 创建多个对象,其中存储的是半成品的 AOP 的单例 Bean

如果没有 AOP 的话,我们其实只要 一、三级缓存,就可以满足要求。

4.4. Spring 为什么不能解决构造器的循环依赖?

    构造器注入形成的循环依赖:也就是 beanB 需要在 beanA 的构造函数中完成初始化,beanA 也需要在 beanB 的构造函数中完成初始化,这种情况的结果就是两个 Bean 都不能完成初始化,循环依赖难以解决。

    Spring 解决循环依赖主要是依赖三级缓存,但是的在调用构造方法之前还未将其放入三级缓存之中,因此后续的依赖调用构造方法的时候并不能从三级缓存中获取到依赖的 Bean,因此不能解决。

4.5. Spring 为什么不能解决 prototype 作用域循环依赖?

    这种循环依赖同样无法解决,因为 Spring 不会缓存 prototype 作用域的 Bean,而 Spring 中循环依赖的解决正是通过缓存来实现的。

4.6. Spring 为什么不能解决多例的循环依赖?

    多实例 Bean 是每次调用一次 getBean 都会执行一次构造方法并且给属性赋值,根本没有三级缓存,因此不能解决循环依赖。

五. 总结

我们再回顾一下三级缓存的作用:

  1. 一级缓存:为“Spring 的单例属性”而生,就是个单例池,用来存放已经初始化完成的单例 Bean;
  2. 二级缓存:为“解决 AOP”而生,存放的是半成品的 AOP 的单例 Bean;
  3. 三级缓存:为“打破循环”而生,存放的是生成半成品单例 Bean 的工厂方法。

生成代理对象产生的循环依赖解决方法,主要有:

  1. 使用 @Lazy 注解,延迟加载;
  2. 使用 @DependsOn 注解,指定加载先后关系;
  3. 修改文件名称,改变循环依赖类的加载顺序。

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

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

相关文章

百面嵌入式专栏(面试题)驱动开发面试题汇总1.0

沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇我们将介绍驱动开发面试题 。 1、Linux驱动程序的功能是什么? 对设备初始化和释放。进行内核与硬件的数据交互。检测和处理设备出现的错误。2、内核程序中申请内存使用什么函数? 答案:kmalloc()、kzalloc()、vm…

Camunda流程引擎数据库架构

&#x1f496;专栏简介 ✔️本专栏将从Camunda(卡蒙达) 7中的关键概念到实现中国式工作流相关功能。 ✔️文章中只包含演示核心代码及测试数据&#xff0c;完整代码可查看作者的开源项目snail-camunda ✔️请给snail-camunda 点颗星吧&#x1f618; &#x1f496;数据库架构…

vue3 mathjax2.7.7 数学公式

1. index.html代码部分 <script type"text/x-mathjax-config">MathJax.Hub.Config({extensions: ["tex2jax.js"],jax: ["input/TeX","output/HTML-CSS"],tex2jax: {inlineMath: [["$","$"],["\\(&quo…

软件测试学习笔记-使用jmeter进行性能测试

性能测试&#xff1a;使用自动化工具&#xff0c;模拟不同的场景&#xff0c;对软件各项性能指标进行测试和评估的过程。 性能测试的目的&#xff1a; 评估当前系统的能力寻找性能瓶颈&#xff0c;优化性能评估软件是否能够满足未来的需要 性能测试和功能测试对比 焦点不同&…

沁恒微WCH32v003驱动ST7735S硬件spi+DMA调试小坑(2)

上一篇文章解决了spidma传输数据时DC线操作时序不匹配的问题&#xff0c;但是屏幕依旧没有点亮&#xff0c;所以这一篇文章继续找还存在的问题。上一篇文章&#xff1a;沁恒微WCH32v003驱动ST7735S硬件spiDMA调试小坑-CSDN博客 老规矩&#xff0c;先用逻辑分析仪抓取一下波形。…

幻兽帕鲁怎么样?好玩? Mac版的玩《幻兽帕鲁》也很简单,只需三个步骤

幻兽帕鲁怎么样 幻兽帕鲁是一款集合了多种游戏元素的游戏&#xff0c;它巧妙地融合了《方舟:生存进化》的野外生存挑战、《荒野之息》的开放世界探索、《魔兽世界》的多元角色互动以及宝可梦的精灵捕捉与培养等经典游戏元素。游戏的核心系统是「帕鲁」捕获&#xff0c;你可以让…

Redis -- zset有序集合

聪明在于勤奋&#xff0c;天才在于积累。 目录 zset 有序集合 zset相关命令 zadd zcard zcount zrange zrevrange zrangebyscore zpopmax bzpopmax zpopmin bzpopmin zrank zscore zrem zRemRangeByRank zRemRangeByScore zincrby 集合间操作 zinte…

光学PCIe 6.0技术引领AI时代超大规模集群

随着云计算、大数据和人工智能技术的快速发展&#xff0c;超大规模数据中心正经历一场前所未有的变革。传统的集中式架构逐渐转变为解聚式&#xff08;disaggregated&#xff09;架构&#xff0c;这种架构将计算、存储和网络资源从单一的物理服务器中分离出来&#xff0c;形成独…

【Vue3】解决路由缓存问题(响应路由参数的变化)

官方文档解释&#xff1a; 解决问题的思路: 让组件实例不复用,强制销毁重建监听路由变化,变化之后执行数据更新操作 方案一&#xff1a;给router-view添加key 以当前路由完整路径为key 的值&#xff0c;给router-view组件绑定 <RouterView :key"$route.fullPath&qu…

C++-模板基础

1. 泛型编程 大家在学习过程中一定写过swap函数吧&#xff0c;那么swap函数的可以写成很多种形式&#xff0c;因为形参的类型可以是任意类型&#xff0c;那么我们如果想用多种swap函数的话&#xff0c;就意味着我们必须写多个swap函数吗&#xff1f;不是的&#xff0c;C为了解…

Android14之Selinux报错:unknown type qemu_device at token (一百八十三)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

CleanMyMac X 4.14.7帮您安全清理Mac系统垃圾

CleanMyMac X 4.14.7是一款强大的 Mac 清理、加速工具和健康卫士,可以让您的 Mac 再次恢复巅峰性能。 移除大型和旧文件、卸载应用,并删除浪费磁盘空间的无用数据。 5倍 更多可用磁盘空间 CleanMyMac X 4.14.7帮您安全清理Mac系统垃圾 CleanMyMac X 4.14.7一键深度扫描mac系统…

python介绍,安装Cpython解释器,IDE工具pycharm的使用

python介绍 官方的Python解释器本质是基于C语言开发的一个软件&#xff0c;该软件的功能就是读取以py.结尾的文件内容&#xff0c;然后按照Guido定义好的语法和规则去翻译并执行相应的代码。这种C实现的解释器被称为Cpython。 python解释器的种类&#xff1a;Jython IPyth…

java数据结构与算法刷题-----LeetCode198. 打家劫舍

java数据结构与算法刷题目录&#xff08;剑指Offer、LeetCode、ACM&#xff09;-----主目录-----持续更新(进不去说明我没写完)&#xff1a;https://blog.csdn.net/grd_java/article/details/123063846 很多人觉得动态规划很难&#xff0c;但它就是固定套路而已。其实动态规划只…

第6节、T型加减速转动【51单片机+L298N步进电机系列教程】

↑↑↑点击上方【目录】&#xff0c;查看本系列全部文章 摘要&#xff1a;本章介绍步进电机T型加减速的控制方法&#xff0c;分三个小节&#xff0c;本小节主要内容为该控制方法的推导与计算。目前各平台对该控制方法介绍的文章目前较多&#xff0c;但部分关键参数并未给出推导…

莉莉与神奇花朵的冒险

现在&#xff0c;我将根据这些步骤编写一个对话形式的童话故事。 在很久很久以前的一个小村庄里&#xff0c;有一个勤劳善良的小女孩叫莉莉。她住在一间小茅屋里&#xff0c;和她的奶奶一起生活。奶奶年纪大了&#xff0c;行动不便&#xff0c;所以莉莉每天都要照顾她。 一天&a…

游戏开发-会飞的小鸟(已完结,附源码)

游戏开发-会飞的小鸟&#xff08;已完结&#xff0c;附源码&#xff09; 你将学到的课程链接详细介绍 你将学到的 掌握Java编程的基本技能开发出自己的“会飞的小鸟”游戏对面向对象编程有深刻的理解学会运用常见算法和数据结构解决问题能够独立调试和优化自己的代码 课程链接…

NLP_循环神经网络(RNN)

文章目录 RNN结构RNN实战RNN小结 RNN结构 NPLM 在处理长序列时会面临一些挑战。首先&#xff0c;由于它仍然是基于词的模型&#xff0c;因此在处理稀有词汇或者词汇表外的词汇时效果不佳。其次&#xff0c;NPLM不能很好地处理长距离依赖关系。而上面这两个局限&#xff0c;恰恰…

【芯片设计- RTL 数字逻辑设计入门 8 -- 四选一多路器】

文章目录 四选一多路输出器verilog case 语句verilog 代码testbench 代码仿真波形 问题小结 四选一多路输出器 制作一个四选一的多路选择器&#xff0c;要求输出定义上为线网类型 状态转换&#xff1a; d0 00 d1 01 d2 10 d3 11verilog case 语句 case(express…

蓝桥杯Web应用开发-CSS 基础语法4(字体属性、链接中的伪类、列表样式)

专栏持续更新中 字体属性 字体属性用于定义字体的类型、字号大小、加粗、斜体等方面样式。常用的字体属性如下表所示&#xff1a; 属 性可 取 值描 述fontfont-style、font-variant、font-weight、font-size&#xff08;或 line-height&#xff09;、font-family在一个声明中…