为什么Spring Boot条件注解那么多,而标题中是@ConditionalOnBean呢?
因为,相比之下我们用的比较多的条件装配注解也就是@ConditionalOnClass、@ConditionalOnBean了,而@ConditionalOnClass对顺序并不敏感(说白了就是判断类加载器是否可以在其路径下加载到Class,所以和Spring的处理顺序没啥关系),但是@ConditionalOnBean就不同了,如果顺序无法保证,那么自动装配中的@ConditionalOnBean就可能会失效。
还有一点需要强调一下,正如Spring Boot官方建议的那样,请在自动装配类中使用条件装配注解,不要在自己定义普通配置类(普通配置类指的是我们自定义的@Configuration配置类)中使用,在普通配置类中使用条件装配注解,能不能生效那就看命了,尤其是@ConditionalOnBean这种对顺序敏感的注解;可能会出现在IDE中生效,但是到了线上就不生效了,因为你光靠ClassLoader的加载顺序是不靠谱的,在不同的操作系统环境下,class文件的加载顺序存在不确定性,文件是由文件系统管理,不同的文件管理系统有不同的机制。
可能有些人会想,我自定义@Configuration的普通配置类,我自己来显示的控制加载顺序,不过有一点请注意,你要控制的是BeanDefinition的注册顺序,而不是Bean的注入顺序;
我能想到的唯一可以控制BeanDefinition注册顺序的方式就是:自定义一个实现BeanDefinitionRegistryPostProcessor接口的实现类,并且还要实现PriorityOrder接口,要保证顺序比ConfigurationClassPostProcessor靠前,这样才有机会提前注册BeanDefinition到容器中;当然你也可以定义一个ApplicationContextInitializer接口实现类,然后通过initialize方法将自定义的BeanDefinitionRegistryPostProcessor接口的实现类添加到容器中,这样不用实现PriorityOrder接口也可以保证在ConfigurationClassPostProcessor前面执行。
不过上面说的BeanDefinitionRegistryPostProcessor接口实现类向容器添加BeanDefinition属于歪门邪道吧,明明正常扫描@Configuration注解来注册的配置类,非要把@Configuration注解去掉或者放到@ComponentScan扫不到的包下,然后由BeanDefinitionRegistryPostProcessor接口硬编码注入,应该也没有人会这么编码。
所以请切记,条件装配注解并不是无敌的,也是需要考虑使用场景的,不能随便的滥用,因此尽量在自动装配配置类中使用条件装配注解!!!
@ConditionalOnBean判断的是容器中是否存在BD,而不是判断的容器中是否存在Bean对象,这一点请注意。
为什么要保证顺序
我们举个例子来说一下@ConditionalOnBean注解处理顺序的重要性。
例子中是@ConditionalOnMissingBean注解,但是和@ConditionalOnBean注解的逻辑基本一致的。下面代码表达的意思就是,我们定义的普通配置类中,对RedisTemplete进行了个性化设置,此时我们肯定希望容器中只存在我们自定义的RedisTemplate;而Spring Boot的自动装配的条件装配机制也确实满足了我们的这种需求;
@Configuration
public class RedisConfig {//这是我们自定义的普通配置类
@Bean
public RedisTemplate
我们假设一下:如果RedisAutoConfiguration自动装配配置类在我们自定义的RedisConfig普通配置类前面执行了,那就会导致当时的容器中不存在名字为redisTemplate的bean对象,当处理我们自定义的RedisConfig配置类的时候,就会报错,因为存在同名的bean了。
总而言之,对于@ConditionalOnBean注解来说,顺序很重要,是必须要保证的。
关于原理其实还是挺多内容的,因为需要足够清楚Spring的ConfigurationClassPostProcessor以及Spring Boot的AutoConfigurationImportSelector,这样才能彻底掌握原理中的细节内容。考虑到篇幅问题,我们还是以关键逻辑为主,弄清楚@ConditionalOnBean顺序是怎么保证的即可。
首先我们说几点比较关键的基础知识。
可能对不熟悉Spring的人来说,没办法将上述几点串联起来。所以,我们在花费一些篇幅来详细的解释下上面几个点。
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
......此处省略很多无关内容
}
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
......此处省略一些无关内容
}
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
......此处省略很多无关内容
}
public interface DeferredImportSelector extends ImportSelector {
......此处省略一些无关内容
}
通过代码示例,我们可以清楚的看到,在编码层面,这几个注解、接口以及实现类已经串起来了。
下面画个流程简图,来说明一下Spring是如何保证顺序的。
绿色为相关度较高的关键代码逻辑;红色条件装配的判断节点;
我们再用文字把流程中的关键点整理一下:
虽然@ConditionalOnBean是Spring Boot的注解,但是我们通过流程图发现,大部分核心的代码逻辑都是属于Spring的。因此,@ConditionalOnBean条件装配的顺序就是Spring的DeferredImportSelector接口延迟处理机制来保证的。
对于熟悉Spring源码的人来说,可能看这篇文章很容易;如果不太熟悉,可以了解一下ConfigurationClassPostProcessor这个类,因为对于注解驱动(使用xml配置文件的项目越来越少了)的项目来说,这个类真的是太重要了。这个类处理的是BeanDefinition阶段,因此可以不用对Bean注册阶段有过多的了解。了解了ConfigurationClassPostProcessor这个类,那么读懂条件装配的原理,并且掌握代码逻辑细节,那肯定是轻轻松松儿的。
下面是我整理的关于 ConfigurationClassPostProcessor的主要逻辑流程图,也附上,有需要的可以看看。