写点什么

面试必杀技!T6 大佬整理的 Spring 循环依赖,帮我斩获了 20k 的 offer

发布于: 2021 年 06 月 15 日
面试必杀技!T6大佬整理的Spring循环依赖,帮我斩获了20k的offer

今日分享开始啦,请大家多多指教~

Spring 如何处理循环依赖?这是最近较为频繁被问到的一个面试题,这篇文章详细讲一下 Spring 中的循环依赖的处理方案。

什么是循环依赖

依赖指的是 Bean 与 Bean 之间的依赖关系,循环依赖指的是两个或者多个 Bean 相互依赖,如:

构造器循环依赖

代码示例:

配置文件

Setter 循环依赖

代码示例

配置文件

循环依赖包括: 构造器注入循环依赖 set ,注入循环依赖 和 prototype 模式 Bean 的循环依赖。Spring 只解决了单利 Bean 的 setter 注入循环依赖,对于构造器循环依赖,和 prototype 模式的循环依赖是无法解决的,在创建 Bean 的时候就会抛出异常 :“BeanCurrentlyInCreationException” 。

循环依赖控制开关在 AbstractRefreshableApplicationContext 容器工厂类中有定义:

默认情况下是允许 Bean 之间的循环依赖的,在依赖注入时 Spring 会尝试处理循环依赖。如果将该属性配置为“false”则关闭循环依赖,当在 Bean 依赖注入的时遇到循环依赖时抛出异常。可以通过如下方式关闭,但是一般都不这么做。

构造器循环依赖处理

构造器是不允许循环依赖的,动动你的小脑瓜想一想,比如:A 依赖 B ,B 依赖 C,C 依赖 A,在实例化 A 的时候,构造器需要注入 B,然后 Spirng 会实例化 B,此时的 A 属于“正在创建”的状态。当实例化 B 的时候,发现构造器需要注入 C,然后去实例化 C,然而实例化 C 的时候又需要注入 A 的实例,这样就造成了一个死循环,永远无法先实例化出某一个 Bean,所以 Spring 遇到这里构造器循环依赖会直接抛出异常。

那么 Spring 到底是如何做的呢?

  1. 首先 Spring 会走 Bean 的实例化流程尝试创建 A 的实例 ,在创建实例之间先从 “正在创建 Bean 池” (一个缓存 Map 而已)中去查找 A 是否正在创建,如果没找到,则将 A 放入 “正在创建 Bean 池”中,然后准备实例化构造器参数 B。

  2. Spring 会走 Bean 的实例化流程尝试创建 B 的实例 ,在创建实例之间先从 “正在创建 Bean 池” (一个缓存 Map 而已)中去查找 B 是否正在创建,如果没找到,则将 B 放入 “正在创建 Bean 池”中,然后准备实例化构造器参数 A。

  3. Spring 会走 Bean 的实例化流程尝试创建 A 的实例 ,在创建实例之间先从 “正在创建 Bean 池” (一个缓存 Map 而已)中去查找 A 是否正在创建。

  4. 此时:Spring 发现 A 正处于“正在创建 Bean 池”,表示出现构造器循环依赖,抛出异常:“BeanCurrentlyInCreationException”。

DefaultSingletonBeanRegistry#getSingleton

下面我们以 BeanA 构造参数依赖 BeanB, BeanB 构造参数依赖 BeanA 为例来分析。

当 Spring 的 IOC 容器启动,尝试对单利的 BeanA 进行初始化,根据之前的分析我们知道,单利 Bean 的创建入口是 AbstractBeanFactory#doGetBean 在该方法中会先从单利 Bean 缓存中获取,如果没有代码会走到:

DefaultSingletonBeanRegistry#getSingleton(jString beanName, ObjectFactory<?> singletonFactory) 方法中 ,在该方法中会先对把创建的 Bean 加入一个名字为

singletonsCurrentlyInCreation 的 ConcurrentHashMap 中,意思是该 Bean 正在创建中,然后调用 ObjectFactory.getObject() 实例化 Bean , 假设 BeanA 进入了该方法进行实例化:


beforeSingletonCreation 方法非常关键 ,它会把 beanName 加入 singletonsCurrentlyInCreation,一个代表“正在创建中的 Bean”的 ConcurrentHashMap 中。

  • 如果 singletonsCurrentlyInCreation 中没该 beanName,就把该 Bean 存储到 singletonsCurrentlyInCreation 中,

  • 如果 singletonsCurrentlyInCreation 中有 该 Bean,就报错循环依赖异常 BeanCurrentlyInCreationException。

