当前位置: 首页 > news >正文

Spring三级缓存解决循环依赖问题

文章目录

  • 1. 三级缓存解决的问题场景
  • 2. 三级缓存的差异性
  • 3. 循环依赖时的处理流程
  • 4. 源码验证

1. 三级缓存解决的问题场景

循环依赖指的是在对象之间存在相互依赖关系,形成一个闭环,导致无法准确地完成对象的创建和初始化;当两个或多个对象彼此之间相互引用,而这种相互引用形成一个循环时,就可能出现循环依赖问题。

在早期的 Spring 版本中是可以自动解决的循环依赖的问题的,

public class A {@Autowiredprivate B b;
}public class B {@Autowiredprivate A a;
}

img

但要注意,Spring 解决循环依赖是有前提条件的,

🍂第一,要求互相依赖的 Bean 必须要是单例的 Bean。

  • 这是因为对于原型范围的 Bean(prototype scope),每次请求都会创建一个新的 Bean 实例,这样每次尝试解析循环依赖时,都会产生新的 Bean 实例,导致无限循环,由于没有全局的、持续的单例实例的缓存来引用,因此循环依赖无法得到解决。

🍂第二,依赖注入的方式不能都是构造函数注入的方式。

  • 当使用构造函数注入时,一个 Bean 的实例在构造函数被完全调用之前是不会被创建的;如果 Bean A 的构造函数依赖于 Bean B,而 Bean B 的构造函数又依赖于 Bean A,那么就会产生一个死锁的情况,因为两者都不能在对方初始化之前完成初始化。
public class C {private D d;@Autowiredpublic C(D d) {this.dService = dService;}
}public class D {private C c;@Autowiredpublic D(C c) {this.c = c;}
}

Spring 源码中关于三级缓存的定义如下:

// 一级缓存
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// 三级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

所以说,所谓的“三级缓存”就是是指三个 Map 数据结构,分别用于存储不同状态的 Bean。

2. 三级缓存的差异性

img

🎯一级缓存:

一级缓存保存的是已经完全初始化和实例化的 Bean 对象,在程序中使用的 Bean 通常就是从这个缓存中获取的;这个缓存的目的是确保 Bean 只初始化一次(是单例的),避免多次实例化相同的Bean对象,提高性能。

🎯二级缓存:

二级缓存用来解决 Bean 创建过程中的循环依赖问题,它存储的是尚未完成属性注入和初始化的“半成品”Bean 对象;当 Spring容器发现两个或多个 Bean 之间存在循环依赖时,也就是当一个 Bean 创建过程中需要引用另一个正在创建的 Bean,Spring 将创建需要的这些未完全初始化的对象提前暴露在二级缓存中,以便其他 Bean 进行引用,确保它们之间的依赖能够被满足。

🎯三级缓存:

三级缓存中存储的是 ObjectFactory<?> 类型的代理工厂对象,主要用于处理存在 AOP 时的循环依赖问题;每个 Bean 都对应一个 ObjectFactory 对象,通过调用该对象的 getObject 方法,可以获取到早期暴露出去的 Bean;在该 Bean 要被其他 Bean 引用时,Spring 就会用工厂对象创建出该 Bean 的实例对象,最终当该 Bean 完成构造的所有步骤后就会将该 Bean 放入到一级缓存中。

3. 循环依赖时的处理流程

当 Spring 发生循环依赖时(以最开始介绍的场景为例,A B 两个 Bean 相互依赖),以下是完善的执行流程:

  1. 遍历待创建的所有 beanName:在容器启动时,Spring 会遍历所有需要创建的 Bean 名称,在第一次遍历到 A 时,就开始获取 Bean A;如果 Bean A 的实例不在一级、二级缓存中(缓存中没有值),Spring 会开始正常的 Bean 创建流程。
  2. Bean A 的创建:Bean A 的创建过程开始,然后 Spring 会检查是否 A 是单例(Singleton),同时 A 是否已经创建完成;如果 A 是单例且尚未创建完成,将 A 的 BeanFactory 存入三级缓存。
  3. 处理依赖注入:A 开始处理 @Autowired 注解,尝试注入 B 属性;Spring 会在一级、二级缓存中来查找 Bean B,如果找不到,则开始正常创建 Bean B 的流程。
  4. Bean B 的创建:Bean B 的创建过程类似于 A,首先判断B是否是单例,且是否已创建完成,如果否,B 的BeanFactory 也会被存入三级缓存。
  5. B 注入 A 属性:B 开始注入 A 属性,尝试从一级、二级缓存中查找 A;如果在缓存中找不到 A,B 会尝试从三级缓存获取 A 的 BeanFactory,并通过 BeanFactory的getObject()方法获取 A 的属性,此时,A 被存入二级缓存,同时清除三级缓存;因此,B 能够成功注入 A 属性,B 接着执行初始化,处于实例化和初始化都已完成的完全状态。
  6. B 存入一级缓存:B执行addSingleton(),将完全状态的 B 存入一级缓存,并清空二级,三级缓存。
  7. A 继续注入 B 属性:A 继续注入 B 属性,调用beanFactory.getBean()方法获取 B,由于第六步已经将 B 存入一级缓存,A 可以直接获取 B,成功注入 B 属性, A 接着执行初始化,得到一个完全状态的 A。
  8. A 存入一级缓存:A 执行addSingleton(),将完全状态的 A 存入一级缓存,并清空二级缓存。

