【Spring源码分析】循环依赖的底层源码剖析

循环依赖的底层源码剖析

  • 一、预知知识
  • 二、循环依赖的底层源码剖析
    • 1. Spring 是如何存储半成品Bean的?
      • getEarlyBeanReference 方法的源码分析
    • 2. Spring 是如何解决的循环依赖呢?
      • 测试
    • 3. 哪些循环依赖 Spring 是无法解决的呢?
      • @Async 引起的循环依赖
        • 解决方案
      • 单构造注入引起的循环依赖
        • 解决方案
  • 三、总结

阅读此需阅读下面这些博客先
【Spring源码分析】Bean的元数据和一些Spring的工具
【Spring源码分析】BeanFactory系列接口解读
【Spring源码分析】执行流程之非懒加载单例Bean的实例化逻辑
【Spring源码分析】从源码角度去熟悉依赖注入(一)
【Spring源码分析】从源码角度去熟悉依赖注入(二)
【Spring源码分析】@Resource注入的源码解析

这篇博客不想阐述循环依赖是啥,只是想从源码的角度分析Spring如何解决的循环依赖?哪种循环依赖Spring自身是解决不了的?(源码分析这玩意每个人看)

一、预知知识

在 getBean->doGetBean 中,如果在单例池中找不到对应的实例的话,就会去尝试创建(这里指的是单例),下面是单例作用域的创建代码(重复阐述这块代码是为了引出一个重要的集合——singletonsCurrentlyInCreation):
在这里插入图片描述public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory)

singletonFactory 参数对应的就是那个 Lambda 表达式,本质就是创建Bean。

getSingleton 方法中,在 singletonFactory.getObject() 调用前(创建Bean),会调用 beforeSingletonCreation(beanName),在那之后又会调用 afterSingletonCreation(beanName),其实就是将 beanName 放入到 singletonsCurrentlyInCreation 集合中和从集合中移除,表示在创建中,和创建后的逻辑。
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
经过了实例化、属性注入、初始化后的Bean最后通过 addSingleton 加入到单例池中。

在这里插入图片描述
在属性注入阶段,针对一般情况,排除掉 @Value->autowireCandidate->@Qualifier->@Primary->@Priority 这六步筛选…然后其去找Bean注入时,要么就是单例池已经存在,那么直接使用,要么就是不存在,调用getBean方法去获取,也就是会去走创建逻辑,也就是走上面阐述的流程,创建前会将其放入到那个 singletonsCurrentlyInCreating 集合中,表示在创建。

那么可以分析得出,如果当前创建的Bean,在进行属性注入时,属性注入的这个Bean也是在创建中(意思就是在 singletonCurrentlyInCreating集合中),那么就是发生了循环依赖

下面画个简图:
在这里插入图片描述循环依赖主要问题就是属性注入的时候出现了循环,那么出现循环的Bean就不能完成真正的实例化(这里的真正的实例化是指已经进行完了属性注入和初始化最终的Bean,不是说单纯的经过了JVM对象创建出来的过程)。

想要解决这个问题,很简单,就是咱推断构造然后通过反射得到的Bean(就是经过了JVM规范对象创建流程的)将其注入属性,这样就不会阻塞得到真正的Bean了。那 Spring 是如何实现的将这个半成品Bean可以在属性注入阶段拿出来然后注入进去呢? 下面进行源码分析。

在此之前有提,属性注入的时候,是先尝试从单例池中拿,这个从单例池中拿,在调用的 getSingleton 方法,如果没找到再去 getBean 方法。

二、循环依赖的底层源码剖析

其实有关 Spring 对循环依赖问题的解决代码不多的。

1. Spring 是如何存储半成品Bean的?

