三探循环依赖 → 记一次线上偶现的循环依赖问题

开心一刻

  心里一直在想明天该以何种方式祭拜列祖列宗,彻夜难眠,辗转反侧,最好下定了决心

  给弟发了个微信:别熬夜了,早上早点起来,咱俩去上坟

  弟:知道了,哥

  我:记得带上口罩

  弟:坟就在家后边的山上,这么近带什么口罩?

  我:就你这逼样,好意思见列祖列宗?

  弟:我知道了,那哥你带吗?

  我:我也带

前情回顾

  一探

  Spring 的循环依赖,源码详细分析 → 真的非要三级缓存吗 中讲到了循环依赖问题

  同样说明了 Spring 只能解决 setter 方式的循环依赖,不能解决构造方法的循环依赖

  重点介绍了 Spring 是如何解决 setter 方式的循环依赖,感兴趣的可以去看下

  二探

  既然 Spring 不能解决构造方法的循环依赖,那么它是如何甄别构造方法循环依赖的了?

  所以进行了二探:再探循环依赖 → Spring 是如何判定原型循环依赖和构造方法循环依赖的

  从源码的角度讲述了 Spring 是如何判定构造方法循环依赖、原型循环依赖的

  感兴趣的可以去看下

 

  大家跟源码的时候,一定要注意版本!!!

项目模拟

  自认为经过了前两探,对 Spring 循环依赖的问题已了若指掌,可面对线上突如其来的循环依赖问题,楼主竟然没能一眼看出来!!!

  这楼主能忍?于是楼主又跟起了 Spring 源码,看看问题到底出在哪?

   SpringBoot 版本是 2.0.3.RELEASE 

  线上服务采用 k8s 部署,本地环境未采用 k8s 部署

  本地启动从未出现循环依赖问题,线上环境也只是偶发的 pod 启动失败(提示信息直指循环依赖)

  问题偶发,而非必现,很是头疼,但问题还是得解决,从提示信息着手呗

  根据错误提示信息,楼主模拟出了一个简化的工程,方便我们进行问题排查

 

  非常简单,完整地址:spring-other-circular-reference

  我们来看下类图

   MyListener 、 MyService 、 MyManager 很常规,特殊的是 MyConfig 和 MySender 

问题复现

  如果按上述工程结构,本地很难复现问题 ,反正楼主是没复现出来

  我们稍做调整,将 MySender 前置,如下

  启动失败,错误信息如下:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'myConfig': Unsatisfied dependency expressed through field 'myListener'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'myListener': Unsatisfied dependency expressed through field 'myService'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'myServiceImpl': Unsatisfied dependency expressed through field 'myManager'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'myManager': Unsatisfied dependency expressed through field 'mySender'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'mySender': Requested bean is currently in creation: Is there an unresolvable circular reference?

  此刻的 Is there an unresolvable circular reference? 让楼主感到了陌生

