Spring的缓存机制-循环依赖

news2024/11/23 18:36:50

群公告
Java每日大厂面试题:

1、Spring 是如何解决循环依赖?
           
答案:三级缓存,简单来说,A创建过程中需要B,于是A将自己放到三级缓存里面,去实例化B,B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A,B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态)然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A放到一级缓存中。

1. 背景

在最近的业务需求开发过程中遇到了“传说中”的循环依赖问题,在之前学习Spring的时候经常会看到Spring是如何解决循环依赖问题的,所谓循环依赖即形成了一个环状的依赖关系,这个环中的某一个点产生不稳定变化都会导致整个链路产生不稳定的变化;此外循环依赖还会导致应用程序启动失败内存溢出、甚至出现一些难以排查的问题,于是便系统性的对该问题进行学习和总结并整理文章如下。



2. 循环依赖
2.1. 什么是循环依赖?

循环依赖指的是多个对象之间的依赖关系形成了一个闭环。图1、2分别是两个对象和多个对象形成循环依赖图示,实际编程中由于依赖层次深、关系复杂等因素,导致依赖关系难以清晰梳理。





图1 两个对象间的循环依赖





图2 多个对象间的循环依赖

2.2. 为什么会产生循环依赖?

Spring创建bean的本质还是创建对象,一个完整的对象包含两部分:当前对象的实例化对象属性的实例化。在Spring中,对象的实例化是通过反射实现的,而对象的属性则是在对象实例化之后通过一定的方式设置的。





图3 Spring创建Bean流程

接下来以Demo中的类A、B为例描述循环依赖产生过程:

Demo中的类A、B中各自都以对方为自己的全局属性,并且在Spring中实例化bean是通过ApplicationContext.getBean()方法来进行的。

如果获取的对象依赖了另一个对象,那么会首先创建当前对象,然后通过递归的调用ApplicationContext.getBean()方法来获取所依赖的对象,最后将获取到的对象注入到当前对象中。

@Component
public class A {

  private B b;

  public void setB(B b) {
    this.b = b;
  }
}
@Component
public class B {

  private A a;

  public void setA(A a) {
    this.a = a;
  }
}

此处以Demo中初始化A对象为例,

第一步:首先Spring尝试通过ApplicationContext.getBean()方法获取A对象的实例,由于Spring容器中还没有A对象实例,因而其会创建一个A对象,然后发现其依赖了B对象,所以会尝试递归的通过ApplicationContext.getBean()方法获取B对象的实例,但是Spring容器中此时也没有B对象的实例,因而其还是会先创建一个B对象的实例。( 此时A对象和B对象都已经创建了,并且保存在Spring容器中了,只不过A对象的属性b和B对象的属性a都还没有设置进去。)

第二步:在前面Spring创建B对象之后,Spring发现B对象依赖了属性a,因而此时还是会尝试递归的调用ApplicationContext.getBean()方法获取A对象的实例,因为Spring中已经有一个A对象的实例,虽然只是半成品(其属性b还未初始化),但其也还是目标bean,因而会将该A对象的实例返回。(此时,B对象的属性a就设置进去了,然后还是ApplicationContext.getBean()方法递归的返回,也就是将B对象的实例返回,此时就会将该实例设置到A对象的属性b中。)

第三步:在上面这个递归过程的最后,Spring将获取到的B对象实例设置到了A对象的属性b中了,这里的A对象其实和前面设置到实例B中的半成品A对象是同一个对象,其引用地址是同一个,这里为A对象的b属性设置了值,其实也就是为那个半成品的a属性设置了值。

实际加载过程流程图如图4所示:其中图中getBean()表示调用Spring的ApplicationContext.getBean()方法,而该方法中的参数,则表示我们要尝试获取的目标对象。图中的黑色箭头表示一开始的方法调用走向,走到最后,返回了Spring中缓存的A对象之后,表示递归调用返回了,此时使用红色的箭头表示。从图中我们可以很清楚的看到,B对象的a属性是在第三步中注入的半成品A对象,而A对象的b属性是在第二步中注入的成品B对象,此时半成品的A对象也就变成了成品的A对象,因为其属性已经设置完成了。





图4 Bean加载流程图

3. Spring缓存机制
3.1. Spring是如何解决循环依赖的?

Spring在DefaultSingletonBeanRegistry类中维护了三个Map,也就是我们通常说的三级缓存。

◦singletonObjects (一级缓存)它是我们最熟悉的朋友,俗称“单例池”“容器”,缓存创建完成单例Bean的地方。

◦earlySingletonObjects(二级缓存)映射Bean的早期引用,也就是说在这个Map里的Bean不是完整的,甚至还不能称之为“Bean”,只是一个Instance。

◦singletonFactories(三级缓存) 映射创建Bean的原始工厂。





图5 Spring三级缓存

