Spring使用三级缓存解决循环依赖

2023-10-26

前言

Spring 中使用了三级缓存的设计,来解决单例模式下的属性循环依赖问题。

这句话有两点需要注意

  1. 解决问题的方法是「三级缓存的设计」
  2. 解决的只是单例模式下的 Bean 属性循环依赖问题,对于多例 Bean 和 Prototype 作用域的 Bean的循环依赖问题,并不能使用三级缓存设计解决。

Bean 的生命周期

Spring Bean 的生命周期可以简单概括为 4 个阶段

  1. 实例化(Instantiation)
  2. 属性赋值(Populate)
  3. 初始化(Initialization)
  4. 销毁(Destruction)

什么是循环依赖

public class A {
    @Autowired
    private B b;
}

public class B {
    @Autowired
    private A a;
}
复制代码

如上代码所示,即 A 里面注入 B,B 里面又注入 A。此时,就发生了「循环依赖」。

三级缓存

Spring 中,单例 Bean 在创建后会被放入 IoC 容器的缓存池中,并触发 Spring 对该 Bean 的生命周期管理。

单例模式下,在第一次使用 Bean 时,会创建一个 Bean 对象,并放入 IoC 容器的缓存池中。后续再使用该 Bean 对象时,会直接从缓存池中获取。

保存单例模式 Bean 的缓存池,采用了三级缓存设计,如下代码所示。

/** Cache of singleton objects: bean name --> bean instance */
/** 一级缓存:用于存放完全初始化好的 bean **/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);
 
/** Cache of early singleton objects: bean name --> bean instance */
/** 二级缓存:存放原始的 bean 对象(尚未填充属性),用于解决循环依赖 */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);

