循环依赖详解及解决方案

news2024/11/25 14:35:57

介绍

在这里插入图片描述

上图就是循环依赖的三种情况,虽然方式不同,但是循环依赖的本质是一样的,就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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

电子信息工程有哪些SCI期刊推荐? - 易智编译EaseEditing

以下是电子信息工程领域的一些SCI期刊推荐&#xff1a; IEEE Transactions on Information Theory&#xff1a; 该期刊由IEEE出版&#xff0c;专注于信息理论领域的研究&#xff0c;包括编码理论、信道编码、信息传输、信息论应用等方面的研究。 IEEE Transactions on Signal…

Apache网页与安全优化

系列文章目录 文章目录系列文章目录一、1.构建虚拟web主机2.一、基于域名的虚拟主机二、Apache 日志分割1.三、Apache的网页优化总结一、 1.构建虚拟web主机 虚拟Web主机指的是在同一台服务器中运行多个Web站点&#xff0c;其中每一个站点实际上并不独立占用整个服务器&#…

天选姬 - 桌面宠物

天选姬 - 桌面宠物前言下载使用更新设置右键菜单人机交互系统状态闹钟壁纸前言 桌面宠物顾名思义指在电脑桌面的宠物&#xff0c;可以是各种动物或Q版人物。可以进行交互并拥有各种各样的功能&#xff0c;本文介绍一款适用于各种电脑的桌面宠物&#xff0c;天选姬&#xff0c;…

【Redis-面试题及持久化方案】Redis相关面试题(缓存穿透、缓存击穿、缓存血崩) Redis两种持久化方案详情对比(RDB、AOF)

【Redis-面试题及持久化方案】Redis相关面试题&#xff08;缓存穿透、缓存击穿、缓存血崩&#xff09; & Redis两种持久化方案详情对比&#xff08;RDB、AOF&#xff09;1&#xff09;Redis 面试题1.1.高频面试题&#xff1a;缓存穿透、缓存击穿、缓存血崩1.2.低频面试题&a…

电脑0X000000D1蓝屏错误U盘重新安装系统教学

电脑0X000000D1蓝屏错误U盘重新安装系统教学。最近有用户遇到了电脑桌面变成了0X000000D1错误代码的蓝屏界面了&#xff0c;无法继续操作使用。那么这个问题怎么去进行系统U盘重装呢&#xff1f;来看看以下的详细解决方法吧。 准备工作&#xff1a; 1、U盘一个&#xff08;尽量…

智慧城市我国发展现状怎样?

智慧城市从概念提出到落地实践&#xff0c;历经十多年的建设与发展&#xff0c;我国智慧城市建设数量持续增长。我国智慧城市整体建设和发展情况总结如下。 北京智汇云舟科技有限公司成立于2012年&#xff0c;专注于创新性的“视频孪生(实时实景数字孪生)”技术研发与应用。目前…

江南爱窗帘十大品牌,怎么合理的搭配窗帘配色

窗帘行业圈&#xff1a;窗帘行业内部交流圈&#xff0c;窗帘从业者的交流内部圈。 当阳光照进房间的那一刻&#xff0c; 光线给空间带来了无限的可能。 窗边的帘帐既是美丽的风景 又是可爱的魔术师。 在光影变幻的时空里 让你的生活布满温馨和奇幻。 1.窗帘材质怎么选 窗帘的材…

DataSecurity Plus如何阻止数据泄露

随着互联网的发展&#xff0c;数据泄露事件时有发生&#xff0c;给个人和企业造成了巨大的损失。为了保护数据安全&#xff0c;企业需要采取一系列的安全措施&#xff0c;而其中一项重要的措施就是使用数据安全工具。DataSecurity Plus 是一款功能强大的数据安全工具&#xff0…

苹果AirPods耳机推送新固件更新,TWS耳机与Find My可实现智能防丢

苹果推送 iOS / iPadOS 16.5 Beta 2 更新的同时&#xff0c;原本还为 AirPods 耳机推送了固件更新 5E133。苹果在临时撤回之后&#xff0c;再次向用户推送了本次更新&#xff0c; 苹果官方表示当你的 AirPods 在充电且在 iPhone、iPad 或 Mac 的蓝牙通信范围内时&#xff0c;…

【LeetCode:(每日一题1023. 驼峰式匹配) -> 剑指 Offer II 097. 子序列的数目 | 暴力递归=>记忆化搜索=>动态规划】