此时,A 和 B 都被实例化和初始化完成,解决了循环依赖的问题;这个流程确保了每个Bean在需要时都能够获取到已完全初始化的依赖项。

🚀常见疑问解答

问题一:为什么在 Bean B 被注入 Bean A 之前,需要将 Bean A 存入二级缓存?

  • 主要原因是,如果存在其他循环依赖需要用到 A,从二级缓存中直接取出早期的 Bean A 对象会更加高效。

问题二:为什么创建完 Bean 后要清空二、三级缓存?

  • 清空是为了节省存储空间,一旦 Bean 完全初始化并存储在一级缓存中,其在二、三级缓存中的记录就不再需要了。

问题三:三级缓存为什么不能解决构造器引起的循环依赖?

  • 这是因为构造器引起的循环依赖发生在 Bean 的实例化阶段,这个阶段比二、三级缓存处理的阶段还要早,无法创建出早期的半成品对象。

问题四:如果不使用三级缓存,只使用二级缓存,能否解决循环依赖?

肯定是不能的,二级缓存存储的 Bean 可能是两种类型,一种是实例化阶段创建出来的对象,另一种是实例化阶段创建出来的对象的代理对象;是否需要代理对象取决于你的配置需要,如是否添加了事务注解或自定义 AOP 切面;如果放弃使用三级缓存,即没有 ObjectFactory,那么就需要将早期的 Bean 放入二级缓存;但问题是,应该将未被代理的 Bean 还是代理的 Bean 放入二级缓存,这只能在属性注入阶段,处理注解时才能分辨。

  • 如果直接往二级缓存添加没有被代理的 Bean,那么此时注入给其它对象的 Bean 可能跟最后完全生成的 Bean 是不一样的,因为最后生成使用的是可能代理对象,此时注入的是原始对象,这这种情况是不允许发生的。
  • 如果直接往二级缓存添加一个代理 Bean,在不确定是否要使用代理对象的情况下,就有提前暴露代理对象的可能;正常的代理的对象都是初始化后期调用生成的,是基于后置处理器的,若提早的代理就违背了 Bean 定义的生命周期。

Spring 在一个三级缓存放置一个工厂,如果产生循环依赖 ,这个工厂的作用就是判断这个对象是否需要代理,如果否则直接返回,如果是则返回代理对像。

4. 源码验证

在项目中双击 Shift,全局查找文件:AbstractAutowireCapableBeanFactory,找到 550 行左右的 doCreateBean 方法,重点看一下 580 行到 600 行这20行代码就行,包含了三级缓存、属性注入、初始化,精华都在这20行,下面在源码中给出了关键注释。

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)throws BeanCreationException {// Instantiate the bean.BeanWrapper instanceWrapper = null;if (mbd.isSingleton()) {instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);}// 通过BeanDefinition实例化对象if (instanceWrapper == null) {instanceWrapper = createBeanInstance(beanName, mbd, args);}Object bean = instanceWrapper.getWrappedInstance();Class<?> beanType = instanceWrapper.getWrappedClass();if (beanType != NullBean.class) {mbd.resolvedTargetType = beanType;}// Allow post-processors to modify the merged bean definition.synchronized (mbd.postProcessingLock) {if (!mbd.postProcessed) {try {applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);}catch (Throwable ex) {throw new BeanCreationException(mbd.getResourceDescription(), beanName,"Post-processing of merged bean definition failed", ex);}mbd.postProcessed = true;}}// Eagerly cache singletons to be able to resolve circular references// even when triggered by lifecycle interfaces like BeanFactoryAware.// 对象是否单例、是否未创建完成boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&isSingletonCurrentlyInCreation(beanName));if (earlySingletonExposure) {if (logger.isTraceEnabled()) {logger.trace("Eagerly caching bean '" + beanName +"' to allow for resolving potential circular references");}// 将对象的工厂加入到三级缓存addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}// Initialize the bean instance.Object exposedObject = bean;try {// 属性注入 (在这里解析@Autowired注解时,触发循环依赖)populateBean(beanName, mbd, instanceWrapper);// 初始化exposedObject = initializeBean(beanName, exposedObject, mbd);}catch (Throwable ex) {if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {throw (BeanCreationException) ex;}else {throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);}}if (earlySingletonExposure) {// 从缓存中获取 BeanObject earlySingletonReference = getSingleton(beanName, false);if (earlySingletonReference != null) {if (exposedObject == bean) {exposedObject = earlySingletonReference;}else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {String[] dependentBeans = getDependentBeans(beanName);Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);for (String dependentBean : dependentBeans) {if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {actualDependentBeans.add(dependentBean);}}if (!actualDependentBeans.isEmpty()) {throw new BeanCurrentlyInCreationException(beanName,"Bean with name '" + beanName + "' has been injected into other beans [" +StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +"] 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.");}}}}// Register bean as disposable.try {registerDisposableBeanIfNecessary(beanName, bean, mbd);}catch (BeanDefinitionValidationException ex) {throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);}return exposedObject;
}

