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

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

开心一刻

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

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

  弟:知道了,哥

  我:记得带上口罩

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

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

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

  我:我也带

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

前情回顾

  一探

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

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

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

  二探

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

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

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

  感兴趣的可以去看下

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

项目模拟

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

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

  SpringBoot版本是2.0.3.RELEASE

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

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

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

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

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

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

  我们来看下类图

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

  MyListenerMyServiceMyManager很常规,特殊的是MyConfigMySender

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

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

问题复现

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

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

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

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

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定义的扫描

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

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

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

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

  @ComponentScan的处理早于@Bean

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

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

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

  先被扫描的BeanDefinitionBeanName会被先添加到beanDefinitionNames

  BeanDefinition 覆盖

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

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

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

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

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

  Bean 实例化顺序

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

  是先于MyConfigMyListenerMyServiceImplMyManager实例化的

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

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

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

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

  再看问题

  我们再回到前面的问题

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

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

  相较于MyConfigMyListenerMyManagerMyServiceImplMySender是最先被扫描到的,所以它最先被实例化

  因为MyConfig中通过@Bean修饰了MySenderBeanDefinition

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

  会覆盖掉MySender自身的无参BeanDefinition

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

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

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

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

  Is there an unresolvable circular reference?就此产生

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

  最初状态

  我们还原MySender位置

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

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

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

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

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

  线上问题

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

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

问题修复

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

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

  删掉MyConfigMySender改成

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

  或MySender改成

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

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

总结

  1、BeanDefinition扫描顺序

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

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

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

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

  2、BeanDefinition覆盖

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

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

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

  3、Bean实例化顺序

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

  4、Spring版本

    一定要结合版本来看问题

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

文章来源:https://www.cnblogs.com/youzhibing/p/15835048.html

本文经用户投稿或网站收集转载,如有侵权请联系本站。

发表评论

0条回复