&#x1f34e;作者简介&#xff1a;硕风和炜&#xff0c;CSDN-Java领域新星创作者&#x1f3c6;&#xff0c;保研|国家奖学金|高中学习JAVA|大学完善JAVA开发技术栈|面试刷题|面经八股文|经验分享|好用的网站工具分享&#x1f48e;&#x1f48e;&#x1f48e; &#x1f34e;座右…

iOS 吸顶效果

项目中&#xff0c;在列表向上滚动时&#xff0c;有时需要将某个控件置顶&#xff0c;这就是我们常见的吸顶效果。 1. UITableView 吸顶效果 UITableView是自带吸顶效果&#xff0c;我们把需要置顶的控件设置为SectionHeaderView&#xff0c;这样在滚动时&#xff0c;该控件会…

360城市全景,开启全新城市探索方式

引言 360城市全景技术的出现和发展&#xff0c;让我们的生活方式和观念发生了革命性的变化。在过去&#xff0c;我们只能通过实地旅行、图片和视频来了解不同城市的文化、建筑和风景。现在&#xff0c;通过360城市全景技术&#xff0c;我们可以像亲临现场一样&#xff0c;透过…

Vue实现自动化平台(四)--接口管理页面的实现

上一章&#xff1a; Vue实现自动化平台&#xff08;三&#xff09;_做测试的喵酱的博客-CSDN博客 github地址&#xff1a;https://github.com/18713341733/vuemiaotest 这个目前只是用来练手的&#xff0c;项目还没成型。等以后我写完了&#xff0c;再更新一下项目链接。 …

软件测试——概念篇

目录 一、软件的生命周期 二、瀑布模型&#xff08;Waterfall Model&#xff09; 优点&#xff1a; 缺点&#xff1a; 三、螺旋模型&#xff08;Spiral Model&#xff09; ​编辑优点&#xff1a; 缺点&#xff1a; 四、增量、迭代 区别&#xff1a; 五、敏捷 scrum…

windows下如何搭建属于自己的git服务器

前一阵子公司需要&#xff0c;领导让我给我们技术部搭建一个git服务器。以前看过教程&#xff0c;但自己没动手做过&#xff0c;开始按照网上的教程来&#xff0c;但搭建过程中发现还是不够详细&#xff0c;今天给大家一个比较详细的&#xff0c;希望对大家有帮助。 高能预警&…

DB2安装指导文档

操作系统windowsXP 版本&#xff1a;9.1.7 一&#xff1a;db2安装 用户为当前操作系统得用户 域为空 二&#xff1a;添加许可证&#xff0c;因为当前系统得试用期已到&#xff0c;所以必须输入有效的序列号&#xff0c;在安装数据库的过程中有时候也会莫名的死掉&#xff0c…

【LeetCode: 剑指 Offer II 099. 最小路径之和 | 暴力递归 | DFS =>记忆化搜索=>动态规划】

&#x1f34e;作者简介&#xff1a;硕风和炜&#xff0c;CSDN-Java领域新星创作者&#x1f3c6;&#xff0c;保研|国家奖学金|高中学习JAVA|大学完善JAVA开发技术栈|面试刷题|面经八股文|经验分享|好用的网站工具分享&#x1f48e;&#x1f48e;&#x1f48e; &#x1f34e;座右…

4.java程序员必知必会类库之xml解析库

前言 百度百科解释 可扩展标记语言 (Extensible Markup Language, XML) &#xff0c;标准通用标记语言的子集&#xff0c;可以用来标记数据、定义数据类型&#xff0c;是一种允许用户对自己的标记语言进行定义的源语言。 XML是标准通用标记语言 可扩展性良好,内容与形式分离,遵…

(2023)基于多模态概率融合提示的少样本多模态情感分析

论文题目&#xff08;Title&#xff09;&#xff1a;Few-shot Multimodal Sentiment Analysis based on Multimodal Probabilistic Fusion Prompts 研究问题&#xff08;Question&#xff09;&#xff1a;用概率融合提示进行少样本的情感分析任务 研究动机&#xff08;Motiva…

IDEA插件-Lombok

在 Java 开发领域中&#xff0c;Lombok 插件已经成为一个非常流行的代码库。该插件让 Java 开发更加便捷、高效&#xff0c;因此提高了开发者的生产力。本文将对 Lombok 插件进行详细的介绍和分析。 1.Lombok是什么 Lombok 是一款 Java 开发工具&#xff0c;它可以通过注解来…