【注意】也就意味着同一个 beanName 进入该方法 2 次就会抛异常 , 现在 BeanA 已经加入了 singletonsCurrentlyInCreation。

AbstractAutowireCapableBeanFactory#autowireConstructor

我们前面分析过 ObjectFactory.getObject 实例化 Bean 的详细流程,这里我只是大概在复盘一下就行了。因为我们的 BeanA 的构造器注入了一个 BeanB,所以 代码最终会走到 AbstractAutowireCapableBeanFactory#autowireConstructor ,通过构造器来实例化 BeanA。

在 autowireConstructor 方法中会通过 ConstructorResolver#resolveConstructorArguments 来解析构造参数,调用 BeanDefinitionValueResolver 去把 ref="beanB" 这种字符串的引用变成一个实实在在的 Bean,即 BeanB,所以在 BeanDefinitionValueResolver 属性值解析器中又会去实例化 BeanB,同样会走到 DefaultSingletonBeanRegistry#getSingleton 中把 BeanB 加入 singletonsCurrentlyInCreation “正在创建 Bean 池”中,然后调用 ObjectFactory.getObject 实例化 BeanB。

低于 BeanB 而已同样需要通过构造器创建,BeanB 构造器参数依赖了 BeanA,也就意味着又会调用 BeanDefinitionValueResolver 去把 ref=“beanA” 这种字符串引用变成容器中的 BeanA 的 Bean 实例,然后代码又会走到 DefaultSingletonBeanRegistry#getSingleton。然后再一次地尝试把 BeanA 加入 singletonsCurrentlyInCreation “正在创建 Bean 池”。

此时问题就来了,在最开始创建 BeanA 的时候它已经加入过一次“正在创建 Bean” 池,这会儿实例化 BeanB 的时候,由于构造器参数依赖了 BeanA,导致 BeanA 又想进入“正在创建 Bean” 池 ,此时 Spring 抛出循环依赖异常:

Error creating bean with name ‘beanA’: Requested bean is currently in creation: Is there an unresolvable circular reference?

到这,Spring 处理构造器循环依赖的源码分析完毕。

setter 循环依赖处理

setter 循环依赖是可以允许的。Spring 是通过提前暴露未实例化完成的 Bean 的 ObjectFactory 来实现循环依赖的,这样做的目的是其他的 Bean 可以通过 ObjectFactory 引用到该 Bean。

实现流程如下:

  • Spring 创建 BeanA,通过无参构造实例化,并暴露一个 ObjectFactory,用来获取创建中的 BeanA,然后把 BeanA 添加到“正在创建 Bean 池”中,然后通过 setter 注入 BeanB。

  • Spring 创建 BeanB,通过无参构造实例化,并暴露一个 ObjectFactory,用来获取创建中的 BeanB,然后把 BeanB 添加到“正在创建 Bean 池”中,然后通过 setter 注入 BeanA。

  • 在 BeanB 通过 setter 注入 BeanA 时,由于 BeanA 提前暴露了 ObjectFactory ,通过它返回一个提前暴露一个创建中的 BeanA。

  • 然后完成 BeanB 的依赖注入。

AbstractAutowireCapableBeanFactory#doCreateBean

我们以 BeanA 通过 settter 依赖 BeanB,BeanB 通过 setter 依赖 BeanA 为例来分析一下源码,在之前的 Bean 实例化流程分析过程中我们了解到,Bean 的实例化会走 AbstractBeanFactory#doGetBean,然后查找单利缓存中是否有该 Bean ,如果没有就调用 DefaultSingletonBeanRegistry#getSingleton,方法会把 BeanA 加入 singletonsCurrentlyInCreation “创建中的 Bean 池”,然后调用 ObjectFactory.getObject 创建 Bean。

org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean 源码:

第一次进来,缓存中是没有 BeanA 的,所以会走 getSingleton 方法,然后代码最终会走到 AbstractAutowireCapableBeanFactory#doCreateBean 方法中 。

AbstractAutowireCapableBeanFactory#doCreateBean 源码:



该方法中把 BeanA 实例化好之后,会把 ObjectFactory 存储到一个 singletonFactories (HashMap)中来提前暴露 Bean 的创建工厂,用于解决循环依赖【重要】,然后调用 populateBean 走属性注入流程。