在 Spring 生命周期的实例化->MergedBeanDefinitionPostProcessor操作完后,会有下面这么一操作:

		// 为了解决循环依赖提前缓存单例创建工厂// Eagerly cache singletons to be able to resolve circular references// even when triggered by lifecycle interfaces like BeanFactoryAware.// 只要当前创建的实例是单例的,earlySingletonExposure 一般情况下都为true,这里的 allowCircularReferences默认是trueboolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));// 那既然 earlySingletonExposure 是true,那就会尝试向三级缓存中添加一个ObjectFactory实例// 这个 ObjectFactory 是一个函数式接口if (earlySingletonExposure) {// 循环依赖-添加到三级缓存addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}
  • 只要咱当前创建的实例是单例的,那么 singletonCurrentlyInCreating 集合中就有这个 beanName,而 allowCircularReferences 默认初始化是 true 的,所以说咱要是当前Bean是单例的话,那么 earlySingletonExposure 这个标志变量就是 true。
  • earlySingletonExposure 为 true 那就会调用 addSingletonFactory 方法,添加啥东东,咱看看:
    • private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
    • protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) { synchronized (this.singletonObjects) { if (!this.singletonObjects.containsKey(beanName)) { this.singletonFactories.put(beanName, singletonFactory); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } } }
    • ObjectFactory 是一个函数是接口,从传递的 Lambda 来看,其内部的 getObject 的实现就对应着 getEarlyBeanReference 方法。最后将这个放入到一个叫 singletonFactories 的一个 HashMap 中。

这样做有什么意义呢?咱根据上面的分析进行这里的回答:上述说要解决循环依赖,不就可以在它需要这个Bean的实例,不要进行完整的Bean的生命周期,而是把这个还没进行属性注入前的Bean就直接返回给那个需要这个Bean的Bean。这样就话就不影响它最终Bean的创建,从而解决这个问题。

那这个Bean无非就是指实例化后的Bean,而这个 singletonFactories 的 Map 集合就是为了缓存这个半成品Bean,要是后续出现循环依赖了好使用这个Bean,这样就不影响最终Bean的生成了(最终成品的Bean是否创建完成是判断有无在单例池中,而要想进入到单例池中就应该先进行完整个Bean的生命周期(这里不包括销毁))。

那为啥传这个 ObjectFactory 这个对象,而不是直接缓存刚实例化后的对象呢?

咱先看一下 getEarlyBeanReference 方法的实现:

getEarlyBeanReference 方法的源码分析

	protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {Object exposedObject = bean;if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);}}return exposedObject;}
  • 哦吼~又是遍历BeanPostProcessor,而这次遍历的是 SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference
    • getEarlyBeanReference 方法的默认实现就是将 Bean 直接返回default Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { return bean; }
    • 但是它有一个地方进行了重写:
      • 在这里插入图片描述
      • 在这里插入图片描述

现在可以知道,不直接传递Bean,而是传递 ObjectFactory 的原因了,就是除了考虑普通Bean的情况下,还需要考虑 AOP 的情况,因为咱不能属性注入需要的是经过 AOP 动态代理的代理Bean,而我们且给普通Bean。

AOP 是在初始化后进行的,所以应该在此之前就应该把 AOP 代理Bean给交出去进行属性注入的,不然也不符合单例Bean的设计了。

我们知道打开 AOP 是需要加个 @EnableAspectJAutoProxy 注解的。

在没有开启 @EnableAspectJAutoProxy 注解时,容器里的 BeanPostProcessor 只有六个(其中一个PostProcessorTest是我自己瞎写的):
在这里插入图片描述开启后(有七个,多了个 AnnotationAwareAspectJAutoProxyCreator):在这里插入图片描述

分析它的关系图,咱可以知道它是继承自 AbstractAutoProxyCreator

在这里插入图片描述了解到这就可以了,后面就是阐述 AOP 时该阐述的了,反正你只要知道这个Bean进行AOP返回后,会有标志位进行记录,初始化后判断了在之前进行过了 AOP,那就直接返回原型 Bean 就可以了。

2. Spring 是如何解决的循环依赖呢?

首先需要知道在进行属性注入的时(不考虑一个类型容器内出现多个Bean哈,为了统一点),先进行的是 @Lazy 的判断,若有就直接返回了对应代理了(这也是很多Spring没发解决然后使用@Lazy注解可以解决的原因)-> getSingleton 去获取对应 Bean -> getBean 方法去创建Bean

