再探循环依赖 → Spring 是如何判定原型循环依赖和构造方法循环依赖的?

开心一刻

  一天,侄子和我哥聊天,我坐在旁边听着

  侄子:爸爸,你爱我妈妈吗?

  哥:这话说的,不爱能有你吗?

  侄子:确定有我不是因为荷尔蒙吗?

  哥:因为什么荷尔蒙,因为爱情!

  侄子:那我妈花点钱,你咋老说呢?

  哥:这你就不懂了,挣钱本不易,花钱要仔细

  侄子:快得了吧,挣钱这么少,我妈都没跑,给你照顾家,钱还不让花

  哥:我发现你这孩子怎么不知道好赖呢,我攒钱不是为了给你去媳妇啊

  侄子:那你赶紧给我妈花吧,我妈要是跑了,你还得花钱娶一个,到最后,钱我捞不着,亲妈还混没了

  我:通透!!!

写在前面

  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 的过程中

  示例代码地址:spring-circle-dependence-type

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

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

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

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

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

  还是那句话

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

  源码起点

    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 启动过程中完成的,而原型实例是在获取的时候创建的

    所以两者的循环依赖的报错时机不一致

参考

  Spring 的循环依赖,源码详细分析 → 真的非要三级缓存吗

posted @ 2021-03-15 09:08  青石路  阅读(1603)  评论(2编辑  收藏  举报