从缓存中获取 Bean 的源码

protected Object getSingleton(String beanName, boolean allowEarlyReference) {// Quick check for existing instance without full singleton lockObject singletonObject = this.singletonObjects.get(beanName);// 从一级缓存中获取// 如果一级缓存里没有,且 Bean 正在创建中// 就从二级缓存里获取if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {singletonObject = this.earlySingletonObjects.get(beanName);// 二级缓存没有,就从三级缓存获取一个工厂if (singletonObject == null && allowEarlyReference) {synchronized (this.singletonObjects) {// Consistent creation of early reference within full sinsingletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null) {ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);if (singletonFactory != null) {// 能获取到工厂则创建 BeansingletonObject = singletonFactory.getObject();// 把实例存入二级缓存this.earlySingletonObjects.put(beanName, singletonObject);// 把工厂从三级缓存移除this.singletonFactories.remove(beanName);}}}}}}return singletonObject;
}

总之,Spring 的三级缓存机制是一个巧妙的设计,它解决了在 Bean 初始化过程中可能出现的循环依赖问题;对于 Spring 的用户来说,这意味着更加稳定和可靠的 Bean 管理和依赖注入机制。

相关文章:

Spring三级缓存解决循环依赖问题

文章目录 1. 三级缓存解决的问题场景2. 三级缓存的差异性3. 循环依赖时的处理流程4. 源码验证 1. 三级缓存解决的问题场景 循环依赖指的是在对象之间存在相互依赖关系&#xff0c;形成一个闭环&#xff0c;导致无法准确地完成对象的创建和初始化&#xff1b;当两个或多个对象彼…...

Unity 中使用波浪动画创建 UI 图像

如何使用 只需将此组件添加到画布中的空对象即可。强烈建议您将此对象放入其自己的画布/嵌套画布中&#xff0c;因为它会弄脏每一帧的画布并导致重新生成整个网格。 注意&#xff1a;不支持切片图像。 using System.Collections.Generic; using UnityEngine; using UnityEng…...

支付功能测试用例测试点?

支付功能测试用例测试点是指在测试支付功能时&#xff0c;需要关注和验证的各个方面。根据不同的支付场景和需求&#xff0c;支付功能测试用例测试点可能有所不同&#xff0c;但一般可以分为以下几类&#xff1a; 功能测试&#xff1a;主要检查支付功能是否符合设计和业务需求…...

HFS 快速搭建 http 服务器

HFS 是一个轻量级的HTTP 服务工具&#xff0c;3.0版本前进提供Windows平台安装包&#xff0c;3.0版本开提供Linux和macOS平台的安装包。 HFS更适合在局域网环境中搭建文件共享服务或者安装配置源服务器。 甲 非守护进程的方式运行 HFS &#xff08;Ubuntu 22.04&#xff09; 一…...

学生专用台灯怎么选?双十一专业学生护眼台灯推荐

台灯应该是很多家庭都会备上一盏的家用灯具&#xff0c;很多大人平时间看书、用电脑都会用上它&#xff0c;不过更多的可能还是给家中的小孩学习、阅读使用的。而且现在的孩子近视率如此之高&#xff0c;这让家长们不得不重视孩子的视力健康问题。那么孩子学习使用的台灯应该怎…...

Go 常用标准库之 fmt 介绍与基本使用

