深入剖析Spring框架:循环依赖的解决机制

你好,我是柳岸花开。

什么是循环依赖?

很简单,就是A对象依赖了B对象,B对象依赖了A对象。

alt

在Spring中,一个对象并不是简单new出来了,而是会经过一系列的Bean的生命周期,就是因为Bean的生命周期所以才会出现循环依赖问题。当然,在Spring中,出现循环依赖的场景很多,有的场景Spring自动帮我们解决了,而有的场景则需要程序员来解决。

三级缓存

一级缓存为:singletonObjects

二级缓存为:earlySingletonObjects

三级缓存为:singletonFactories

先稍微解释一下这三个缓存的作用,后面详细分析:

  • singletonObjects中缓存的是已经经历了完整生命周期的bean对象。
  • earlySingletonObjects比singletonObjects多了一个early,表示缓存的是早期的bean对象。早期是什么意思?表示Bean的生命周期还没走完就把这个Bean放入了earlySingletonObjects。
  • singletonFactories中缓存的是ObjectFactory,表示对象工厂,表示用来创建早期bean对象的工厂。

解决循环依赖思路分析

先来分析为什么缓存能解决循环依赖。

上文分析得到,之所以产生循环依赖的问题,主要是:

A创建时--->需要B---->B去创建--->需要A,从而产生了循环

那么如何打破这个循环,加个中间人(缓存)

A的Bean在创建过程中,在进行依赖注入之前,先把A的原始Bean放入缓存(提早暴露,只要放到缓存了,其他Bean需要时就可以从缓存中拿了),放入缓存后,再进行依赖注入,此时A的Bean依赖了B的Bean,如果B的Bean不存在,则需要创建B的Bean,而创建B的Bean的过程和A一样,也是先创建一个B的原始对象,然后把B的原始对象提早暴露出来放入缓存中,然后在对B的原始对象进行依赖注入A,此时能从缓存中拿到A的原始对象(虽然是A的原始对象,还不是最终的Bean),B的原始对象依赖注入完了之后,B的生命周期结束,那么A的生命周期也能结束。

因为整个过程中,都只有一个A原始对象,所以对于B而言,就算在属性注入时,注入的是A原始对象,也没有关系,因为A原始对象在后续的生命周期中在堆中没有发生变化。

从上面这个分析过程中可以得出,只需要一个缓存就能解决循环依赖了,那么为什么Spring中还需要singletonFactories呢?

这是难点,基于上面的场景想一个问题:如果A的原始对象注入给B的属性之后,A的原始对象进行了AOP产生了一个代理对象,此时就会出现,对于A而言,它的Bean对象其实应该是AOP之后的代理对象,而B的a属性对应的并不是AOP之后的代理对象,这就产生了冲突。

B依赖的A和最终的A不是同一个对象

AOP就是通过一个BeanPostProcessor来实现的,这个BeanPostProcessor就是AnnotationAwareAspectJAutoProxyCreator,它的父类是AbstractAutoProxyCreator,而在Spring中AOP利用的要么是JDK动态代理,要么CGLib的动态代理,所以如果给一个类中的某个方法设置了切面,那么这个类最终就需要生成一个代理对象。

一般过程就是:A类--->生成一个普通对象-->属性注入-->基于切面生成一个代理对象-->把代理对象放入singletonObjects单例池中。

而AOP可以说是Spring中除开IOC的另外一大功能,而循环依赖又是属于IOC范畴的,所以这两大功能想要并存,Spring需要特殊处理。

如何处理的,就是利用了第三级缓存singletonFactories

首先,singletonFactories中存的是某个beanName对应的ObjectFactory,在bean的生命周期中,生成完原始对象之后,就会构造一个ObjectFactory存入singletonFactories中。这个ObjectFactory是一个函数式接口,所以支持Lambda表达式:() -> getEarlyBeanReference(beanName, mbd, bean)

上面的Lambda表达式就是一个ObjectFactory,执行该Lambda表达式就会去执行getEarlyBeanReference方法,而该方法如下:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
 Object exposedObject = bean;
 if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
  for (BeanPostProcessor bp : getBeanPostProcessors()) {
   if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
    SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
    exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
   }
  }
 }
 return exposedObject;
}

该方法会去执行SmartInstantiationAwareBeanPostProcessor中的getEarlyBeanReference方法,而这个接口下的实现类中只有两个类实现了这个方法,一个是AbstractAutoProxyCreator,一个是InstantiationAwareBeanPostProcessorAdapter,它的实现如下:

// InstantiationAwareBeanPostProcessorAdapter
@Override
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
 return bean;
}
// AbstractAutoProxyCreator
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
 Object cacheKey = getCacheKey(bean.getClass(), beanName);
 this.earlyProxyReferences.put(cacheKey, bean);
 return wrapIfNecessary(bean, beanName, cacheKey);
}

