不懂就问,Spring 是如何判定原型循环依赖和构造方法循环依赖的?

Posted Java知音_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了不懂就问,Spring 是如何判定原型循环依赖和构造方法循环依赖的?相关的知识,希望对你有一定的参考价值。


作者:青石路

cnblogs.com/youzhibing/p/14514823.html

写在前面

Spring 中常见的循环依赖有 3 种:单例 setter 循环依赖、单例构造方法循环依赖、原型循环依赖

关于单例 setter 循环依赖,Spring 是如何甄别和处理的,可查看:Spring 的循环依赖,源码详细分析 → 真的非要三级缓存吗

单例构造方法循环依赖

何谓单例构造方法循环依赖了,我们看具体代码就明白了

两个要素:① scope 是默认值,也就是 singleton;② 多个实例之间通过构造方法形成了循环依赖

这种情况下,Spring 是怎么处理的了,我们先来看看执行结果

Spring 启动过程中报错了:Error creating bean with name 'cat': Requested bean is currently in creation: Is there an unresolvable circular reference?

问题就来了:Spring 是如何甄别单例情况下的构造方法循环依赖的,然后进行报错的

大家先把这个问题暂留在心里,我们再来看看什么是原型循环依赖

原型循环依赖

同样,我们直接看代码就明白何谓原型循环依赖了

同样是 2 个要素:① scope 不是默认值,而是 prototype,也就是原型,每次获取该实例的时候都会新建;② setter 循环依赖

这种情况下 Spring 又会有什么样的执行结果了

Spring 启动正常,但从 Spring 容器获取 loop 实例的时候,报了同样的错误

问题来了:① Spring 是如何甄别原型循环依赖的,然后进行报错提示的

② 为什么两种情况的报错时机不一致,一个在 Spring 启动过程中,一个却在使用 Spring 的过程中

示例代码地址:

https://gitee.com/youzhibing/spring-circle/tree/master/spring-circle-dependence-type

上面的 3 个问题,概括下就是

  • Spring 是如何甄别单例情况下的构造方法循环依赖的

  • Spring 是如何甄别原型循环依赖的

  • 为什么单例构造方法循环依赖和原型循环依赖的报错时机不一致

我们慢慢往下看,跟源码的过程可能比较快,大家看仔细了

还是那句话

看完之后仍有疑问,可以评论区留言,也可以自行去查阅相关资料进行解疑

源码起点

Spring 读取和解析 xml 的过程,我们就不去跟了,我们重点跟一下我们关注的内容

我们从 DefaultListableBeanFactory 类的 preInstantiateSingletons 方法作为起点

按如下顺序可以快速的找到起点,后面两种情况都从此处开始进行源码跟踪

构造方法循环依赖的甄别

闲话少说,我们直接开始跟源码

获取 cat 实例

cat 的 RootBeanDefinition 中有几个属性值得我们注意下

接着往下走

我们来到了 createBeanInstance 方法,此时 Set<String> singletonsCurrentlyInCreation 只存放了 cat

singletonsCurrentlyInCreation 看字面意思就知道,存放的是当前正在创建中的单例对象

我们接着往下跟

由于 constructorArgumentValues 中有元素,所以需要通过有参构造函数来创建 cat 对象

因为构造函数的参数是 Dog 类型的 dog ,所以通过反射调用 Cat 的有参构造函数来创建 cat 之前,需要先从 Spring 容器中获取到 dog 对象

获取 Cat 构造函数依赖的 dog 实例

所以流程又来到了我们熟悉的 getBean ,只是现在获取的是 dog ;获取流程与获取 cat 时一样,所以跟的速度会快一些,大家注意看我停顿的地方

此时 singletonsCurrentlyInCreation 存放了 cat 和 dog ,表示他们都在创建中

又来到了 createBeanInstance ,过程与之前 cat 的过程一样,我们接着往下看

又来到了熟悉的 getBean ,需要从 Spring 容器获取 Dog 构造函数依赖的 cat 对象

获取 Dog 构造函数依赖的 cat 对象

接下来重点来了,大家看清楚了