3.2. Spring源码之“获取Bean

接下来结合具体的Spring源码进行分析,首先分析“获取Bean”的源码,注意getSingleton()方法。

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
        //第1级缓存 用于存放 已经属性赋值、完成初始化的 单例Bean
        private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
        //第2级缓存 用于存在已经实例化,还未做代理属性赋值操作的 单例Bean
        private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
        //第3级缓存 存储创建单例Bean的工厂
        private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
        //已经注册的单例池里的beanName
        private final Set<String> registeredSingletons = new LinkedHashSet<>(256);
        //正在创建中的beanName集合
        private final Set<String> singletonsCurrentlyInCreation =
                Collections.newSetFromMap(new ConcurrentHashMap<>(16));
        //缓存查找bean  如果第1级缓存没有,那么从第2级缓存获取。如果第2级缓存也没有,那么从第3级缓存创建,并放入第2级缓存。
        protected Object getSingleton(String beanName, boolean allowEarlyReference) {
            Object singletonObject = this.singletonObjects.get(beanName); //第1级
            if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
                synchronized (this.singletonObjects) {
                    singletonObject = this.earlySingletonObjects.get(beanName); //第2级
                    if (singletonObject == null && allowEarlyReference) {
                        //第3级缓存  在doCreateBean中创建了bean的实例后,封装ObjectFactory放入缓存的bean实例
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            //创建未赋值的bean
                            singletonObject = singletonFactory.getObject();
                            //放入到第2级缓存
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            //从第3级缓存删除
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
            return singletonObject;
        }   
    }
3.3. Spring源码之“添加到第1级缓存”

其中“添加到第1级缓存”的源码为:

 protected void addSingleton(String beanName, Object singletonObject) {
            synchronized (this.singletonObjects) {
                // 放入第1级缓存
                this.singletonObjects.put(beanName, singletonObject);
                // 从第3级缓存删除
                this.singletonFactories.remove(beanName);
                // 从第2级缓存删除
                this.earlySingletonObjects.remove(beanName);
                // 放入已注册的单例池里
                this.registeredSingletons.add(beanName);
            }
        }

添加到“第3级缓存”的源码为:

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
            synchronized (this.singletonObjects) {
                // 若第1级缓存没有bean实例
                if (!this.singletonObjects.containsKey(beanName)) {
                    // 放入第3级缓存
                    this.singletonFactories.put(beanName, singletonFactory);
                    // 从第2级缓存删除,确保第2级缓存没有该bean
                    this.earlySingletonObjects.remove(beanName);
                    // 放入已注册的单例池里
                    this.registeredSingletons.add(beanName);
                }
            }
        }

“创建Bean”的源码为下面所示,通过这段代码,我们可以知道:Spring 在实例化对象之后,就会为其创建一个 Bean 工厂,并将此工厂加入到三级缓存中。

因此,Spring 一开始提前暴露的并不是实例化的 Bean,而是将 Bean 包装起来的ObjectFactory。为什么要这么做呢?

这实际上涉及到 AOP。如果创建的 Bean 是有代理的,那么注入的就应该是代理 Bean,而不是原始的 Bean。但是,Spring一开始并不知道 Bean是否会有循环依赖,通常情况下(没有循环依赖的情况下),Spring 都会在“完成填充属性并且执行完初始化方法”之后再为其创建代理。但是,如果出现了循环依赖,Spring 就不得不为其提前创建"代理对象";否则,注入的就是一个原始对象,而不是代理对象。因此,这里就涉及到"应该在哪里提前创建代理对象。

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
    BeanWrapper instanceWrapper = null;
    
    if (instanceWrapper == null) {
        //实例化对象
        instanceWrapper = this.createBeanInstance(beanName, mbd, args);
    }
 
    final Object bean = instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null;
    Class<?> beanType = instanceWrapper != null ? instanceWrapper.getWrappedClass() : null;
   
    //判断是否允许提前暴露对象,如果允许,则直接添加一个 ObjectFactory 到第3级缓存
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        //添加到第3级缓存
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }
 
    //填充属性
    this.populateBean(beanName, mbd, instanceWrapper);
    //执行初始化方法,并创建代理
    exposedObject = initializeBean(beanName, exposedObject, mbd);
    return exposedObject;
}

Spring通过在ObjectFactory中去提前创建代理对象,该对象会执行getObject()方法来获取Bean。执行方法如下:

public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
        implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
    @Override
    public Object getEarlyBeanReference(Object bean, String beanName) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        // 记录已被代理的对象
        this.earlyProxyReferences.put(cacheKey, bean);
        return wrapIfNecessary(bean, beanName, cacheKey);
    }
}

提前进行对象的代理工作,并在 earlyProxyReferences map中记录已被代理的对象,是为了避免在后面重复创建代理对象。