属性注入会通过 BeanDefinition 得到 bean 的依赖属性,然后调用 AbstractAutowireCapableBeanFactory#applyPropertyValues ,把属性应用到对象上。在 applyPropertyValues 方法中最终调用 BeanDefinitionValueResolver#resolveValueIfNecessary 解析属性值,比如:ref=“beanB” 这种字符串引用变成 对象实例的引用。

在 BeanDefinitionValueResolver 解析依赖的属性值即:BeanB 的时候,同样会触发 BeanB 的实例化,代码会走到 AbstractBeanFactory#doGetBean ,然后走方法 DefaultSingletonBeanRegistry#getSingleton 中把 BeanB 加入 singletonsCurrentlyInCreation “创建中的 Bean 池”,然后代码会走到 AbstractAutowireCapableBeanFactory#doCreateBean 方法中创建 BeanB,

该方法中会先实例化 BeanB,接着会把 BeanB 的 ObjectFactory 存储到 singletonFactories (HashMap)中来提前暴露 Bean 的创建工厂,用于解决循环依赖,然后调用 populateBean 走属性注入流程。

同样因为 BeanB 通过 Setter 注入了 A,所以在 populateBean 属性注入流程中会解析 ref=“beanA” 为容器中的 BeanA 的实例。

然后会走到 AbstractBeanFactory#doGetBean 中获取 BeanA 的实例。这个时候流程就不一样了,我们先看一下 AbstractBeanFactory#doGetBean 中的代码

在获取单利 Bean 的实例的时候是会先去单利 Bean 的缓存中去查看 Bean 是否已经存在,如果不存在,才会走 DefaultSingletonBeanRegistry#getSingleton 方法创建 Bean。

问题是:此刻单利 Bean 缓存中已经有 BeanA 了,因为在最开始 BeanA 已经处于“正在创建 Bean 池”中了。我们先来看一下是如何从缓存获取 Bean 的。

DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)源码如下:


这里就是经典的三级缓存解决 Spring 循环依赖。你看到了,这里会先从 singletonObjects 单利 Bean 缓存集合中获取 Bean(该缓存是实例化完成了的 Bean),如果没有,就从 earlySingletonObjects 早期对象缓存中获取 Bean(该缓存中存放的是还未实例化完成的早期 Bean),如果还是没有,就从 singletonFactories 中得到暴露的 ObjectFactory 来获取依赖的 Bean。然后放入早期缓存中。并把 ObjectFactory 从 singletonFactories 中移除。最后返回 Bean 的实例。

由于在实例化 BeanA 的时候已经把 BeanA 的 ObjectFactory 添加到了 singletonFactories 缓存中,那么这里就会走到 singletonFactory.getObject(); 方法得到 BeanA 的实例,并且会把 BeanA 存储到 earlySingletonObjects 早期单利 Bean 缓存中。

BeanA 的实例成功返回,那么 BeanB 的 setter 注入成功,代表 BeanB 实例化完成,那么 BeanA 的 setter 方法注入成功,BeanA 实例化完成。

prototype 模式的循环依赖

对于 prototype 模式下的 Bean 不允许循环依赖,因为 这种模式下 Bean 是不做缓存的,所以就没法暴露 ObjectFactory,也就没办法实现循环依赖。

总结

不知道你有没有看晕,反正我但是在源码时的过程是比较辛苦的~~~~(>_<)~~~~ ,这里需要你对前面 Bean 的实例化流程和属性注入流程比较熟悉,否则就会晕菜。

这里总结一下:

  1. 构造器循环依赖是不允许的,主要通过 singletonsCurrentlyInCreation “正在创建 Bean 池” 把创建中的 Bean 缓存起来,如果循环依赖,同一个 Bean 势必会尝试进入该缓存 2 次,抛出循环依赖异常。

  2. setter 循环依赖是可以允许的。Spring 是通过提前暴露未实例化完成的 Bean 的 ObjectFactory 来实现循环依赖的,这样做的目的是其他的 Bean 可以通过 ObjectFactory 引用到该 Bean 。 在获取依赖的 Bean 的时候使用到了三级缓存。

今日份分享已结束,请大家多多包涵和指点!

用户头像

还未添加个人签名 2021.04.20 加入

Java工具与相关资料获取等WX: pfx950924(备注来源)

评论

发布
暂无评论
面试必杀技!T6大佬整理的Spring循环依赖,帮我斩获了20k的offer