因为 singletonsCurrentlyInCreation 已经存在 cat 了, !this.singletonsCurrentlyInCreation.add(beanName) 结果就是 true

说明陷入死循环了,所以抛出了 BeanCurrentlyInCreationException

我们在控制台看到的异常信息就从这来的

原型循环依赖的甄别

原型类型的实例有个特点:每次获取都会重新创建一个实例,那在 Spring 启动过程中,还有创建的必要吗?

Spring 启动不创建 prototype 类型的实例

我们来跟下源码就明白了

关键代码

不符合上述 3 个条件的实例,在 Spring 启动过程中都不会被创建

下面接着讲正题,来看看 Spring 是如何甄别原型循环依赖的

获取 loop 实例

在 loop 实例创建之前,调用了 beforePrototypeCreation 方法,将 loop 名放到了 ThreadLocal<Object> prototypesCurrentlyInCreation

表示当前线程正在创建 loop ,我们接着往下看

原型类型的对象创建过程分两步:① 实例化(反射调构造方法),② 初始化(属性填充),和单例类型对象的创建过程是一样的

依赖的处理是在初始化过程中进行的, loop 对象依赖 circle 属性,所以对 loop 对象的 circle 属性进行填充的时候,需要去 Spring 容器获取 circle 实例

又来到了我们熟悉的 getBean ,获取 loop 依赖的 circle 实例,我们继续往下跟

circle 对象创建之前,同样调用了 beforePrototypeCreation 方法,那么此时 prototypesCurrentlyInCreation 中就同时存在 loop 和 circle

表示当前线程正在创建 loop 实例和 circle 实例;继续往下走

兜兜转转又来到了 getBean ,获取 circle 对象依赖的 loop 属性,接下来是重点,大家看仔细了

因为 prototypesCurrentlyInCreation 中存在 loop 了,说明当前线程正在创建 loop 实例

而现在又要创建新的 loop ,说明陷入死循环了,所以抛出了 BeanCurrentlyInCreationException

总结

经过上面的梳理,相信大家对之前的三个问题都没有疑问了,我们来总结下

1、Spring 是如何甄别单例情况下的构造方法循环依赖的

Spring 通过 Set<String> singletonsCurrentlyInCreation 记录当前正在创建中的实例名称

创建实例对象之前,会判断 singletonsCurrentlyInCreation 中是否存在该实例的名称,如果存在则表示死循环了,那么抛出 BeanCurrentlyInCreationException

2、Spring 是如何甄别原型循环依赖的

Spring 通过 ThreadLocal<Object> prototypesCurrentlyInCreation 记录当前线程正在创建中的原型实例名称

创建原型实例之前,会判断 prototypesCurrentlyInCreation 中是否存在该实例的名称,如果存在则表示死循环了,那么抛出 BeanCurrentlyInCreationException

3、为什么单例构造方法循环依赖和原型循环依赖的报错时机不一致

单例构造方法实例的创建是在 Spring 启动过程中完成的,而原型实例是在获取的时候创建的

所以两者的循环依赖的报错时机不一致。另外,关注Java知音公众号,回复“后端面试”,送你一份面试题宝典!

推荐好文

>>【练手项目】基于SpringBoot的ERP系统,自带进销存+财务+生产功能>>分享一套基于SpringBoot和Vue的企业级中后台开源项目,代码很规范!
>>能挣钱的,开源 SpringBoot 商城系统,功能超全,超漂亮!


以上是关于不懂就问,Spring 是如何判定原型循环依赖和构造方法循环依赖的?的主要内容,如果未能解决你的问题,请参考以下文章

「 不懂就问 」esbuild 为什么这么快?

不懂就问」CPU 到底是怎么识别代码的?

不懂就问:为什么MySQL不建议使用NULL作为列默认值?

不懂就问,机器人做核酸是一种什么体验?|一周AI新闻

不懂就问,nrf51822,这个是蓝牙模块吗? 是用来给单片机串口变成蓝牙输出的是吗

不懂就问SpringFramework中的注解声明式事务怎么被Shiro搞失效了