问题分析

  我们从以下几个方面来分析

  BeanDefinition 扫描

  目前 XML 方式的 Bean 定义越来越少,除了一些遗留的老项目,基本看不到 XML 方式的 Bean 定义了

  所以我们只关注注解方式的 Bean 定义的扫描

  文件夹的扫描顺序与文件夹名字的升序一致,文件的顺序与文件名的升序一致,如下所示

  有兴趣的可以去跟下 ConfigurationClassParser 类中 doProcessConfigurationClass 方法;楼主做了下简单的总结

   @ComponentScan 的处理早于 @Bean 

   BeanDefinition 扫描过程中,会按扫描顺序会往 DefaultListableBeanFactory 的 beanDefinitionMap 中添加 BeanDefinition ,往 beanDefinitionNames 添加 BeanName 

  我们来跟下源码,看是不是如上所说

  先被扫描的 BeanDefinition 的 BeanName 会被先添加到 beanDefinitionNames 

  BeanDefinition 覆盖

   MyConfig 中通过 @Bean 定义了 MySender ,而 MySender 类上又用了 @Component 进行修饰

  那创建 MySender 实例的时候到底调用的哪个构造方法?(有参还是无参?)

  关于 Spring Boot 中创建对象的疑虑 → @Bean 与 @Component 同时作用同一个类,会怎么样?从源码的角度分析了这个问题

  结论是: SpringBoot 2.0.3.RELEASE 中, @Configuration + @Bean 修饰的 BeanDefinition 会覆盖掉 @Component 修饰的 BeanDefinition 

  也就说 MySender 类上的 @Component 其实没用,加不加效果是一样的,这里说的 没用效果 仅仅指的是 MySender 的 BeanDefinition 

  Bean 实例化顺序

   BeanDefinition 用来构建实例,那么 MySender 上的 @Component 就有作用了,它决定了 MySender 的实例化顺序

  是先于 MyConfig 、 MyListener 、 MyServiceImpl 、 MyManager 实例化的

  我们来看下 Bean 的实例化顺序

  理论上来讲,先被扫描的 Bean 会先被实例化; Bean 实例化的过程中会填充属性,可能会导致后被扫描的 Bean 提前被实例化

  如果 Bean 之间没有依赖,那么会严格按照 Bean 的扫描顺序实例化

  再看问题

  我们再回到前面的问题

  这种情况下,我们分析下 Is there an unresolvable circular reference? 是如何产生的

  相较于 MyConfig 、 MyListener 、 MyManager 、 MyServiceImpl , MySender 是最先被扫描到的,所以它最先被实例化

  因为 MyConfig 中通过 @Bean 修饰了 MySender 的 BeanDefinition 

  会覆盖掉 MySender 自身的无参 BeanDefinition 

  所以会通过 MySender 的有参构造方法来创建 MySender 实例

  因为有参构造方法依赖 myListener ,所以去 Spring 容器中找 MyListener 实例,没有找到则创建,然后填充 MyListener 实例的属性

  以此类推,实例的创建过程如下所示:

   Is there an unresolvable circular reference? 就此产生

  相当于是变种的构造方法循环依赖

  最初状态

  我们还原 MySender 位置

  此时最先实例化的是 MyConfig ,实例化过程如下

  对象是都可以正常实例化、初始化的

  这种情况理论上来讲是不会出现 Is there an unresolvable circular reference? 

  线上问题

  一通分析下来,还是没能找到线上 Is there an unresolvable circular reference? 的原因

  很是尴尬,但是我萌生了这样的想法:是不是在 k8s 部署过程中, BeanDefinition 的扫描会有偶发的随机性?

问题修复

  虽然我们没能找到线上问题的确切原因,但还是有办法去根治这个问题的

   Spring 不能处理构造方法循环依赖,那我们就去规避它

  删掉 MyConfig , MySender 改成

 

  或 MySender 改成

 

   还有 @PostConstruct 等,方式有很多,只要不产生构造方法循环依赖就好

 总结

  1、 BeanDefinition 扫描顺序

    如果我们去跟源代码就会发现,以启动类为起点,扫描启动类同级目录下的所有文件夹 

    按文件夹名升序顺序进行扫描,会递归扫描每个文件夹

    文件扫描也是按文件名升序顺序进行

    从线上问题来看,对这个扫描顺序,楼主是持怀疑态度的:是 Spring 会偶发的随机扫描,还是 pod 会导致偶发的随机扫描

  2、 BeanDefinition 覆盖

    只要我们读了源码,了解 Spring 对各个注解的扫描顺序,就清楚它们的替换关系了

     BeanDefinition 覆盖并不会影响 BeanDefinition 的扫描顺序

    也就是不会改变 BeanName 在 beanDefinitionNames 中的位置,即不会影响 Bean 的示例化顺序

  3、 Bean 实例化顺序

    理论上来讲,先被扫描到的就先被实例化,但实例化过程中的属性填充会打乱这个顺序,会将被依赖的对象提前实例化

  4、 Spring 版本

    一定要结合版本来看问题

    版本不同,底层实现可能会不同