在整个Spring中,默认就只有AbstractAutoProxyCreator真正意义上实现了getEarlyBeanReference方法,而该类就是用来进行AOP的。上文提到的AnnotationAwareAspectJAutoProxyCreator的父类就是AbstractAutoProxyCreator。

那么getEarlyBeanReference方法到底在干什么? 首先得到一个cachekey,cachekey就是beanName。 然后把beanName和bean(这是原始对象)存入earlyProxyReferences中 调用wrapIfNecessary进行AOP,得到一个代理对象。

那么,什么时候会调用getEarlyBeanReference方法呢?回到循环依赖的场景中

从singletonFactories根据beanName得到一个ObjectFactory,然后执行ObjectFactory,也就是执行getEarlyBeanReference方法,此时会得到一个A原始对象经过AOP之后的代理对象,然后把该代理对象放入earlySingletonObjects中,注意此时并没有把代理对象放入singletonObjects中,那什么时候放入到singletonObjects中呢?

我们这个时候得来理解一下earlySingletonObjects的作用,此时,我们只得到了A原始对象的代理对象,这个对象还不完整,因为A原始对象还没有进行属性填充,所以此时不能直接把A的代理对象放入singletonObjects中,所以只能把代理对象放入earlySingletonObjects,假设现在有其他对象依赖了A,那么则可以从earlySingletonObjects中得到A原始对象的代理对象了,并且是A的同一个代理对象。

当B创建完了之后,A继续进行生命周期,而A在完成属性注入后,会按照它本身的逻辑去进行AOP,而此时我们知道A原始对象已经经历过了AOP,所以对于A本身而言,不会再去进行AOP了,那么怎么判断一个对象是否经历过了AOP呢?会利用上文提到的earlyProxyReferences,在AbstractAutoProxyCreator的postProcessAfterInitialization方法中,会去判断当前beanName是否在earlyProxyReferences,如果在则表示已经提前进行过AOP了,无需再次进行AOP。

对于A而言,进行了AOP的判断后,以及BeanPostProcessor的执行之后,就需要把A对应的对象放入singletonObjects中了,但是我们知道,应该是要把A的代理对象放入singletonObjects中,所以此时需要从earlySingletonObjects中得到代理对象,然后入singletonObjects中。

整个循环依赖解决完毕。

alt

总结

至此,总结一下三级缓存:

  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在依赖注入时发现出现了循环依赖(当前正在创建的bean被其他bean依赖了),则从三级缓存中拿到Lambda表达式,并执行Lambda表达式得到一个对象,并把得到的对象放入二级缓存((如果当前Bean需要AOP,那么执行lambda表达式,得到就是对应的代理对象,如果无需AOP,则直接得到一个原始对象))。
  4. 其实还要一个缓存,就是 earlyProxyReferences,它用来记录某个原始对象是否进行过AOP了。

为什么需要二级缓存?

  • 一级缓存和二级缓存相比: 二级缓存只要是为了分离成熟Bean和纯净Bean(未注入属性)的存放, 防止多线程中在Bean还未创建完成时读取到的Bean时不完整的。所以也是为了保证我们getBean是完整最终的Bean,不会出现不完整的情况。
  • 一二三级缓存下二级缓存的意义: 二级缓存为了存储 三级缓存的创建出来的早期Bean, 为了避免三级缓存重复执行。

为什么需要三级缓存?

我们都知道Bean的aop动态代理创建时在初始化之后,但是循环依赖的Bean如果使用了AOP。 那无法等到解决完循环依赖再创建动态代理, 因为这个时候已经注入属性。 所以如果循环依赖的Bean使用了aop. 需要提前创建aop。

但是需要思考的是动态代理在哪创建? 在实例化后直接创建? 但是我们正常的Bean是在初始化创建啊。 所以可以加个判断如果是循环依赖就实例化后调用,没有循环依赖就正常在初始化后调用。

二级缓存确实完全可以解决循环依赖的任何情况,包括扩展能力(因为也可以在这里调用BeanPostProcessor, 当然AOP也是基于BeanPostProcessor)。 那要三级缓存干嘛? 我们只能这样解释: Spring的方法职责都比较单例,一个方法通常只做一件事, getBean就是获取bean 但是调用创建动态代BeanPostProcessor 是属于create的过程中的, 如果在这里明显代码比较耦合,阅读性也不太好。 所以为了解耦、方法职责单一、方便后期维护, 将调用创建动态代BeanPostProcessor 放在createBean中是最合适不过了, 但是我们判断当前是否循环依赖还是要写在getSingleton里面啊,这怎么办

三级缓存 存一个函数接口, 函数接口实现 创建动态代理调用BeanPostProcessor 。 为了避免重复创建, 调用把返回的动态代理对象或者原实例存储在二级缓存, 三个缓存完美解决解耦、扩展、性能、代码阅读性。

为什么Spring不能解决构造器的循环依赖?