出现循环依赖了,我们肯定不能让它走创建逻辑啊,那么循环依赖的解决肯定是在 DefaultSingletonBeanRegistry#getSingleton 中获取 Bean 的时候了。

	@Nullableprotected Object getSingleton(String beanName, boolean allowEarlyReference) {// Quick check for existing instance without full singleton lockObject singletonObject = this.singletonObjects.get(beanName);// 单例池中没有,且在SingletonCurrentlyInCreating集合中// 表示在创建中,说明存在循环依赖if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {// 从 earlySingletonObjects Map 中获取对应的Bean,这主要是 Spring 修改之前的一个死锁bugsingletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null && allowEarlyReference) {synchronized (this.singletonObjects) {// Consistent creation of early reference within full singleton locksingletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null) {ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);if (singletonFactory != null) {// 调用之前传入Lambda表达式实现的ObjectFactory的getObject()方法获取对应半成品BeansingletonObject = singletonFactory.getObject();// 将半成品Bean放入到 earlySingletonObjects 中this.earlySingletonObjects.put(beanName, singletonObject);// 将其从 singletonFactories 中移除this.singletonFactories.remove(beanName);}}}}}}return singletonObject;}
  • 主要逻辑就是如果单例池中存在就直接返回;
  • 单例池中不存在,且这个beanName是在创建中,就说明存在循环依赖,那么就会从 singletonFactories 中获取对应的 ObjectFactory 实例调用 getObject 方法去获取半成品Bean;
  • 然后将这个半成品Bean放入到 earlySingletonObjects 这个 Map 中,最后将这个半成品Bean进行返回。

以上的逻辑就解决了循环依赖问题了,可以解决循环依赖最关键的一步还是在 singletonFactories 上,因为是它缓存了半成品的Bean,不管是普通Bean还是AOP代理Bean出现循环依赖都可以得到解决。

测试

在这里插入图片描述在这里插入图片描述在这里插入图片描述

不管开不开 AOP 都没啥子问题滴,这种循环依赖 Spring 都会给俺们解决滴。

3. 哪些循环依赖 Spring 是无法解决的呢?

上面阐述了在开启了 AOP 然后产生了循环依赖的话会提前对其进行创建(也不是说提前吧,就是出现循环依赖那个注入调用getSingleton的时候,就是在属性注入阶段,相对真正AOP初始化后阶段肯定是提前了的),若提前了的话那初始化后操作就不会进行 AOP 了,而是直接返回的 Bean,那它是怎么最后返回的代理对象的呢?

对应着下面这段代码:

if (earlySingletonExposure) {Object earlySingletonReference = getSingleton(beanName, false);if (earlySingletonReference != null) {if (exposedObject == bean) {exposedObject = earlySingletonReference;}else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {// beanName被哪些bean依赖了,现在发现beanName所对应的bean对象发生了改变,那么则会报错String[] dependentBeans = getDependentBeans(beanName);Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);for (String dependentBean : dependentBeans) {if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {actualDependentBeans.add(dependentBean);}}if (!actualDependentBeans.isEmpty()) {throw new BeanCurrentlyInCreationException(beanName,"Bean with name '" + beanName + "' has been injected into other beans [" +StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +"] in its raw version as part of a circular reference, but has eventually been " +"wrapped. This means that said other beans do not use the final version of the " +"bean. This is often the result of over-eager type matching - consider using " +"'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");}}}}

就是如果存在循环依赖,那么后面调用 getSingleton 获取到的 earlySingletonReference 实例对象就不会为空,这个 earlySingletonReference 可能是普通Bean也可能是 AOP 的代理Bean。
但是在赋值给需要暴露的exposedObject之前,还经过了一个 exposedObject == bean ,这样是去保证循环依赖的出现应该最终得到的Bean也应该和最初反射得到的Bean是指向同一个堆内存的,意思就是同一个对象,这样的话就允许赋值,否则会抛出 BeanCurrentlyInCreationException 异常,表示循环依赖导致出错。

@Async 引起的循环依赖

通过 @EnableAsync 注解开启 @Async 的使用,就相当于向容器中添加了 AsyncAnnotationBeanPostProcessor 这个 BeanPostProcessor。也是在初始化后进行的操作,若用了 @Async 的话会给 Bean 生成一个代理。

在这里插入图片描述就是说当这个Bean存在循环依赖,又开启并使用了 @Async 注解,那么就会出现暴露出来的Bean和原先反射的实例Bean并不是指向同一个堆内内存。这样的话就会报错咯~
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当然这个还和本身加载的顺序有关,比如这里是先创建的 OrderService,然后去属性注入 UserService,UserService就会出现循环依赖,那么注入OrderService时候就会使用三级缓存,然后把OrderService会放入二级缓存,然后OrderService又使用了@Async注解,那在初始化后那个exposedBean就会是一个代理Bean且OrderService在二级缓存中是存在的,所以抛出了异常。

所以我们需要尽量避免这种存在循环依赖的代码,有的时候加载Bean的顺序不同会有着不一样的效果,遍历是遍历 beannames集合,而存储 beanName 是 Set 集合,所以是无序的。

解决方案

注入 OrderService 的时候加上个 @Lazy 注解,那 UserService Bean 创建的时候注入 OrderService 的时候就不需要去调用 getSingleton 方法,Bean就不会到二级缓存中,那么最后就不会有 exposedBean 和 原Bean == 对比。

在这里插入图片描述

在这里插入图片描述

单构造注入引起的循环依赖

这边将 OrderService 改成构造注入 UserService,也会产生异常(也是和顺序有关,上面说了,我这里的例子是先注入的OrderService):
在这里插入图片描述
在这里插入图片描述在这里插入图片描述这里还是得和生命周期联系在一起,需要知道将半成品Bean放入三级缓存是在实例化Bean和MergedBeanDefinition之后的

那么先创建 OrderService,实例化Bean的时候需要注入 UserService,此时 OrderService 已在 singletonsCurrentlyInCreation 集合中,在注入UserService时,需要填充 OrderService,首先 OrderService 没被 @Lazy 修饰,getSingleton 方法也返回的是null,虽存在循环依赖,但三级缓存中是不存在这个的,因为OrderService还在实例化阶段。所以这里填充 OrderService 只能走 getBean 逻辑,那么又得尝试放入 singletonsCurrentlyInCreation 集合中。所以会抛出异常:

在这里插入图片描述

解决方案

在注入 OrderService 的时候加上个 @Lazy 注解,这样的话 UserService Bean 就可以创建出来了。那么 OrderService 自然是可以创建出来的。
在这里插入图片描述
在这里插入图片描述

三、总结

  • 普通用法和AOP引起的循环依赖,Spring都自动给咱解决了,但扔存在一些用法的时候引起的循环依赖需要我们注意。在写代码的时候,咱尽量别写存在循环依赖的代码。

  • 像 @Async 注解和构造注入引起的循环依赖,执行顺序不同的时候可能会抛出异常,使用 @Lazy 注解也是可以解决的,但是尽量注意不要写。

  • Spring解决循环依赖靠的就是三级和二级缓存,那咱来总结下三级缓存在 Spring 的作用都是啥:

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

在这里插入图片描述

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

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

相关文章

华为配置小型网络WLAN 的基本业务示例

配置小型网络WLAN基本业务示例 组网图形 图1 配置小型网络WLAN基本业务组网图 小型WLAN网络简介配置注意事项组网需求数据规划配置思路操作步骤配置文件 小型WLAN网络简介 本文介绍的WLAN网络是指利用频率为2.4GHz或5GHz的射频信号作为传输介质的无线局域网&#xff0c;相对于有…

Leetcode—1828. 统计一个圆中点的数目【中等】

2024每日刷题&#xff08;一零五&#xff09; Leetcode—1828. 统计一个圆中点的数目 实现代码 class Solution { public:vector<int> countPoints(vector<vector<int>>& points, vector<vector<int>>& queries) {vector<int> a…

LINUX基础培训十九之常见服务dns介绍

前言、本章学习目标 了解dns服务用途掌握dns服务器的配置掌握dns服务的使用 一、DNS服务概述 DNS是域名系统(Domain Name System)的缩写&#xff0c;是因特网的一项核心服务&#xff0c;它作为可以将域名和IP地址相互映射的个分布式数据库&#xff0c;能够使人更方便的访问…

STL-priority_queue

文档 目录 1.关于priority_queued1的定义 2.priority_queue的使用 1.关于priority_queued1的定义 1. 优先队列是一种容器适配器&#xff0c;根据严格的弱排序标准&#xff0c;它的第一个元素总是它所包含的元素中最大的。 2. 此上下文类似于堆&#xff0c;在堆中可以随时插入元…

主从数据库MySQL服务重启步骤与注意事项

主从数据库MySQL服务重启步骤与注意事项 实验环境&#xff1a; 172.20.26.34 &#xff08;主应用服务器&#xff09; 172.20.26.26 &#xff08;备应用服务器&#xff09; 172.20.26.37 &#xff08;主库服务器&#xff09; 172.20.26.38 &#xff08;从库服务器&…

Android MediaCodec 简明教程(四):使用 MediaCodec 将视频解码到 Surface,并使用 SurfaceView 播放视频

系列文章目录 Android MediaCodec 简明教程&#xff08;一&#xff09;&#xff1a;使用 MediaCodecList 查询 Codec 信息&#xff0c;并创建 MediaCodec 编解码器Android MediaCodec 简明教程&#xff08;二&#xff09;&#xff1a;使用 MediaCodecInfo.CodecCapabilities 查…

kafka集群搭建需要做的事情

首先&#xff0c;虚拟机克隆好之后的步骤如下&#xff1a; 1. 修改IP、主机名&#xff0c;关闭防火墙&#xff1b;&#xff08;reboot重启&#xff09; 2. 在/etc/hosts文件中进行IP与主机名的映射配置&#xff0c;集群中每天都记得配置&#xff1b; 3. 安装JDK并进行分发&a…

力扣题目训练(5)

2024年1月29日力扣题目训练 2024年1月29日力扣题目训练345. 反转字符串中的元音字母349. 两个数组的交集350. 两个数组的交集 II96. 不同的二叉搜索树97. 交错字符串44. 通配符匹配 2024年1月29日力扣题目训练 2024年1月29日第五天编程训练&#xff0c;今天主要是进行一些题训…

用友GRP-U8 forgetPassword_old.jsp SQL注入漏洞(QVD-2023-31085)

0x01 产品简介 用友GRP-U8R10行政事业内控管理软件是用友公司专注于国家电子政务事业,基于云计算技术所推出的新一代产品,是我国行政事业财务领域最专业的政府财务管理软件。 0x02 漏洞概述 用友GRP-U8R10行政事业内控管理软件 forgetPassword_old.jsp接口处存在SQL注入漏…

移动机器人正引领着高端芯片实现量产

在数字化时代&#xff0c;高端芯片已经成为推动各行各业创新发展的关键要素。而随着移动机器人技术的不断突破&#xff0c;高端芯片的量产也迎来了新的发展机遇。在这个过程中&#xff0c;移动机器人不仅为高端芯片提供了广阔的应用场景&#xff0c;还为其实现量产提供了强有力…

【Go】Viper读取配置文件

go get github.com/spf13/viper 1. 设置配置文件的信息 etcd:ip: "192.168.6.106"port: 2379dialTimeout: 3redis:ip: "192.168.6.107"port: 6379password: "root1028"2. 读取配置文件的信息 2.1 通过kv的方式 package mainimport ("fm…

【蓝桥杯日记】复盘篇二:分支结构

前言 本篇笔记主要进行复盘的内容是分支结构&#xff0c;通过学习分支结构从而更好巩固之前所学的内容。 目录 前言 目录 &#x1f34a;1.数的性质 分析&#xff1a; 知识点&#xff1a; &#x1f345;2.闰年判断 说明/提示 分析&#xff1a; 知识点&#xff1a; &am…