/** Cache of singleton factories: bean name --> ObjectFactory */
/** 三级级缓存:存放 bean 工厂对象,用于解决循环依赖 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
复制代码
缓存层级 名称 描述
第一层缓存 singletonObjects 单例对象缓存池,存放的 Bean 已经实例化、属性赋值、完全初始化好(成品)
第二层缓存 earlySingletonObjects 早期单例对象缓存池,存放的 Bean 已经实例化但尚未属性赋值、未执行 init 方法(半成品)
第三层缓存 singletonFactories 单例工厂的缓存

使用三级缓存解决循环依赖

getSingleton方法中三级缓存的使用

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
  // Spring首先从singletonObjects(一级缓存)中尝试获取
  Object singletonObject = this.singletonObjects.get(beanName);
  // 若是获取不到而且对象在建立中,则尝试从earlySingletonObjects(二级缓存)中获取
  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) {
            //若是仍是获取不到而且允许从singletonFactories经过getObject获取,则经过singletonFactory.getObject()(三级缓存)获取
              singletonObject = singletonFactory.getObject();
              //若是获取到了则将singletonObject放入到earlySingletonObjects,也就是将三级缓存提高到二级缓存中
              this.earlySingletonObjects.put(beanName, singletonObject);
              this.singletonFactories.remove(beanName);
          }
        }
    }
  }
  return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
复制代码

getSingleton() 方法中

  • isSingletonCurrentlyInCreation() 方法用于判断当前单例 Bean 是否正在创建中,即「还没有执行初始化方法」。比如,A 的构造器依赖了 B 对象因此要先去创建 B 对象,或者在 A 的属性装配过程中依赖了 B 对象因此要先创建 B 对象,这时 A 就是处于创建中的状态。
  • allowEarlyReference 变量表示是否允许从三级缓存 singletonFactories 中经过 singletonFactory 的 getObject() 方法获取 Bean 对象。

分析 getSingleton() 的整个过程,可知三级缓存的使用过程如下

  1. Spring 会先从一级缓存 singletonObjects 中尝试获取 Bean。
  2. 若是获取不到,而且对象正在建立中,就会尝试从二级缓存 earlySingletonObjects 中获取 Bean。
  3. 若还是获取不到,且允许从三级缓存 singletonFactories 中经过 singletonFactory 的 getObject() 方法获取 Bean 对象,就会尝试从三级缓存 singletonFactories 中获取 Bean。
  4. 若是在三级缓存中获取到了 Bean,会将该 Bean 存放到二级缓存中。

第三级缓存为什么可以解决循环依赖

Spring 解决循环依赖的诀窍就在于 singletonFactories 这个三级缓存。 三级缓存中使用到了ObjectFactory 接口,定义如下

public interface ObjectFactory<T> {
    T getObject() throws BeansException;
}
复制代码

在 Bean 建立过程当中,有两处比较重要的匿名内部类实现了该接口。一处是 Spring 利用其建立 Bean 的时候,另外一处就是在 addSingletonFactory 方法中,如下代码所示。

addSingletonFactory(beanName, new ObjectFactory<Object>() {
   @Override   
   public Object getObject() throws BeansException {
       return getEarlyBeanReference(beanName, mbd, bean);
   }
});
复制代码

此处就是解决循环依赖的关键,这段代码发生在 createBeanInstance 以后

  1. 此时,单例 Bean 对象已经实例化(可以通过对象引用定位到堆中的对象),但尚未属性赋值和初始化。
  2. Spring 会将该状态下的 Bean 存放到三级缓存中,提早曝光给 IoC 容器(“提早”指的是不必等对象完成属性赋值和初始化后再交给 IoC 容器)。也就是说,可以在三级缓存 singletonFactories 中找到该状态下的 Bean 对象。

解决循环依赖示例分析

public class A {
    @Autowired
    private B b;
}

public class B {
    @Autowired
    private A a;
}
复制代码

在上文章节铺垫的基础上,此处结合一个循环依赖的案例,分析下如何使用三级缓存解决单例 Bean 的循环依赖。

  1. 创建对象 A,完成生命周期的第一步,即实例化(Instantiation),在调用 createBeanInstance 方法后,会调用 addSingletonFactory 方法,将已实例化但未属性赋值未初始化的对象 A 放入三级缓存 singletonFactories 中。即将对象 A 提早曝光给 IoC 容器。

  2. 继续,执行对象 A 生命周期的第二步,即属性赋值(Populate)。此时,发现对象 A 依赖对象,所以就会尝试去获取对象 B。

  3. 继续,发现 B 尚未创建,所以会执行创建对象 B 的过程。

  4. 在创建对象 B 的过程中,执行实例化(Instantiation)和属性赋值(Populate)操作。此时发现,对象 B 依赖对象 A。

  5. 继续,尝试在缓存中查找对象 A。先查找一级缓存,发现一级缓存中没有对象 A(因为对象 A 还未初始化完成);转而查找二级缓存,二级缓存中也没有对象 A(因为对象 A 还未属性赋值);转而查找三级缓存 singletonFactories,对象 B 可以通过 ObjectFactory.getObject 拿到对象 A。

  6. 继续,对象 B 在获取到对象 A 后,继续执行属性赋值(Populate)和初始化(Initialization)操作。对象 B 完成初始化操作后,会被存放到一级缓存中。

  7. 继续,转到「对象 A 执行属性赋值过程并发现依赖了对象 B」的场景。此时,对象 A 可以从一级缓存中获取到对象 B,所以可以顺利执行属性赋值操作。

  8. 继续,对象 A 执行初始化(Initialization)操作,完成后,会被存放到一级缓存中。

Spring为何不能解决非单例Bean的循环依赖

Spring 为何不能解决非单例 Bean 的循环依赖? 这个问题可以细分为下面几个问题

  1. Spring 为什么不能解决构造器的循环依赖?
  2. Spring 为什么不能解决 prototype 作用域循环依赖?
  3. Spring 为什么不能解决多例的循环依赖?

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

对象的构造函数是在实例化阶段调用的。

上文中提到,在对象已实例化后,会将对象存入三级缓存中。在调用对象的构造函数时,对象还未完成初始化,所以也就无法将对象存放到三级缓存中。

在构造函数注入中,对象 A 需要在对象 B 的构造函数中完成初始化,对象 B 也需要在对象 A的构造函数中完成初始化。此时两个对象都不在三级缓存中,最终结果就是两个 Bean 都无法完成初始化,无法解决循环依赖问题。

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

Spring IoC 容器只会管理单例 Bean 的生命周期,并将单例 Bean 存放到缓存池中(三级缓存)。Spring 并不会管理 prototype 作用域的 Bean,也不会缓存该作用域的 Bean,而 Spring 中循环依赖的解决正是通过缓存来实现的。

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

多实例 Bean 是每次调用 getBean 都会创建一个新的 Bean 对象,该 Bean 对象并不能缓存。而 Spring 中循环依赖的解决正是通过缓存来实现的。

非单例Bean的循环依赖如何解决

  • 对于构造器注入产生的循环依赖,可以使用 @Lazy 注解,延迟加载。
  • 对于多例 Bean 和 prototype 作用域产生的循环依赖,可以尝试改为单例 Bean。

为什么一定要三级缓存

为什么一定要三级缓存,使用两级缓存可以解决循环依赖吗?

带着这个思考,进入下文。

尝试使用两级缓存解决依赖冲突

第三级缓存的目的是为了延迟代理对象的创建,因为如果没有依赖循环的话,那么就不需要为其提前创建代理,可以将它延迟到初始化完成之后再创建。

既然目的只是延迟的话,那么我们是不是可以不延迟创建,而是在实例化完成之后,就为其创建代理对象,这样我们就不需要第三级缓存了。因此,我们可以将 addSingletonFactory() 方法进行改造。

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");

    synchronized (this.singletonObjects) {
        // 判断一级缓存中不存在此对象
        if (!this.singletonObjects.containsKey(beanName)) { 
            // 直接从工厂中获取 Bean
            Object o = singletonFactory.getObject();

            // 添加至二级缓存中
            this.earlySingletonObjects.put(beanName, o);
            this.registeredSingletons.add(beanName);
        }
    }
}
复制代码

这样的话,每次实例化完 Bean 之后就直接去创建代理对象,并添加到二级缓存中。

测试结果是完全正常的,Spring 的初始化时间应该也是不会有太大的影响,因为如果 Bean 本身不需要代理的话,是直接返回原始 Bean 的,并不需要走复杂的创建代理 Bean 的流程。

三级缓存的意义

测试证明,二级缓存也是可以解决循环依赖的。为什么 Spring 不选择二级缓存,而要额外多添加一层缓存,使用三级缓存呢?

如果 Spring 选择二级缓存来解决循环依赖的话,那么就意味着所有 Bean 都需要在实例化完成之后就立马为其创建代理,而 Spring 的设计原则是在 Bean 初始化完成之后才为其创建代理。

使用三级缓存而非二级缓存并不是因为只有三级缓存才能解决循环引用问题,其实二级缓存同样也能很好解决循环引用问题。使用三级而非二级缓存并非出于 IOC 的考虑,而是出于 AOP 的考虑,即若使用二级缓存,在 AOP 情形注入到其他 Bean的,不是最终的代理对象,而是原始对象。

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring使用三级缓存解决循环依赖 的相关文章

  • 如果在睡眠线程上调用interrupt()会发生什么?

    我有一个线程 然后run I call sleep 如果我中断这个线程会发生什么 MyThread extends Thread public void run try sleep 1000000 catch InterruptedExce
  • spring webflow,评估表达式在转换中被忽略

    我有一个流定义为流
  • 无法使用maven编译java项目

    我正在尝试在 java 16 0 1 上使用 maven 构建 IntelliJ 项目 但它无法编译我的项目 尽管 IntelliJ 能够成功完成 在此之前 我使用maven编译了一个java 15项目 但我决定将所有内容更新到16 0 1
  • 如何在 JSP 中导入类?

    我是一个完全的JSP初学者 我正在尝试使用java util List在 JSP 页面中 我需要做什么才能使用除以下类之外的类java lang 使用以下导入语句进行导入java util List 顺便说一句 要导入多个类 请使用以下格式
  • 在 ContainerRequestFilter 中填充 spring request 作用域 bean

    我使用 jersey 1 13 和 spring 3 1 1 编写了一个在 tomcat 6 上运行的休息服务 在 tomcat 中 我使用一个领域来进行身份验证 在我的应用程序中 我需要当前用户 但我不想从每个资源中的球衣访问Securi
  • Java套接字:在连接被拒绝异常时重试的最佳方法?

    现在我正在这样做 while true try SocketAddress sockaddr new InetSocketAddress ivDestIP ivDestPort downloadSock new Socket downloa
  • 无法在 Spring Boot 测试中模拟 persistenceContext

    我正在使用带有 Mockito 框架的 spring boot 测试来测试我的应用程序 存储库类 EntityManager 之一作为参考 我的班级如下所示 Repository Transactional Slf4j public cla
  • Firestore - RecycleView - 图像持有者

    我不知道如何编写图像的支架 我已经设置了 2 个文本 但我不知道图像的支架应该是什么样子 你能帮我告诉我图像的文字应该是什么样子才能正确显示吗 holder artistImage setImageResource model getArt
  • 在java中实现你自己的阻塞队列

    我知道这个问题之前已经被问过并回答过很多次了 但我只是无法根据互联网上找到的示例找出窍门 例如this http tutorials jenkov com java concurrency blocking queues html or t
  • 用于从字段中查找最大值的 MongoTemplate 方法或查询

    我正在使用 MongoTemplate 进行数据库操作 现在我想从所选结果中获取最大字段值 有人可以指导我如何编写查询 以便当我将查询传递给 find 方法时 它将返回我所需的文档最大字段 提前致谢 问候 可以在spring data mo
  • 如何将使用消息侦听器接收到的 JMS 消息转换为域对象

    我在用 春季3 1 1 ActiveMQ 5 6 0 我有两个 JMS 应用程序 应用程序 A 使用 JmsTemplate 使用 jmsTemplate convertAndSend msg 发送域对象 App B 使用消息监听器并注册了
  • 计算日期之间的天数差异

    在我的代码中 日期之间的差异是错误的 因为它应该是 38 天而不是 8 天 我该如何修复 package random04diferencadata import java text ParseException import java t
  • Java 收集返回顶级项目的映射的嵌套流

    我有以下模型 class Item String name List
  • 无法捕获 Spring Batch 的 ItemWriter 中的异常

    我正在编写一个 Spring Batch 流程来将数据集从一个系统迁移到另一个系统 在这种情况下 这就像使用RowMapper实现在传递给查询之前从查询构建对象ItemWriter The ItemWriter称为save我的 DAO 上的
  • 将图像添加到自定义 AlertDialog

    我制作了一个 AlertDialog 让用户可以从我显示的 4 个选项中选择一个 前 3 个让他们在单击号码时直接拨打号码 第 4 个显示不同的视图 现在看起来是这样的 由于第四个选项的目的是不同的任务 我想让它看起来不同 因为用户可能会感
  • 如何在 Quartz 调度程序中每 25 秒运行一次?

    我正在使用 Java 的 Quartz Scheduling API 你能帮我使用 cron 表达式每 25 秒运行一次吗 这只是一个延迟 它不必总是从第 0 秒开始 例如 序列如下 0 00 0 25 0 50 1 15 1 40 2 0
  • 解决错误javax.mail.AuthenticationFailedException

    我不熟悉java中发送邮件的这个功能 我在发送电子邮件重置密码时遇到错误 希望你能给我一个解决方案 下面是我的代码 public synchronized static boolean sendMailAdvance String emai
  • 哪个集合更适合存储多维数组中的数据?

    我有一个multi dimensional array of string 我愿意将其转换为某种集合类型 以便我可以根据自己的意愿添加 删除和插入元素 在数组中 我无法删除特定位置的元素 我需要这样的集合 我可以在其中删除特定位置的数据 也
  • Java的-XX:+UseMembar参数是什么

    我在各种地方 论坛等 看到这个参数 并且常见的答案是它有助于高并发服务器 尽管如此 我还是找不到 sun 的官方文档来解释它的作用 另外 它是Java 6中添加的还是Java 5中存在的 顺便说一句 许多热点虚拟机参数的好地方是这一页 ht
  • Hibernate 和可序列化实体

    有谁知道是否有一个框架能够从实体类中剥离 Hibernate 集合以使它们可序列化 我查看了 BeanLib 但它似乎只进行实体的深层复制 而不允许我为实体类中的集合类型指定实现映射 BeanLib 目前不适用于 Hibernate 3 5

随机推荐

  • http://chdbits.org/signup.php 邀请码,www.chdbits.co

    Domain Name chdbits co Registry Domain ID D167587220 CO Registrar WHOIS Server whois godaddy com Registrar URL www godad
  • IAR下如何确定某一段代码的执行时间

    1 接出来一个I O口 然后设置反转 用示波器查看反转周期 2 软件仿真时计算两断点CYCLECOUNTER 在CPU registers中 的差值 乘以指令周期 MCLK 便是执行时间
  • getopt_long 杂谈

    首先先解释一下 getopt long 的 struct 的形式 const struct option longopts help 0 0 h container 1 0 c statistics 1 0 s verbose 0 0 v
  • 关于实现订单超时的几种方案

    更新 2022 10 28 说明 关于使用rabbitmq实现订单超时的部分说明有错误 首先mq是可以实现自定义超时时间的 我们可以在创建队列queue ordercreate时不设置它的x message ttl参数 转而在代码里设置消息
  • 用卷积神经网络实现手写字体的识别

    代码如下所示 coding utf 8 Time 2018 4 4 13 22 Author mgliu FileName mnist py Software PyCharm Community Edition coding utf 8 i
  • 使用R语言进行股票价格预测

    目录 1 准备工作 2 数据准备 3 数据预处理 4 构建和训练模型 5 评估模型
  • win7定位位置服务器,win7 定位 服务器地址

    win7 定位 服务器地址 内容精选 换一换 反向解析主要应用于自建邮箱服务器 可以提高邮箱服务器IP地址和域名的信誉度 多数垃圾邮件发送者使用动态分配或者没有注册域名的IP地址来发送垃圾邮件 以逃避追踪 设置了邮箱服务器IP地址到域名的反
  • eu5,eu7,ex3,ex5安装第三方app

    本教程介绍了 如何简单快速的在北汽的eu5 eu7 ex3 ex5等车安装第三方app应用 比如高德地图 QQ音乐 大大提高车机的娱乐性和可用性 安装过程简单明了 只需要一个U盘就可以了 不需要输入任何命令 按照教程点击按钮就可以完成所有操
  • 车联网安全基础知识学习笔记

    全球车联网产业生态不断丰富完善 当前 全球数字经济快速发展 新一代信息通信技术与各行各业融合渗透 车联网 工业互联网 物联网等新型产业生态不断壮大 有力推动了汽车 交通等传统产业的数字化 网络化 智能化发展 也逐步衍生出智慧出行 交通数字化
  • JeeSite简介

    系统管理 SYS 模块 包括组织架构 用户管理 机构管理 区域管理 菜单管理 角色权限管理 字典管理等功能 内容管理 CMS 模块 包括内容管理 文章 链接 栏目管理 站点管理 公共留言 文件管理 前端网站展示等功能 在线办公 OA 模块
  • 【MRI图像超分辨率入门及研究综述(个人总结版)】

    提示 文章写完后 目录可以自动生成 如何生成可参考右边的帮助文档 文章目录 前言 一 了解核磁共振成像 1 机械系统 2 物理学原理 3 数据类型及类型转换 类型 格式转换 1 为什么转换 2 如何进行转换 4 成像过程 k space 定
  • 012-Unity中的几种Find方法

    版本 2018 3 14 1 transform Find 1 可以查找隐藏对象 2 支持路径查找 3 查找隐藏对象的前提是transform所在的根节点必须可见 即active true 注意 只能查找挂载物体上的子物体 否则查不到 2
  • vite require is not defined

    vuex使用webpack中的require context modules true js 获取目录下的所有文件 升级vue3使用vite报 require is not defined import createStore from v
  • element的分页怎么改中文配置_ElementUI pagination分页 自定义配置

    如何使用 ElementUI 自定义分页呢 尽管ElementUI比较全面的提供了分页 但实际开发中的需求是多种多样的 比如我需要把下图中的下拉选择每页的数据条数 换成可以手动输入的形式 并且把他的位置放到前往xx页的左边位置 实现代码如下
  • airpods耳机敲击没反应_为什么华强北AirPods出了那么多仿制品还有很多人不怕被坑,愿意买?...

    原因很简单 原版AirPods功能过于强大 体验感极好 设计非常细节 其次 华强北AirPods价格比原版优惠力度大 原版AirPods外观上 名称 经销商 UPS QI充电器指示 充电外壳采用反磁设计 打开后因为磁极排斥而不会自由下落 耳
  • Google Mock

    View Edit History Content 什么是Mock Google Mock概述 参考文档 最简单的例子 典型的流程 自定义方法 成员函数的期望行为 我改过的例子 现实中的例子 Mock protected private方法
  • 设计模式之观察者模式

    案例展示 原理分析 代码实现 Observer 接口 观察者接口 由观察者来实现 interface Observer fun update temperatrue Float pressure Float humidity Float O
  • 详解Python中的切片(一看就懂版)

    前言 在我们使用Python的时候 经常会听到 切片 这个词 那什么是切片呢 切片是对序列数据 列表 元组 字符串 根据下标索引 对一定范围内数据的获取 简单来说就是 通过下标索引获取一定范围内的元素 基本索引 什么叫基本索引呢 在Pyth
  • 深度学习的基本概念总结

    1 基本概念 1 1 为什么要使用深层网络 深度神经网络的学习是特征递进的 浅层的神经元只能学习一些低层次的简单特征 如边缘 纹理 而深层神经网络可以学到更高级特征 深层网络的隐藏单元数目较少 隐藏层数目较多 若浅层网络想达到同样的计算结果
  • Spring使用三级缓存解决循环依赖

    前言 Spring 中使用了三级缓存的设计 来解决单例模式下的属性循环依赖问题 这句话有两点需要注意 解决问题的方法是 三级缓存的设计 解决的只是单例模式下的 Bean 属性循环依赖问题 对于多例 Bean 和 Prototype 作用域的