循环依赖详解及解决方案

介绍

在这里插入图片描述

上图就是循环依赖的三种情况,虽然方式不同,但是循环依赖的本质是一样的,就A的完整创建要依赖与B,B的完整创建要依赖于A,相互依赖导致没办法完整创建造成失败.

循环依赖代码演示

public class Demo {

    public static void main(String[] args) {
        new Demo1();
    }
}

class Demo1{
    private Demo2 demo2 = new Demo2();
}

class Demo2 {
    private Demo1 demo1 = new Demo1();
}

在这里插入图片描述

上述代码就是最基本的循环依赖的场景,Demo1依赖Demo2,Demo2依赖Demo1,然后就报错了,而上面的这种设计情况是无解的.

分析问题

首先我们要明确一点就是如果这个对象A还没创建成功,在创建的过程中要依赖另一个对象B,而另一个对象B也是在创建中要依赖对象A,这种肯定是无解的,这时我们就要缓缓思路,我们先把A创建出来,但是还没有完成初始化操作,也就是这是一个半成品对象,然后再赋值的时候提前把A暴露出来,然后创建B,让B创建完成后找到暴露出来的A完成整体的实例化,这时再把B交给A完成A的后续操作.从而解决循环依赖,也就是下图:

在这里插入图片描述

代码解决

public class Demo {

    /**
     * 保存提前暴露的对象,也就是半成品对象
     */
    private final static Map<String, Object> singletonObjects = new ConcurrentHashMap<>();

    public static void main(String[] args) throws Exception {
        System.out.println(getBean(Demo1.class).getDemo2());
        System.out.println(getBean(Demo2.class).getDemo1());
    }

    private static <T> T getBean(Class<T> clazz) throws Exception {
        // 获取beanName
        String beanName = clazz.getName().toLowerCase();
        // 查找缓存中是否存在半成品对象
        if (singletonObjects.containsKey(beanName)) {
            return (T) singletonObjects.get(beanName);
        }
        // 缓存中不存在半成品对象,反射进行实例化
        T res = clazz.newInstance();
        // 将实例化后的对象储存到缓存
        singletonObjects.put(beanName, res);
        // 获取所有属性
        Field[] fields = res.getClass().getDeclaredFields();
        // 循环进行属性填充
        for (Field field : fields) {
            // 针对private修饰
            field.setAccessible(Boolean.TRUE);
            // 获取属性类型
            Class<?> fieldClazz = field.getType();
            // 获取属性beanName
            String filedBeanName = fieldClazz.getName().toLowerCase();
            // 属性填充,查找缓存是否有对应属性,没有就递归调用
            field.set(res, singletonObjects.containsKey(filedBeanName) ? singletonObjects.get(filedBeanName) : getBean(fieldClazz));
        }
        return res;
    }
}

class Demo1 {
    private Demo2 demo2;

    public Demo2 getDemo2() {
        return demo2;
    }

    public void setDemo2(Demo2 demo2) {
        this.demo2 = demo2;
    }
}

class Demo2 {
    private Demo1 demo1;

    public Demo1 getDemo1() {
        return demo1;
    }

    public void setDemo1(Demo1 demo1) {
        this.demo1 = demo1;
    }
}

在这里插入图片描述

在上面的方法中核心就是getBean方法,Demo1创建后填充属性时依赖Demo2,那么就去创建Demo2,在创建Demo2开始填充时发现依赖Demo1,但此时Demo1这个半成品对象已经放在缓存singletonObjects中了,所以Demo2正常创建,再结束递归把Demo1也创建完整了.
在这里插入图片描述

Spring循环依赖

针对Spring中Bean对象的各种场景,支持的方案不一样

  • 单例
    • 构造注入:无解,避免栈溢出,需要检测是否存在循环依赖的情况,如果有直接抛异常
    • 设值注入:三级缓存–>提前暴露
  • 原型
    • 构造注入:无解,避免栈溢出,需要检测是否存在循环依赖的情况,如果有直接抛异常
    • 设置注入:不支持循环依赖

Spring是如何解决循环依赖问题的?上述代码中对象的生命周期就两个:创建对象和属性填充,而Spring涉及到对象生命周期的方法就很多了,简单举例,如下图:

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

