一个bug引起的蝴蝶效应:Spring中Bean支持重复加载么?

导语:本次文章的背景,是基于spring框架,实现了一套监控组件,但是在某一次升级过程中,导致了一个问题,基于此本文聊一聊关于spring加载Bean的那些事~~~

一、背景

本人自定义了某一个组件Component-A,并集成了spring框架,而组件A依赖了另外一个封装好的组件Component-B及其中的BeanC。

当集成了组件Component-A的应用,那么就自动集成了组件Component-B和BeanC,使用方反馈,某些应用应用无法加载到BeanC,导致组件Component-A的使用异常,经过排查发现组件Component-B中的BeanC是在com.aaa.bbb.ccc包下,并做了如下的声明(这里为啥有这个异常,本人也做了反思:自己还是有点想当然了,认为团队的包都已经做了统一和一致的规约,所以也没有去仔细的核对其中的出入,其实每个团队可能或多或少都有自己的遗留系统或者问题,再进行二次开发或者对接的时候,需要细心细心再细心

//代码一
package com.aaa.bbb.ccc;
@Configuration
public class ComponentBConfig {

    @Bean
    public BeanC beanC() {
        return new BeanC();
    }
}

 而自定义的组件Component-A的扫描包是com.aaa.ddd,因此出现了某些应用无法扫描BeanC的,按理说如果在组件Component-A中新增如下注解就能顺利解决这个问题,也就没有这篇文章了。

//代码二
@ComponentScan(basePackages = {"com.aaa.bbb.ccc.*"})

 但是,命运弄人,由于自定义组件Component-A之前开发的时间过于久远(隔了一个春节),导致本人忘记了Component-B(其他人开发,之前首次对接的时候看过)的一些内容,认为其内部没有【代码一】的声明,所以为了解决无法扫描到Component-B组件的BeanC,鬼使神差的在本人的组件Component-A里面主动声明了和代码一类似的代码:

//代码三
package com.aaa.ddd;
@Configuration
public class ComponentBConfig {
/**这里本人还考虑到万一Component-B组件也被其他的组件依赖,也主动声明了该Bean,特意还加了@ConditionalOnMissingBean注解,以解决spring防止如果已经存在了该Bean在容器中,不重新注册**/
    @Bean
    @ConditiononalMissingBean
    public BeanC beanC() {
        return new BeanC();
    }
}

 然后接下去就是自己将测试版本集成到应用W(之前组件测试无异常)和应用V(后来组件测试有异常,扫描不到BeanC)中进行自测,没有任何问题,顺利解决缺少BeanC的问题,接着就开始大范围的在测试环境升级。

事情到这里,其实还没有结束,重点来了,过了半天,有应用X做了升级,发了测试环境,发现启动失败,应用都无法正常发布了,而且应用还是之前正常的应用,和我用来自测的应用W一样~~~WTF~~~,然后我去看了应用方相关的日志,如下:

 

 

二、问题排查和解决

看了报错日志,然后再次查询了组件B的代码,才意识到,组件B也进行了BeanC的申明,然后基于报错文案,去spring源码里面一搜,发现原来spring对bean的注册做了如下的处理:

所以基于此,分析一波并解决该问题

方案一:设置属性值

解决方案按照提示,可以通过显示的设置属性值解决

spring.main.allow-bean-definition-overriding=true

但是这个属性值影响的spring全局的体系,如何能轻易变动呢,万一导致其他脚手架组件异常咋办,风险和影响面都不允许该种解决方案,而且个人也不建议在团队协助开发中把这个值设置为true,所以考虑一番首先就排除该方案。

方案二:bean改名

看校验逻辑,既然是通过beanName进行重复校验,那么我把我声明的BeanC名称改一个,不也能解决么,但是该死的处女座完美主义使我做不了这个妥协,太不优雅了,果断放弃这个念头。

另外还有两个疑点,需要进一步排查

疑点一:

为啥自测的应用W和应用V没有问题,按理说应用W最开始经过测试的时候是正常的,而应用X确不行,我还特意在自己自定义的声明BeanC的时候考虑到这点,主动加了@ConditionalOnMissingBean注解,难道应用X中这个注解没生效?

分析一波疑问一:

@ConditionalOnMissingBean生效问题,了解这个注解的人应该都知道,这个如果是先扫描A组件的BeanC,再扫描B组件的BeanC,是没法生效的,再根据之前的异常日志中描述的谁和谁冲突,结合个人经验来判断,大概率原因应该是应用X是先扫描了组件A的BeanC声明,再扫描到组件B的BeanC声明,导致BeanC在注册的时候,无法通过Bean重复定义的校验,而测试用的应用W,先扫描了组件B的BeanC声明,再扫描到组件A的BeanC声明,而组件A扫描的时候,@ConditionalOnMissingBean应该生效了,所以没有进一步注册,被跳过,应用正常启动。为了求证,果断的对应用X开启了debug模式,并加了断点的条件便于调试,

可以看到在对进行BeanC注册的时候,组件B声明的已经被注册进去了

原来,果真如我们的猜测的那样,所以得出结论;

spring扫描Bean的时候,是不受控的,特别是不同的环境,扫描顺序也会有不同

疑点二:

应用X把原先方案一中提到的配置被主动设置为false了?(默认是true)

分析一波疑问二:

老套路,源码看起,看看到底是哪里把咱们的默认值true变为了false,我们把断点打在设置这个属性的方法上,再根据调用堆栈往回找,发现了一个很蛋疼的事

spring是将allowBeanDefinitionOverriding默认值设置为true的,但是,经过了springboot的包装,springboot把allowBeanDefinitionOverriding默认值设置为false了,然后把值设置到org.springframework.beans.factory.support.DefaultListableBeanFactory#allowBeanDefinitionOverriding属性上,

靠,看到这里,心里是不是有点瑟瑟发抖,细节无处不在啊~~~

好了,疑点分析完,接着讲方案,哈哈哈😂😂😂😂😂😂

基于上面的描述,我把Bean初始化顺序控制控制不就可以了,就有了如下-------->-------->

方案三:控制Bean初始化顺序

控制组件B的BeanC声明在自定义组件A的BeanC后面,加一个如下注解@AutoConfigurationAfter

在我们的组件A上,结合springboot的自动装配机制,并改造成spring-boot-starter(用上述注解,Spring建议结合spring-boot-starter模块机制使用),但是问题又来了,组件B的BeanC声明,不是基于springboot的自动装配机制的,也不是spring-boot-starter模块机制,要改组件B,还不如进行方案四的改造,如下-------->-------->

方案四:改造组件B

改动组件B的BeanC声明方式,也加个@ConditionalOnMissingBean注解,毕竟一些比较牛逼的开源组件,打通spring生态的时候,一般都是这么声明的,给用户自定义开放个口子,但是吧,个人又不太想这么改,还是想从自身的组件A去兼容这些特殊情况。

方案五:加个包扫描,去掉自己的自定义BeanC声明

回到问题原点,由于扫描包的配置没有完美的统一,导致某些应用没有扫描到组件B中的BeanC么,那么加一个包扫描得了,也能完美解决,然后果断的在我的组件A上加了如下注解,测试一波,完美解决!

@ComponentScan(basePackages = {"com.aaa.bbb.ccc.*"})

三、疑问再起

@ComponentScan(basePackages = {"com.aaa.bbb.ccc.*"}) 扫描包注解一加,不也是扫描到一样的BeanC进行注册么,怎么不会报错?带着疑问,看来还得去源码里面找一波答案🤔️🤔️🤔️🤔️

从上述截图中可以看出,扫描包进行Bean注册,他会进行防重复的校验,所以避免了重复Bean注册的异常。

到这里,就结束了么?

 

我又冒出一个问题:为啥spring在包扫描里面有这个防重注册判断,而比如以@Configuration+@Bean的形式,反而没有这一个防重注册判断机制呢? 

个人觉得到这里,虽然两种机制,都能使得spring进行bean的扫描并注册,但是,@ComponentScan包扫描无法精准的控制Bean的生命周期,同时很多其他开源组件,可能也会有类重复命名的情况,如果不做这一机制,可能会导致集成开源组件困难重重,而@Configuration+@Bean的形式,是一种精准的声明和控制,spring框架可能更加希望我们对自己能把控的Bean,做全面细致的考量,这也是springboot为啥又把allowBeanDefinitionOverriding默认值设置为false,类似的原因吧,个人理解,抛砖引玉,有知道设计背景或者其他想法的小伙伴,欢迎留言交流!

 

 

 

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值