Spring的循环依赖问题

文章目录

  • 1.什么是循环依赖
  • 2.代码演示
  • 3.分析问题
  • 4.问题解决
  • 5.Spring循环依赖
  • 6. 疑问点
    • 6.1 为什么需要三级缓存
    • 6.2 没有三级缓存能解决吗?
    • 6.3 三级缓存分别什么作用

1.什么是循环依赖

image-20231108002206734

上图是循环依赖的三种情况,虽然方式有点不一样,但是循环依赖的本质是一样的,就你的完整创建要依赖与我,我的完整创建也依赖于你。相互依赖从而没法完整创建造成失败。

2.代码演示

public class CircularTest {
    public static void main(String[] args) {
        // 出现了循环依赖的情况,死循环-OOM
        new CircularServiceA();
    }
}

class CircularServiceA {
    // A 中依赖了 B
    private CircularServiceB circularServiceB = new CircularServiceB();
}

class CircularServiceB {
    // B 中依赖了 A
    private CircularServiceA circularServiceA = new CircularServiceA();
}

执行后出现了 StackOverflowError 错误:

image-20231108002757556

上面的就是最基本的循环依赖的场景,你需要我,我需要你,然后就报错了。而且上面的这种设计情况我们是没有办法解决的。那么针对这种场景我们应该要怎么设计呢?这个是关键!

3.分析问题

首先我们要明确一点就是如果这个对象 A 还没创建成功,在创建的过程中要依赖另一个对象 B,而另一个对象 B 也是在创建中要依赖对象 A,这种肯定是无解的。

这时我们就要转换思路,我们先把 A 创建出来,但是还没有完成初始化操作,也就是这是一个半成品的对象,然后在赋值的时候先把 A 暴露出来,然后创建B,让 B 创建完成后找到暴露的 A 完成整体的实例化,这时再把 B 交给 A,完成 A 的后续操作,从而揭开了循环依赖的密码。

image-20231108012150662

4.问题解决

明白了上面的本质后,我们可以自己来尝试解决下。先来把上面的案例改为 set/get 来依赖关联,然后我们再通过把对象实例化和成员变量赋值拆解开来处理。从而解决循环依赖的问题。

public class CircularTest {
    public static void main(String[] args) throws Exception {
        // 需要把构造方法和属性赋值作为一个整体,需要提供一个获取实例对象的方法
        System.out.println(getBean(CircularServiceA.class).getCircularServiceB()); // com.zhulang.circular.CircularServiceB@74a14482
        System.out.println(getBean(CircularServiceB.class)); // com.zhulang.circular.CircularServiceB@74a14482
    }

    // 存储半成品的容器,解决半成品的关键点
    private static final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();