上面就是Spring的生命周期方法和循环依赖出现相关的流程了.下面就是放入三级缓存的源码:

/**
     * 添加对象到三级缓存
     *
     * @param beanName
     * @param singletonFactory
     */
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    // 确保singletonFactory不为null
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    // 使用singletonObjects进行加锁,保证线程安全
    synchronized (this.singletonObjects) {
        //如果singletonObjects缓存中没有该对象
        if (!this.singletonObjects.containsKey(beanName)) {
            // 将对象放置到singletonFactories(三级缓存)中
            this.singletonFactories.put(beanName, singletonFactory);
            // 从earlySingletonObjects(二级缓存)中移除该对象
            this.earlySingletonObjects.remove(beanName);
            // 将beanName添加到已经注册的单例集中
            this.registeredSingletons.add(beanName);
        }
    }
}

放入二级缓存的源码:

/**
     * 返回在给定名称下注册的(原始)单例对象.检查已经实例化的单例,并允许对当前创建的单例进行早期引用(解决循环引用)
     *
     * @param beanName
     * @param allowEarlyReference
     * @return
     */
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 不需要完全获取单例锁的情况下快速检查现有实例
    Object singletonObject = this.singletonObjects.get(beanName);
    // 如果单例对象为空,并且当前单例正在创建中,则尝试获取早期单例对象
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        singletonObject = this.earlySingletonObjects.get(beanName);
        // 如果早期单例对象为空,并且允许早期引用,则再完全获取单力所的情况下创建早期单例对象
        if (singletonObject == null && allowEarlyReference) {
            synchronized (this.singletonObjects) {
                // 检查早期单例对象是否存在
                singletonObject = this.singletonObjects.get(beanName);
                // 如果早期对象仍然为空则创建单例对象
                if (singletonObject == null) {
                    // 从二级缓存获取
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        // 获取不到对象从三级缓存中获取
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            singletonObject = singletonFactory.getObject();
                            // 获取到添加到二级缓存并从三级缓存中移除该对象
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}

放入一级缓存中的源码:

/**
     * 将单例对象添加到一级缓存
     *
     * @param beanName
     * @param singletonObject
     */
protected void addSingleton(String beanName, Object singletonObject) {
    // 使用singletonObjects进行加锁,保证线程安全
    synchronized (this.singletonObjects) {
        // 将映射关系添加到一级缓存
        this.singletonObjects.put(beanName, singletonObject);
        // 从三级缓存;二级缓存中移除该对象
        this.singletonFactories.remove(beanName);
        this.earlySingletonObjects.remove(beanName);
        // 将beanName添加到已经注册的单例集中
        this.registeredSingletons.add(beanName);
    }
}

总结

三级缓存分别有什么作用

  1. singletonObjects:缓存经过了完整生命周期的bean
  2. earlySingletonObjects:缓存未经过完整生命周期的bean,如果某个bean出现了循环依赖,就会提前把这个暂时未经过完整生命周期的bean放入earlySingletonObjects中,如果这个bean要经过AOP,那么就会把代理对象放入到earlySingletonObjects中,否则就是把原始对象放入earlySingletonObjects,但是不管怎么样就是代理对象,代理对象所代理的原始对象也是没有经过完整生命周期的,所以放入earlySingletonObjects我们就可以统一认为是未经过完整生命周期的bean
  3. singletonFactories:缓存的是一个ObjectFactory,也就是一个Lambda表达式,在每个bean的生成过程中,经过实例化得到一个原始对象后,都会提前基于原始对象暴露一个Lambda表达式,并保存到三级缓存中,这个Lambda表达式可能用到,也可能用不到, 如果当前bean没有出现循环依赖,那么这个Lambda表达式就没有用,当前bean按照自己的生命周期正常执行,执行完直接把当前bean放入singletonObjects中,如果当前bean在依赖注入时出现了循环依赖,则从三级缓存中拿到Lambda表达式,并执行Lambda表达式得到一个对象,并把得到的对象放入到二级缓存(如果当前bean需要AOP,那么执行Lambda表达式,得到的就是对应的代理对象,如果无需AOP,则直接得到一个原始对象)
  4. 其实还要一个缓存,用来记录某个原始对象是否进行过AOP了

为什么需要三级缓存

如果A的原始对象注入给B的属性之后,A的原始对象进行了AOP产生了一个代理对象,此时就会出现,对于A而言,它的bean对象应该是AOP之后的代理对象,而B的a属性对应的不是AOP之后的代理对象,这就产生了冲突,B依赖的A和最终的A不是同一个对象,三级缓存主要处理的是AOP的代理对象,存储的是一个ObjectFactory

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

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

相关文章

打造出ChatGPT的,是怎样一群人?

震惊世界的ChatGPT&#xff0c;要多少人才能开发出来&#xff1f;几百&#xff0c;还是几千&#xff1f; 答案是&#xff1a;87个人。 老实说&#xff0c;刚看到这个数字真是惊到我了&#xff0c;印象里&#xff0c;之前看媒体报道各大巨头人工智能人才储备时&#xff0c;动辄…

长草

4 5 .g… … …g… … 2 gggg. gggg. ggggg .ggg. #include <iostream> using namespace std;int r,l;char arr[1005][1005];int t;char dp[1005][1005]; int dx[4]{0,0,1,-1},dy[4]{1,-1,0,0}; void dfs(int x,int y) {for(int i0;i<4;i){int axdx[i];int bydy[i];if…

Qt Quick - 分隔器综述

Qt Quick - 分隔器综述一、概述二、MenuSeparator 控件1. 用法&#xff1a;三、ToolSeparator 控件1. 用法一、概述 Qt Quick Controls 提供了多种分隔符&#xff0c;其实就是分割一下MenuBar和ToolBar里面的内容。 控件功能MenuSeparator将菜单中的一组项目与相邻项目分开To…

dolphinscheduler资源中心

资源中心 资源中心介绍 资源中心提供文件管理&#xff0c;UDF管理&#xff0c;任务组管理。 文件管理可以访问要执行的hive的sql文件 UDF管理可以放置fllink执行的自定义udf函数jar包&#xff0c;hive自定义的UDF函数jar包 以上的*.sql,*.jar文件可以理解为资源&#xff0c…

【服务器数据恢复】 重装系统导致xfs文件系统分区丢失的数据恢复案例

服务器数据恢复环境&#xff1a; EMC某型号存储&#xff0c;20块磁盘组建raid5磁盘阵列&#xff0c;划分2个lun。 服务器故障&#xff1a; 管理员执行重装系统操作后发现分区发生改变&#xff0c;原先的sdc3分区丢失&#xff0c;该分区采用xfs文件系统&#xff0c;存储了公司重…

红队内网靶场

文章目录开篇介绍靶场介绍靶场下载以及配置Tomcat Get Shell突破DMZ防火墙拿下域内成员机器将内网机器上线到CS使用Adfind侦察子域信息控制子域DCRadmin登录子域进行权限维持(白银票据/ACL)子域bloodhound获取父域信息分析子域Krbtgt密钥创建跨域金票Dcsync父域PTH父域DC准备打…

什么是 三维渲染内核?

一、引言 随着计算机图形学的发展&#xff0c;三维图形已经成为 电子游戏、动画电影 和 可视化、数字孪生等领域的关键技术。为了将三维模型转换成二维图像&#xff0c;我们需要依赖一个称为三维渲染内核的工具。本文将详细介绍三维渲染内核的原理、实现方法和应用&#xff0c…

每日做题总结——day01

目录 选择题 for循环 指针数组 位段 getchar 大小端存储 进制与格式控制符 位运算 数组指针 二维数组的存储 计算二进制中1的个数 斐波那契数列求递归次数 编程题 删除公共字符 排序子序列 倒置字符串 选择题 for循环 解析&#xff1a;该题主要看for…

面试题React

1.React Fiber是什么&#xff1f; 在 React V16 将调度算法进行了重构&#xff0c; 将之前的 stack reconciler 重构成新版的 fiber reconciler&#xff0c;变成了具有链表和指针的 单链表树遍历算法。通过指针映射&#xff0c;每个单元都记录着遍历当下的上一步与下一步&…

【从零开始学Skynet】工具篇(二):虚拟机文件的复制粘贴

大家在Linux系统下开发的时候肯定会遇到虚拟机与主机间无法复制粘贴的问题&#xff0c;现在我们就来解决这样的问题&#xff0c;方便我们的开发。 1、打开设置 我们可以系统界面的菜单栏点击“控制”&#xff0c;然后打开“设置”&#xff1b; 也可以在VirtualBox界面打开“设…

项目管理中,这些思维误区一定要避开

项目需要在限定的时间要求完成的事情&#xff0c;可控的关键把握是&#xff1a;人、时、事。 但是&#xff0c;项目实施时间一般较长&#xff0c;总有很多项目实施结果不尽人意。那么&#xff0c;IT项目管理过程中&#xff0c;容易出现哪些思维误区呢&#xff1f; 1、忘记项…

TCP三次握手四次挥手及time_wait状态解析

TCP的建立——三次握手 1.服务器必须准备好接受外来的连接。通常通过调用socket&#xff0c;bind&#xff0c;listen这三个函数来完成&#xff0c;我们称之为被动打开(passive open)。 2. 客户端通过调用connect函数发起主动的打开(active open)。这导致客户TCP发送一个SYN(同步…

Nginx基础教程

Nginx 目标 Nginx简介【了解】 Nginx安装配置【掌握】 一、Nginx简介 Nginx称为:负载均衡器或 静态资源服务器:html,css,js,img ​ Nginx(发音为“engine X”)是俄罗斯人编写的十分轻量级的HTTP服务器,是一个高性能的HTTP和反向代理服务器&#xff0c;同时也是一个IMAP/P…

初探MyBatis实现简单查询

文章目录一、创建数据库与表1、创建数据库2、创建用户表3、添加表记录二、基于配置文件方式使用MyBatis1、创建Maven项目2、添加相关依赖3、创建用户实体类4、创建用户映射器配置文件5、创建MyBatis配置文件6、创建日志属性文件7、测试用户操作1)创建用户操作测试类2)测试按编号…