Go 常用标准库之 fmt 介绍与基本使用 文章目录 Go 常用标准库之 fmt 介绍与基本使用一、介绍二、向外输出2.1 Print 系列2.2 Fprint 系列2.3 Sprint 系列2.4 Errorf 系列 三、格式化占位符3.1 通用占位符3.2 布尔型3.3 整型3.4 浮点数与复数3.5 字符串和[]byte3.6 指针3.7 宽度…...

antv/x6 导出图片方法exportPNG

antv/x6 导出图片方法exportPNG antv/x6 版本如下&#xff1a; "antv/x6": "2.14.1","antv/x6-plugin-export": "2.1.6",在文件中导入 import { Graph, Shape, StringExt } from antv/x6 import { Export } from antv/x6-plugin-exp…...

Decomposed Meta-Learning for Few-Shot Named Entity Recognition

原文链接&#xff1a; https://aclanthology.org/2022.findings-acl.124.pdf ACL 2022 介绍 问题 目前基于span的跨度量学习&#xff08;metric learning&#xff09;的方法存在一些问题&#xff1a; 1&#xff09;由于是通过枚举来生成span&#xff0c;因此在解码的时候需要额…...

C++经典面试题:内存泄露是什么?如何排查?

1.内存泄露的定义&#xff1a;内存泄漏简单的说就是申请了⼀块内存空间&#xff0c;使⽤完毕后没有释放掉。 它的⼀般表现⽅式是程序运⾏时间越⻓&#xff0c;占⽤内存越多&#xff0c;最终⽤尽全部内存&#xff0c;整个系统崩溃。由程序申请的⼀块内存&#xff0c;且没有任何⼀…...

Hadoop+Hive+Spark+Hbase开发环境练习

1.练习一 1.数据准备 在hdfs上创建文件夹&#xff0c;上传csv文件 [rootkb129 ~]# hdfs dfs -mkdir -p /app/data/exam 查看csv文件行数 [rootkb129 ~]# hdfs dfs -cat /app/data/exam/meituan_waimai_meishi.csv | wc -l 2.分别使用 RDD和 Spark SQL 完成以下分析&#xf…...

使用Spring Boot限制在一分钟内某个IP只能访问10次

有些时候&#xff0c;为了防止我们上线的网站被攻击&#xff0c;或者被刷取流量&#xff0c;我们会对某一个ip进行限制处理&#xff0c;这篇文章&#xff0c;我们将通过Spring Boot编写一个小案例&#xff0c;来实现在一分钟内同一个IP只能访问10次&#xff0c;当然具体数值&am…...

ES 数据迁移最佳实践

ES 数据迁移最佳实践与讲解 数据迁移是 Elasticsearch 运维管理和业务需求中常见的操作之一。以下是不同数据迁移方法的最佳实践和讲解&#xff1a; 一、数据迁移需求梳理 二、数据迁移方法梳理 三、各方案对比 方案 优点 缺点&#xff08;限制&#xff09; 适用场景 是否有…...

C++中低级内存操作

C中低级内存操作 C相较于C有一个巨大的优势&#xff0c;那就是你不需要过多地担心内存管理。如果你使用面向对象的编程方式&#xff0c;你只需要确保每个独立的类都能妥善地管理自己的内存。通过构造和析构&#xff0c;编译器会帮助你管理内存&#xff0c;告诉你什么时候需要进…...

Linux硬盘大小查看命令全解析 (linux查看硬盘大小命令)

Linux操作系统是一款广泛应用于服务器和嵌入式设备的操作系统&#xff0c;相比于Windows等其他操作系统&#xff0c;Linux的优点之一就是支持强大的命令行操作。在日常操作中&#xff0c;了解和掌握一些简单但实用的命令可以提高工作效率。比如硬盘大小查看命令&#xff0c;在L…...

什么是供应链金融?

一、供应链金融产生背景 供应链金融兴起的起源来自于供应链管理一个产品生产过程分为三个阶段&#xff1a;原材料 - 中间产品 - 成产品。由于技术进步需求升级&#xff0c;生产过程从以前的企业内分工&#xff0c;转变为企业间分工。那么整个过程演变了如今的供应链管理流程&a…...

Qt之实现支持多选的QCombobox

一.效果 1.点击下拉列表的复选框区域 2.点击下拉列表的非复选框区域 二.实现 QHCustomComboBox.h #ifndef QHCUSTOMCOMBOBOX_H #define QHCUSTOMCOMBOBOX_H#include <QLineEdit> #include <QListWidget> #include <QCheckBox> #include <QComboBox>…...

【UI设计】Figma_“全面”快捷键