从流程图应该不难看出来,在Bean调用构造器实例化之前,一二三级缓存并没有Bean的任何相关信息,在实例化之后才放入三级缓存中,因此当getBean的时候缓存并没有命中,这样就抛出了循环依赖的异常了。

为什么多例Bean不能解决循环依赖?

我们自己手写了解决循环依赖的代码,可以看到,核心是利用一个map,来解决这个问题的,这个map就相当于缓存。

为什么可以这么做,因为我们的bean是单例的,而且是字段注入(setter注入)的,单例意味着只需要创建一次对象,后面就可以从缓存中取出来,字段注入,意味着我们无需调用构造方法进行注入。

  • 如果是原型bean,那么就意味着每次都要去创建对象,无法利用缓存;
  • 如果是构造方法注入,那么就意味着需要调用构造方法注入,也无法利用缓存。

本文由 mdnice 多平台发布

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【高阶数据结构】并查集 -- 详解

一、并查集的原理 1、并查集的本质和概念 (1)本质 并查集的本质:森林。 (2)概念 在一些应用问题中,需要将 n 个不同的元素划分成一些不相交的集合。 开始时,每个元素自成一个单元素集合&…

ruoyi-vue前端的一些自定义插件介绍

文章目录 自定义列表$tab对象打开页签关闭页签刷新页签 $modal对象提供成功、警告和错误等反馈信息(无需点击确认)提供成功、警告和错误等提示信息(类似于alert,需要点确认)提供成功、警告和错误等提示信息&#xff08…

免费语音转文字:自建Whisper,贝锐花生壳3步远程访问

Whisper是OpenAI开发的自动语音识别系统(语音转文字)。 OpenAI称其英文语音辨识能力已达到人类水准,且支持其它98中语言的自动语音辨识,Whisper神经网络模型被训练来运行语音辨识与翻译任务。 此外,与其他需要联网运行…

【软考---系统架构设计师】软件架构

目录 1 一、软件架构的概念 二、软件架构风格 (1)数据流风格​​​​​​​ (2)调用/返回风格 (3)独立构件风格 (4)虚拟机风格 (5)仓库风格 三、架构…

《乱弹篇(30)厌战的杜诗》

时下地球村有一伙成天叫嚣着“打打杀杀”、鼓吹快快发动战争的狂人,他们视老百姓的生命如草芥,毫不珍惜。没有遭受过战火焚烧的人,也跟着成天吠叫“快开战吧”。然而中国唐朝大诗人却是个“厌战派”,他对战争的厌恶集中表现在诗《…

Navicat Premium 16最新版激活 mac/win

Navicat Premium 16 for Mac是一款专业的多连接数据库管理工具。它支持连接多种类型的数据库,包括MySQL、MongoDB、Oracle、SQLite、SQL Server、PostgreSQL等,可以同时连接多种数据库,帮助用户轻松地管理和迁移数据。 Navicat Premium 16 fo…

机器学习与深度学习 --李宏毅(笔记与个人理解)Day 20

Day 20 RNN 2 实际使用和其他应用 在实际的学习(training)过程中是如何工作的? step 1 Loss step 2 training Graindent Descent 反向传播的进阶版 – BPTT CLIpping 设置阈值~ 笑死昨天刚看完关伟说的有这玩意的就不是好东西 Why&#xff1…

大模型系列课程学习-大预言模型微调方法介绍

1.大语言模型相关基本概念综述 语言模型指对语言进行建模,其起源于语音识别(speech recognition),输入一段音频数据,语音识别系统通常会生成多个句子作为候选,究竟哪个句子更合理? 学术上表达为:描述一段自…

驶向成功:如何选择适合国际拓展的完美CRM

一、出海企业应该怎么选择CRM? 出海企业在选择CRM系统时,需要考虑行业特性以及在对外业务过程中可能遇到的问题。接触了一些出海企业客户,总结了以下出海企业在选择CRM时的诉求。 1、合法合规风险 出海企业的业务遍布不同国家地区&#xff…

sso-oauth2单点登录功能笔记

场景:最近公司2个系统需要做单点登录,A系统作为服务器,认证方式是sso-oauth2方式,B系统作为客户端,token方式是ta-token,先来张sso-oauth2认证方式的图 前置准备工作 第一步:要确认谁是服务提…

AI+PS快捷键大全!

hello,我是小索奇, 你会用Photoshop(PS)或者(Illustrator)AI吗?相信很多人都会接触到吧,但有一部分人很少用快捷键,仅凭借鼠标点击来实现功能,殊不知快捷键能…

如何添加所有未跟踪文件到暂存区?

文章目录 如何将所有未跟踪文件添加到Git暂存区?步骤与示例代码1. 打开命令行或终端2. 列出所有未跟踪的文件3. 添加所有未跟踪文件到暂存区4. 验证暂存区状态 如何将所有未跟踪文件添加到Git暂存区? 在版本控制系统Git中,当我们首次创建新文…