除了Jira、禅道还有哪些更好的敏捷开发过程管理平台?

无论是从国内的敏捷调研开发调研报告还是从国外的敏捷状态调查&#xff0c;工具支持一直是决定敏捷成功的关键因素之一&#xff0c;它们可以帮助团队提高软件开发的效率、质量、协作和满意度。选择合适的敏捷开发管理工具&#xff0c;并正确地使用它们&#xff0c;是每个敏捷团…

数字孪生(1)

目前接触的客户群体是做大屏展示&#xff0c;闲鱼上5元包邮的那种科技感前端&#xff08;不好意思我买了&#xff09;各路模型大整合 实景GISiOT&#xff0c;如果再来点动画就好&#xff0c;然满屏动起来&#xff0c;火灾烧起来&#xff0c;水面荡漾起来&#xff0c;工程车开起…

C/C++每日一练(20230414)

目录 1. 寻找峰值 &#x1f31f;&#x1f31f; 2. 相同的树 &#x1f31f; 3. 整数反转 ※ &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 1. 寻找峰值 峰值元素是指其值严格大于左右…

如何借助ChatGPT,自动批量产出短视频爆款文案

如何借助chatgpt批量出爆款文案。 这里我们首先得认识并了解到爆款文案的逻辑。 共通性是打动人&#xff0c;去原创的话&#xff0c;文案能否火&#xff0c;纯靠天吃饭。 所以我们让chatgpt去自己写原创短视频文案&#xff0c;那么chatgpt大概率自由发挥&#xff0c;我们也不…

国产化ChatGPT来袭,景联文科技提供专业数据采集标注服务,人手一个专属ChatGPT或成为可能

ChatGPT作为一个颠覆性的创新&#xff0c;现已成为火爆全球的智能应用。 自ChatGPT爆火以来&#xff0c;国内科技圈开始频频发力&#xff0c;多家科技和互联网公司纷纷表示将开发出中国本土化的ChatGPT。 以百度为例&#xff0c;3月16日&#xff0c;百度推出新一代知识增强大语…

【Linux】页表的深入分析

上一篇文章介绍了线程的基本概念 而本篇文章我们来深入理解一下, CPU再调度我们以往理解的进程和如今的线程都会涉及到的一个内容: 页表 文章目录深入理解页表 *页表的实际组成*什么是page&#xff1f;深入理解页表 * 在介绍进程时, 博主没有深入介绍过页表. 只是简单说了 页…