public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
        implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
    @Override
    public Object getEarlyBeanReference(Object bean, String beanName) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        // 记录已被代理的对象
        this.earlyProxyReferences.put(cacheKey, bean);
        return wrapIfNecessary(bean, beanName, cacheKey);
    }
}

再次分析获取bean的方法getSingleton()方法,可知:提前暴露的对象,虽然已实例化,但是没有进行属性填充,还没有完成初始化,是一个不完整的对象。 这个对象存放在二级缓存中,对于三级缓存机制十分重要,是解决循环依赖一个非常巧妙的设计。接下来我们结合Spring缓存机制来分析上面Demo中A、B循环依赖。

◦A 调用doCreateBean()创建Bean对象:由于还未创建,从第1级缓存singletonObjects查不到,此时只是一个半成品(提前暴露的对象),放入第3级缓存singletonFactories。

◦A在属性填充时发现自己需要B对象,但是在三级缓存中均未发现B,于是创建B的半成品,放入第3级缓存singletonFactories。

◦B在属性填充时发现自己需要A对象,从第1级缓存singletonObjects和第2级缓存earlySingletonObjects中未发现A,但是在第3级缓存singletonFactories中发现A,将A放入第2级缓存earlySingletonObjects,同时从第3级缓存singletonFactories删除

◦将A注入到对象B中。

◦B完成属性填充,执行初始化方法,将自己放入第1级缓存singletonObjects中(此时B是一个完整的对象),同时从第3级缓存singletonFactories和第2级缓存earlySingletonObjects中删除。

◦A得到“对象B的完整实例”,将B注入到A中。

◦A完成属性填充,执行初始化方法,并放入到第1级缓存singletonObjects中。

在创建过程中,都是从第三级缓存(对象工厂创建不完整对象),将提前暴露的对象放入到第二级缓存;从第二级缓存拿到后,完成初始化,并放入第一级缓存。

4. 总结

以上便是有关Spring如何利用缓存机制来解决循环依赖的学习和总结。

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

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

相关文章

智能AI系统ChatGPT系统源码+支持GPT4.0+支持ai绘画(Midjourney)/支持OpenAI GPT全模型+国内AI全模型

一、AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如…

交流负载发电机测试

交流负载发电机测试是一种常用的测试方法&#xff0c;用于评估发电机在负载条件下的性能和稳定性。测试过程中需要使用负载设备模拟实际负载&#xff0c;并通过测量电压、电流、功率等参数来评估发电机的输出能力和稳定性。 在进行测试之前&#xff0c;首先需要准备好测试设备和…

30张图详解IP地址网络知识

你们好&#xff0c;我的网工朋友。 IP地址是所有网络初级课程里最先涉及到的技术点&#xff0c;对于IP地址的合理规划是网络设计的重要环节&#xff0c;必须拿捏。 IP地址规划的好坏&#xff0c;影响到网络路由协议算法的效率&#xff0c;影响到网络的性能&#xff0c;影响到网…

短剧出海火爆,Flat Ads独家流量助泛娱乐赛道App迅速获客增长

10月26日&#xff0c;由扬帆出海主办的GICC2023 | 第四届全球互联网产业CEO大会正式圆满落幕&#xff0c;Flat Ads等出海企业应邀参加。 据悉&#xff0c;本届GICC深圳站邀请200CXO行业领袖、300各路优质厂商、1200全球互联网产业代表共聚一堂&#xff0c;聚焦短剧、游戏、泛娱…

最前端|如何使用Plausible实现页面埋点?

目录 一、业务背景 二、业务场景描述 三、解决方案 //如何集成 Plausible &#xff1f; //如何监控特定功能使用情况&#xff1f; 什么是 MyEventName? //如何向自定义事件传递参数&#xff1f; 一、业务背景 随着公司自研产品的不断发展&#xff0c;对前端页面的监控和…

强力解决使用node版本管理工具 NVM 出现的问题(找不到 node,或者找不到 npm)

强力解决使用node版本管理工具 NVM 出现的问题&#xff08;找不到 node&#xff0c;或者找不到 npm&#xff09; node与npm版本对应关系 nvm是好用的Nodejs版本管理工具&#xff0c; 通过它可以方便地在本地调换Node版本。 2020-05-28 Node当前长期稳定版12.17.0&#xff0c;…

适用于Linux桌面歌词应用程序MusixMatch

导读Musixmatch桌面应用程序可用于Linux&#xff01;不是Linux用户缺少桌面歌词应用程序。包括“即时歌词”和“Lyricfier”&#xff0c;许多开源音乐播放器都会运用某种歌词集成。 但是Musixmatch应用程序与那些有点不同。 Musixmatch的USP是Syncronized歌词 如果您曾经使用…

如何选择高效率的在线分板机主轴?

