SpringBoot @ConditionalOnBean实现原理
在SpringBoot1.5.X时,判断条件是OR,SpringBoot2.x变为了AND
在spring 启动过程中,会调用BeanDefinitionRegistryPostProcessor的postProcessBeanDefinitionRegistry后置处理器 调用ComponentScanAnnotationParser的parse(..)方法解析配置类,实现类路径下的类文件扫描,
遍历被扫描到的类,并通过@ComponentScan注解上指定的规则进行匹配,默认情况下,SpringBoot扫描@Component标记的类,( 但是在spring.factories中通过key org.springframework.boot.autoconfigure.EnableAutoConfiguration指定配置不会被扫描. 这些配置类会由@EnableAutoConfiguration导入的AutoConfigurationImportSelector来处理);
如果出现匹配的类还会通过ConditionEvaluator.shouldSkip(...)来判断是否忽略这个类,这时候会过滤掉小部分无效类,但是不会在这个期间应用@ConditionalOnBean去验证,
因为其验证实现类OnBeanCondition实现了接口ConfigurationCondition的getConfigurationPhase()方法,该方法返回值是一个枚举值,用于判断什么时期应用这个条件验证, shouldSkip()方法会判断这个接口返回值,OnBeanCondition实现的getConfigurationPhase()返回的是ConfigurationPhase.REGISTER_BEAN,而类扫描期间只会执行没有实现ConfigurationCondition接口,或返回值为null,或为ConfigurationPhase.PARSE_CONFIGURATION的验证器.
所以这个期间不会应用@ConditionalOnBean去验证. 这也可以理解因为这个时候有很多类没有被遍历到,现在执行OnBeanCondition验证肯定多数是false.
这也导致很多最终不会被实例化为Spring bean的类被包装成BeanDefinition(实现类ScannedGenericBeanDefinition ),并注册在DefaultListableBeanFactory 的成员beanDefinitionMap中,key为Bean的名称,同时Bean的名称会保存在List集合beanDefinitionNames中, 对于一些手动注册的Bean的名称会保存在List集合manualSingletonNames中,
然后对于parse(..)返回的BeanDefinition集合会遍历调用ConfigurationClassParser的parse(...)递归调用, 当解析到配置中有@Bean定义的Bean,并有条件注解时,会调用ConditionEvaluator.shouldSkip(...)方法,最终调用到的是 Condition实现类的matches(...)方法.
而@ConditionalOnBean注解中的验证实现类OnBeanCondition,最终判断是否有指定的bean就是通过DefaultListableBeanFactory 中的beanDefinitionMap和manualSingletonNames是否包含指定的Bean的名称来验证的.
当然对于扫描到的无效类,在ConfigurationClassPostProcessor的processConfigBeanDefinitions(...)调用parse()方法后会调用this.reader.loadBeanDefinitions(configClasses),来判断最终有效的BeanDefinition,这时候就会执行OnBeanCondition, 移除beanDefinitionMap中无效的BeanDefinition.
鉴于类路径扫描规则和OnBeanCondition验证实现原理,就可能导致无效问题
例如如下代码:
C希望不存在A Bean时创建,B希望在C bean存在时创建,示例中A配置了Bean,这时B C都不应该创建,但执行结果是C确实没有创建,而B却被创建了.
原因就在于类扫描时,C被扫描了,导致beanDefinitionMap存在C 的BeanDefinition.
但是C在this.reader.loadBeanDefinitions(configClasses),中由于条件验证不通过没有创建被移除.(当然和类扫描顺序也有关系,如果B能够在C之后被扫描到,就会执行正常,所以Spring官网建议通过spring.factories来配置,指定执行顺序或者在能够确定执行顺序的情况下使用@ConditionalOnBean,但是一定不能通过文件名来控制顺序)
注意点:
如果将示例中的类配置到spring.factories中即时不指定顺序,也能正常执行,不会创建B C,这是由于spring.factories配置文件中的类并不是全部包装成BeanDefinition然后判断是否有效的,而是排序后封装成ConfigurationClass通过遍历的方式,同样是在this.reader.loadBeanDefinitions(configClasses)方法中判断该类是否通过条件验证是否有效.
当执行到B是,C还没有被处理,所以导致B也不会被创建,(这有点属于巧合导致的正确结果,所以为了安全还是要执行顺序,让B在C之后被处理)
第二种情况, 在配置类中定义Bean,如果使用@ConditionalOnBean依赖的也是配置类中Bean,则执行结果不可控,和配置类加载顺序有关。
示例代码如下:代码中是希望Bean3被创建,但是实际上并没有!!!
解决有两种方式:
1 可以考虑使用@DependsOn来解决,参看Spring中@DependsOn注解的作用及实现原理。
2 如果一定要区分两个配置类的先后顺序,可以将这两个类交与EnableAutoConfiguration管理和触发。 也就是定义在META-INF\spring.factories中声明是配置类, 然后通过@AutoConfigureBefore、AutoConfigureAfter、AutoConfigureOrder控制先后顺序。 因为这三个注解只对自动配置类生效。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。