目录 1.快捷键与键位&#xff08;mac与windows&#xff09;2.基础快捷键3.操作区快捷键3.1视图3.2文字3.3选项3.4图层3.5组件 4.特殊技巧 Figma 是一个 基于浏览器 的协作式 UI 设计工具。【https://www.figma.com/】 Figma Sketch&#xff08;UI 设计&#xff09; InVision&a…...

计算机网络(谢希仁)第八版课后题答案(第一章)

1.计算机网络可以向用户提供哪些服务 连通性:计算机网络使上网用户之间可以交换信息&#xff0c;好像这些用户的计算机都可以彼此直接连通一样。 共享:指资源共享。可以是信息、软件&#xff0c;也可以是硬件共享。 2.试简述分组交换的要点 采用了存储转发技术。把报文(要发…...

argparse模块介绍

argparse是一个Python模块&#xff1a;命令行选项、参数和子命令解析器。argparse 模块可以让人轻松编写用户友好的命令行接口。程序定义了所需的参数&#xff0c;而 argparse 将找出如何从 sys.argv &#xff08;命令行&#xff09;中解析这些参数。argparse 模块还会自动生成…...

分布式、集群、微服务

分布式是以缩短单个任务的执行时间来提升效率的&#xff1b;而集群则是通过提高单位时间内执行的任务数来提升效率。 分布式是指将不同的业务分布在不同的地方。 集群指的是将几台服务器集中在一起&#xff0c;实现同一业务。 分布式中的每一个节点&#xff0c;都可以做集群…...

Android Studio的debug和release模式及签名配置

Android Studio的两种模式及签名配置 使用Android Studio 运行我们的app&#xff0c;无非两种模式&#xff1a;debug和release模式。 https://www.cnblogs.com/details-666/p/keystore.html...

【深蓝学院】手写VIO第8章--相机与IMU时间戳同步--笔记

0. 内容 1. 时间戳同步问题及意义 时间戳同步的原因&#xff1a;如果不同步&#xff0c;由于IMU频率高&#xff0c;可能由于时间戳不同步而导致在两帧camera之间的时间内用多了或者用少了IMU的数据&#xff0c;且时间不同步会导致我们首尾camera和IMU数据时间不同&#xff0c;…...

【Java集合类面试二十一】、请介绍TreeMap的底层原理

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a;请介绍TreeMap的底层原理…...

Go语言Channel

在本教程中&#xff0c;我们将讨论Channel以及 Goroutines 如何使用Channel进行通信。 什么是Channel Channel可以被认为是 Goroutine 用来进行通信的管道。与水在管道中从一端流向另一端的方式类似&#xff0c;可以使用Channel从一端发送数据并从另一端接收数据。 声明Chan…...

java 编译 引用 jar 包进行编译和执行编译后的class文件

编译java文件 javac -encoding UTF-8 -Djava.ext.dirs./ -d . ./FtpTest.java 执行编译class文件 java -Djava.ext.dirs./ com.util.FtpTest com.util为包路径...

Linux系统之部署Tale个人博客系统

Linux系统之部署Tale个人博客系统 一、Tale介绍1.1 Tale简介1.2 Tale特点 二、本地环境介绍2.1 本地环境规划2.2 本次实践介绍 三、检查本地环境3.1 检查本地操作系统版本3.2 检查系统内核版本 四、部署Tale个人博客系统4.1 下载Tale源码4.2 查看Tale源码目录4.3 查看安装脚本内…...

【跟小嘉学 Rust 编程】三十三、Rust的Web开发框架之一: Actix-Web的基础

系列文章目录 【跟小嘉学 Rust 编程】一、Rust 编程基础 【跟小嘉学 Rust 编程】二、Rust 包管理工具使用 【跟小嘉学 Rust 编程】三、Rust 的基本程序概念 【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念 【跟小嘉学 Rust 编程】五、使用结构体关联结构化数据 【跟小嘉学…...

算法通关村|黄金挑战|K个一组进行反转

K个一组进行反转 1.头插法 public ListNode reverseKGroup(ListNode head, int k) {ListNode dummyNode new ListNode(0);dummyNode.next head;ListNode cur head;// 计算链表长度int len 0;while (cur ! null) {len;cur cur.next;}// 计算有几组int n len / k;ListNod…...

【Android Studio】工程中文件Annotate with Git Blame 不能点击

问题描述 工程文件中想要查看代码提交信息但是相关按钮不可点击 解决方法 Android Studio -> Preferences -> Version Control-> 在Unregistered roots里找到你想要的工程文件 点击左上角➕号 然后右下角Apply即可...

Ant Design Vue

2222222222222...