随着智能移动设备和其他电子3C设备需求的增大&#xff0c;PCB分板机的需要也随之而大增。越来越多的企业开始使用在线分板机来替代传统的手工分板&#xff0c;从而提升了生产效率&#xff0c;提高了产品质量&#xff0c;降低了生产成本。在分板机设备中&#xff0c;高速主轴是关…

Pandas数据预处理Pandas合并数据集在线闯关_头歌实践教学平台

这里写目录标题 第1关 Concat与Append操作第2关 合并与连接第3关 案例&#xff1a;美国各州的统计数据 第1关 Concat与Append操作 任务描述 本关任务&#xff1a;使用read_csv()读取两个csv文件中的数据&#xff0c;将两个数据集合并&#xff0c;将索引设为Ladder列&#xff0…

92. 递归实现指数型枚举

题目 思路 因为有n个数&#xff0c;每个数选或不选都是一种方案&#xff0c;而且要递增输出&#xff0c;那么就标记每个数是否备选&#xff0c;然后判断完n个数以后&#xff0c;就可以输出了 代码 #include<bits/stdc.h> using namespace std; int n; bool f[100] {0…

WEB渲染模式——CSR SSR SSG ISR DPR区别

页面渲染 浏览器渲染页面&#xff0c;根据HTML文档类型声明&#xff08;DOCTYPE&#xff09;解析HTML和CSS&#xff0c;渲染步骤&#xff1a;解析、样式计算、元素布局、绘制、重绘重排。HTML、CSS、JavaScript是网页的三大核心技术。 HTML (Hyper Text Markup Language) 超文…

【沐风老师】3dMax快速平铺纹理插件QuickTiles教程

QuickTiles是3ds max的一个插件&#xff0c;允许您将常规瓷砖纹理转换为交互式纹理&#xff0c;就在mat.editor中。 换言之&#xff0c;您可以根据需要对任何纹理进行修改和重新创建&#xff1a;更改布局、瓷砖大小、格式、颜色、接缝、体积、随机化形状或纹理等等。 这种方法大…

eNsp下如何使用wireshark抓包

文章目录 拓扑图抓包操作 拓扑图 抓包操作 可以通过下图上的指示 来设置 Time列的显示样式。 这里有个缺点就是就是抓取ensp上的虚拟设备上的数据包时的&#xff0c;年月日时间显示的不对。暂时无解决办法。 一般选择 日期和时间&#xff08;日期和时间与当前标准时间对应上时…

集简云平台助力无代码开发,实现平安银行与电商平台、CRM系统的快速连接

无代码开发与平安银行 平安银行是中国内地首家公开上市的全国性股份制银行&#xff0c;经过多年发展&#xff0c;已经在科技引领、综合金融、零售转型等领域形成独特竞争力和鲜明经营特色。近年来&#xff0c;平安银行更是积极拥抱科技&#xff0c;为此&#xff0c;选择了与集…

智慧工地源码 手册文档 app 数据大屏、硬件对接、萤石云

智慧工地解决方案依托计算机技术、物联网、云计算、大数据、人工智能、VR、AR等技术相结合&#xff0c;为工程项目管理提供先进技术手段&#xff0c;构建工地现场智能监控和控制体系&#xff0c;弥补传统方法在监管中的缺陷&#xff0c;最终实现项目对人、机、料、法、环的全方…

CROS错误 403 preflight 预检

预检 403 响应 Response for preflight 403 forbidden 如上图&#xff0c;配置了请求接口一直报错&#xff0c;前端看了没有什么问题&#xff0c;不知道哪里报错了&#xff0c;那么可能是后端没有设置跨域。&#xff08;或者是设置了&#xff0c;但是可能需要换一种方式&#…

C语言每日一题(26)

力扣网 203. 移除链表元素 题目描述 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 思路分析 针对如图的普通情况&#xff0c;不能简单的遍历到对应位置然后进行释放&#xff0c;一方…

基于springboot实现致远汽车租赁平台管理系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现致远汽车租赁平台管理系统演示 摘要 首先,论文一开始便是清楚的论述了系统的研究内容。其次,剖析系统需求分析,弄明白“做什么”,分析包括业务分析和业务流程的分析以及用例分析,更进一步明确系统的需求。然后在明白了系统的需求基础上需要进一步地设计系统…

bamboo is currently exporting

bamboo每天到11点会自动挂起执行export任务 构建任务会暂停影响研发发布版本 原因是bamboo设置了Scheduled backups 把时间改成凌晨的1点钟

SpringBoot不同环境加载不同配置文件(dev,sit,uat)

目录 一、springboot的profile配置profile多配置文件 二、maven的profiles策略 我们在使用spring的时候&#xff0c;一般都会有不同的环境需要部署&#xff1a;开发环境、测试环境和验收环境&#xff0c;而不同的环境则会有不同的配置&#xff0c;比如数据库ip。解决这个问题&a…