记一次线上偶现的Spring循环依赖问题
共 4727字,需浏览 10分钟
·
2022-05-11 04:18
点击蓝色“程序员黄小斜”关注我哟
加个“星标”,每天和你一起多进步一点点!
前情回顾
一探
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 版本
一定要结合版本来看问题
版本不同,底层实现可能会不同
转自:青石路
链接: https://www.cnblogs.com/youzhibing/p/15835048.html
CTO 说了,不懂 @Autowired 和 @Resource 区别的人可以领盒饭了
国家出手,开始整治互联网加班了
京东裁员杀红眼了!说要给n+1,员工签字后,公司又反悔了!
— 【 THE END 】— 公众号[程序员黄小斜]全部博文已整理成一个目录,请在公众号里回复「m」获取! 最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 PDF 领取,更多内容陆续奉上。
文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)