    /**
     * 根据类型获取对应的实例对象
     * 1.完成构造
     * 2.完成成员变量的赋值
     *
     * @param className
     * @param <T>
     * @return
     * @throws Exception
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(Class<T> className) throws Exception {
        // 1.获取类对象对应的名称
        String beanName = className.getSimpleName().toLowerCase();

        // 2.根据名称去 singletonObjects 中查看是否有半成品的对象
        if (singletonObjects.containsKey(beanName)) {
            return (T) singletonObjects.get(beanName);
        }

        // 3.singletonObjects 没有半成品的对象,那么就反射实例化对象
        T t = className.newInstance();

        // 4.把这个半成品对象存储在 singletonObjects 中
        singletonObjects.put(beanName, t);

        // 5.获取所有的成员变量
        Field[] declaredFields = className.getDeclaredFields();

        // 6.遍历成员变量,依次赋值
        for (Field field : declaredFields) {
            // 6.1 进行爆破,针对 private 修饰的对象
            field.setAccessible(true);
            // 6.2 获取成员变量 对应的类对象
            Class<?> fieldType = field.getType();
            // 6.3 给成员变量赋值 如果 singletonObjects 中有半成品就获取,否则创建对象
            field.set(t, getBean(fieldType));
        }
        return t;
    }
}

class CircularServiceA {
    // A 中依赖了 B
    private CircularServiceB circularServiceB;

    public CircularServiceB getCircularServiceB() {
        return circularServiceB;
    }

    public void setCircularServiceB(CircularServiceB circularServiceB) {
        this.circularServiceB = circularServiceB;
    }
}

class CircularServiceB {
    // B 中依赖了 A
    private CircularServiceA circularServiceA;

    public CircularServiceA getCircularServiceA() {
        return circularServiceA;
    }

    public void setCircularServiceA(CircularServiceA circularServiceA) {
        this.circularServiceA = circularServiceA;
    }
}

在上面的方法中的核心是 getBean 方法,A 创建后填充属性时依赖 B,那么就去创建 B,在创建 B 开始填充时发现依赖于 A,但此时 A 这个半成品对象已经存放在缓存到 singletonObjects 中了,所以 B 可以正常创建,在通过递归把 A 也创建完整了。

最后总结下该案例解决的本质:

image-20231108013756569

5.Spring循环依赖

刚刚上面的案例中的对象的生命周期的核心就两个:

  1. 创建对象
  2. 属性填充

然后我们再来看看 Spring 中是如何解决循环依赖问题的呢?Spring 创建 Bean 的生命周期中涉及到的方法就很多了。下面是简单列举了对应的方法。

image-20231108014924282

基于前面案例的了解,我们知道肯定需要在调用构造方法方法创建完成后再暴露对象,在 Spring 中提供了三级缓存来处理这个事情,对应的处理节点如下图:

image-20231108015452529

  • 一级缓存:存储的是 成品Bean 对象 ,存储的所有的单例对象,其实可以说和循环依赖没有关系。

  • 二级缓存:存储的是 半成品对象,是解决循环依赖的关键,如果不去考虑 AOP 代理增加的情况,只有二级缓存的情况下也是可以解决循环依赖的,也就是不需要三级缓存。

  • 三级缓存:三级缓存存在的意义是解决 AOP 增强对象的原因,存储的是一个 Lambda 表达式(内部类)–> ObjectFactory。

对应到源码中具体处理循环依赖的流程如下:

image-20231110084404766

上面就是在Spring的生命周期方法中和循环依赖出现相关的流程了。那么源码中的具体处理是怎么样的呢?我们继续往下面看。

首先在调用构造方法的后会放入到三级缓存中

image.png

下面就是放入三级缓存的逻辑

	protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
		Assert.notNull(singletonFactory, "Singleton factory must not be null");
		// 使用singletonObjects进行加锁,保证线程安全
		synchronized (this.singletonObjects) {
			// 如果单例对象的高速缓存【beam名称-bean实例】没有beanName的对象
			if (!this.singletonObjects.containsKey(beanName)) {
				// 将beanName,singletonFactory放到单例工厂的缓存【bean名称 - ObjectFactory】
				this.singletonFactories.put(beanName, singletonFactory);
				// 从早期单例对象的高速缓存【bean名称-bean实例】 移除beanName的相关缓存对象
				this.earlySingletonObjects.remove(beanName);
				// 将beanName添加已注册的单例集中
				this.registeredSingletons.add(beanName);
			}
		}
	}

然后在填充属性的时候会存入二级缓存中

earlySingletonObjects.put(beanName,bean);
registeredSingletons.add(beanName);

最后把创建的对象保存在了一级缓存中

	protected void addSingleton(String beanName, Object singletonObject) {
		synchronized (this.singletonObjects) {
			// 将映射关系添加到单例对象的高速缓存中
			this.singletonObjects.put(beanName, singletonObject);
			// 移除beanName在单例工厂缓存中的数据
			this.singletonFactories.remove(beanName);
			// 移除beanName在早期单例对象的高速缓存的数据
			this.earlySingletonObjects.remove(beanName);
			// 将beanName添加到已注册的单例集中
			this.registeredSingletons.add(beanName);
		}
	}

6. 疑问点

6.1 为什么需要三级缓存

三级缓存主要处理的是 AOP 的代理对象,存储的是一个 ObjectFactory。

三级缓存考虑的是带你对象,而二级缓存考虑的是性能-从三级缓存的工厂里创建出对象,再扔到二级缓存(这样就不用每次都要从工厂里拿)。

6.2 没有三级缓存能解决吗?

没有三级缓存是可以解决循环依赖问题的。

6.3 三级缓存分别什么作用

一级缓存:正式对象

二级缓存:半成品对象

三级缓存:工厂

在 Spring 框架中,singletonObjects、earlySingletonObjects 和 singletonFactories 是三个不同的数据结构,用于管理单例 Bean 的创建和缓存。

  • singletonObjects:该数据结构是一个哈希表,以 Bean 名称为键,存储已经完全初始化的单例 Bean 实例。当我们通过ApplicationContext.getBean() 方法请求获取一个单例 Bean 时,Spring 首先会从 singletonObjects 中查找是否存在该 Bean的实例,如果存在,则直接返回;如果不存在,则创建一个新的实例,并将其添加到 singletonObjects 中。

  • earlySingletonObjects:该数据结构也是一个哈希表,以 Bean 名称为键,存储正在创建过程中但尚未完全初始化的单例 Bean 实例。当 Spring 创建一个单例 Bean 时,它会先将其实例化并放入 earlySingletonObjects 中。在 Bean 的创建过程中,如果其他 Bean 有对该 Bean 的循环引用,就会出现循环依赖的情况,此时 Spring 会从 earlySingletonObjects 中获取到该 Bean 的早期实例,以解决循环依赖的问题。待 Bean 创建完成后,Spring 会将其从 earlySingletonObjects 移除,并放入 singletonObjects 中。

  • singletonFactories:该数据结构是一个哈希表,以 Bean 名称为键,存储用于创建单例 Bean 实例的工厂对象。这是真正打破循环依赖的 Map,缓存的是 ObjectFactory,也就是 Lambda 表达式,在每个 Bean 的生成过程中,经过实例化得到一个原始对象后,都会提前基于原始对象暴露一个 Lambda 表达式,并保存在三级缓存中。这个 Lambda 表达式可能用到,也可能用不到,如果当前 Bean 没有出现循环依赖,那么这个 Lambda 表达式没用,当前 bean 按照自己的生命周期正常执行,执行完后直接把当前 bean 放入 singletonObjects 中。如果当前 bean 在依赖注入时发现出现了循环依赖(当前正在创建的 bean 被其它 bean 依赖了),则从三级缓存中拿到 Lambda 表达式,并执行 Lambda 表达式得到一个对象,把得到的对象放入二级缓存。如果当前 bean 需要 AOP,那么执行 Lambda 表达式得到的是对应的代理对象,如果无需 AOP,则直接得到一个原始对象。

综上所述,singletonObjects 用于缓存已完全初始化的单例 Bean 实例,earlySingletonObjects 用于缓存正在创建中的单例 Bean 实例,singletonFactories 则是用于缓存用于创建单例 Bean 实例的 Factory 对象。这三个数据结构共同协作,确保了单例 Bean 的正确创建和管理。

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

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

相关文章

一个不用充钱也能让你变强的 VSCode 插件!!!

今天给大家推荐一款不用充钱也能让你变强的 vscode 插件 通义灵码&#xff08;TONGYI Lingma&#xff09;&#xff0c;可以称之为 copilot 的替代甜品 &#x1f4aa; 前言 之前一直使用的 GitHub Copilot&#xff0c;虽然功能强大&#xff0c;但是收费相对来说有点贵&#xf…

HTTParty库数据抓取代码示例

使用HTTParty库的网络爬虫程序&#xff0c; ruby require httparty # 设置服务器 proxy_host proxy_port # 使用HTTParty库发送HTTP请求获取网页内容 response HTTParty.get(/, :proxy > { :host > proxy_host, :port > proxy_port }) # 打印获取的网页内容 …

【Python深入学习】- 书籍推荐|数据结构和算法介绍|内建集合数据类型

&#x1f308;个人主页: Aileen_0v0 &#x1f525;系列专栏:PYTHON学习系列专栏 &#x1f4ab;"没有罗马,那就自己创造罗马~" 若把编写代码比作行军打仗&#xff0c;那么要想称霸沙场&#xff0c;不能仅靠手中的利刃&#xff0c;还需深谙兵法。Python是一把利刃&…

解决:AttributeError: ‘WebDriver‘ object has no attribute ‘find_element_by_id‘

解决&#xff1a;AttributeError: ‘WebDriver’ object has no attribute ‘find_element_by_id’ 背景 在使用之前的代码通过selenium定位元素时&#xff0c;报错&#xff1a;selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to loca…

无线测温系统在电厂的必要性,保障电力系统稳定运行

安科瑞电气股份有限公司 上海嘉定 201801 摘要&#xff1a;采集关键电力设备接电的实时温度&#xff0c;克服有线温度监测系统存在的诸如线路多&#xff0c;布线复杂&#xff0c;维护困难等不足&#xff0c;将无线无源传感器与Zigbee无线通信技术相结合&#xff0c;将物联网技…

如何在Visual Studio上创建项目并运行【超级详细】

工欲善其事&#xff0c;必先利其器。想要学好编程&#xff0c;首先要把手中的工具利用好&#xff0c;今天小编教一下大家如何在史上最强大的编译器--Visual Studio上创建项目。&#x1f357; 一.打开编译器&#x1f357; 双击你电脑上的vs&#xff0c;(2012,2019,2022)都行。&…

jQuery中淡入与淡出

在我们jQuery中为我们封装了很多好玩的方法&#xff0c;我为大家介绍一下淡入与淡出&#xff01; 我们需要配合事件来玩淡入淡出 淡出语法&#xff1a;fadeOut([speed,[easing],[fn]) (1)参数都可以省略 (2)speed:三种预定速度之一的字符串(“slow”“normal”or “fast”)或…

实战!工作中常用的设计模式

文章目录 前言一、策略模式1.1、 业务场景1.2 、策略模式定义1.3、 策略模式使用1.3.1、一个接口&#xff0c;两个方法1.3.2、不同策略的差异化实现1.3.3、使用策略模式 二、责任链模式2.1、业务场景2.2、责任链模式定义2.3、责任链模式使用2.3.1、一个接口或者抽象类2.3.2、每…

计网自顶向下(Web服务器+UDPping+邮件客户端)

目录 &#x1f416;前言 &#x1f33c;Web服务器(作业1) &#x1f333;过程 &#x1f333;解释 &#x1f525;代码 &#x1f33c;UDPping程序(作业2) &#x1f333;过程 &#x1f333;解释 Client Server 整体逻辑 &#x1f525;代码 &#x1f33c;邮件客户端(作业…

Postman汉化教程

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 Postman汉化教程 前言 前言 Postman是一款支持http协议的接口调试与测试工具&#xff0c;其主要特点就是功能强大&#xff0c;使用简单且易用性好 。无论是开发人员进行接口…

护眼灯买什么样的好?好用又实惠的护眼台灯推荐

护眼台灯的光照一般比较均匀&#xff0c;相比普通台灯&#xff0c;一般具有防蓝光、防频闪等功能&#xff0c;能够提供一个健康舒适的学习、生活灯光环境&#xff0c;建议选购内置智能感光模式的护眼台灯&#xff0c;以确保灯光亮度一直处于均衡状态&#xff0c;让眼睛更轻松。…

【Spring】AOP实现原理

注册AOP代理创建器 在平时开发过程中&#xff0c;如果想开启AOP&#xff0c;一般会使用EnableAspectJAutoProxy注解&#xff0c;这样在启动时&#xff0c;它会向Spring容器注册一个代理创建器用于创建代理对象&#xff0c;AOP使用的是AnnotationAwareAspectJAutoProxyCreator&…

计算机考研精炼1000题:笔试面试必备攻略

&#x1f482; 个人网站:【工具大全】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 ⭐️ 好书推荐 计算机考…

Qt5多线程<12>

1.多线程的简单实现 <1> 在头文件threaddlg.h声明用于界面显示所需的控件,其代码如下: #ifndef THREADDLG_H #define THREADDLG_H#include <QDialog> #include <QPushButton> #include "workthread.h" #define MAXSIZE 5class ThreadDlg : publ…

2023年的低代码:数字化、人工智能、趋势及未来展望

前言 正如许多专家预测的那样&#xff0c;低代码平台在2023年将展现更加强劲的势头。越来越多的企业正在纷纷转向低代码开发&#xff0c;他们希望能够快速开发内部应用程序&#xff0c;并在经济衰退可能出现的情况下保持灵活性。在这个大背景下&#xff0c;低代码平台为企业软件…

各大电商平台关于预制菜品种酸菜鱼销售量

# 导入需要的包 library(rvest) # 用于网页抓取 library(tidyverse) # 用于数据处理 library(stringr) # 用于字符串处理# 设置代理信息 proxy_host <- "www.duoip.cn" proxy_port <- 8000# 设置要爬取的网页 url <- "https://jshk.com.cn/products/sa…

【趣味随笔】YOLO的“进化史”极简版(YOLO v1-->YOLOP)

&#x1f4e2;&#xff1a;如果你也对机器人、人工智能感兴趣&#xff0c;看来我们志同道合✨ &#x1f4e2;&#xff1a;不妨浏览一下我的博客主页【https://blog.csdn.net/weixin_51244852】 &#x1f4e2;&#xff1a;文章若有幸对你有帮助&#xff0c;可点赞 &#x1f44d;…

httpRequest库代码示例

python # 首先导入所需的库 library(httpRequest) # 设置主机名和端口号 proxy_host <- proxy_port <- # 使用httpRequest库的get函数下载图片 response <- httpRequest(", proxyHost proxy_host, proxyPort proxy_port) # 确保请求成功 if (response$sta…

bat脚本批量修改文件名称的方法,以及乱码问题解决

当有大量的文件&#xff0c;需要按照对应数据进行文件名称的修改时候&#xff0c;用bat脚本&#xff0c;是一个快速&#xff0c;并且简单的方法。 使用命令 “"ren "&A2&" "&D2&""” 其中A2为需要修改的单元格&#xff0c;D2为…

ORA-00257: Archiver error. Connect AS SYSDBA only until resolved错误解决

错误的原因&#xff1a;是因为服务器分配空间不足&#xff0c;数据库归档日志满导致系统数据库登陆失败。 解决办法&#xff1a;1.删除以前的日志 2.增大归档日志的容量 3.关闭归档模式 一、删除以前的容量 1.登录账号后&#xff0c;查看ORACLE_BASE目录 【oraclelocalhost~】$…
最新文章