1 .前言
本文基于 spring-boot 2.2.2.RELEASE 版本
@Conditional 注解在 spring-boot中大量使用,是 spring-boot 自动配置不可缺少的一环,本文将讲解 @Conditional 的运行机制,涉及大量源码
如果觉得枯燥可以直接拉到最后看结论
@Conditional 虽然在spring-boot 中大量使用,但是有的同学可能觉得很陌生,从来没使用过这个注解,但是你一定 见过/用过 他的子注解,比如
ConditionalOnBean,ConditionalOnClass,ConditionalOnProperty
如果上面 几个注解你也没见过, 请自行谷歌,篇幅的关系就不讲解了
这里涉及到spring的一个基础知识点, 注解的继承,例如 自定义一个 @B,这个 @B 上标注了 @A, 那么这个 @B 可以看做 @A 注解的子注解
@A
public @interface B {
}
我们常用的 @Service ,@Configuration, @Controller 都是 注解 @Component 的子类,所以 这 几个注解都有同样把 class 注入进 ioc
的能力 下面是 @Controller 定义的源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
这里 @AliasFor 可以看成 在子类里重写了 父类 Component 的方法 value()
2.spring-boot 注册bean流程
因为 @Conditional 的意义就是在 控制 是否要把对应的 bean 注册到 IOC容器中,那么要探究其原理,就必须了解spring-boot 是如何注册 bean的
整个注册 bean 的流程在 org.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitions(BeanDefinitionRegistry registry)
我这边把整个 流程 分成了2部分
(1) 加载
spring 去扫描了所有 class 文件,然后解析了所有的 @bean 以及 @Import 注解

上图比较抽象,大概用语言描述一下:
上述流程来自 org.springframework.context.annotation.ConfigurationClassParser#doProcessConfigurationClass
有一个 叫做 doProcessConfigurationClass 的方法 是用来解析配置类的,其实也就是我上图画的那个大框
这个方法首先传入的是一个 带有@SpringBootApplication 注解的类(在普通的spring-boot项目中,就是main函数所在那个类),
这个 @SpringBootApplication 注解里面 继承了 注解@ComponentScan 所以他在 doProcessConfigurationClass 里面执行的时候会先去扫描
这个注解所指定路径的所有类,然后通过递归的方式 让每一个新扫描出来的类去执行 doProcessConfigurationClass 方法
同时这个doProcessConfigurationClass 方法
还去解析了 @Bean @Import 注解,找到所有可能会注册进入IOC容器到的对象,生成对象 configurationClass 放入 全局缓存中,当然还包括这个类本身,也会生成
configurationClass 放入缓存中
千言万语 不如看代码, 这是简化后的流程
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws Exception {
ClassMetadata metadata = sourceClass.getMetadata();
if(!(metadata instanceof AnnotationMetadata)){
return null;
}
// 拿到配置类上 所有注解
AnnotationMetadata annotationMetadata = (AnnotationMetadata) metadata;
//拿到注解 ComponentScan 上面的所有属性
AnnotationAttributes componentScan = annotationMetadata.getAnnotationAttributes(ComponentScan.class);
//打了@ComponentScan 注解才会去执行下面的扫描逻辑
if(componentScan != null){
//根据 @ComponentScan 去扫描 对应的 class路径,生成 所有的 BeanDefinitionHolder
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
//在扫描了入口类之后发现了一堆类,这些类里面可能会存在配置类,需要循环处理
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition beanDefinition = holder.getBeanDefinition();
//检查是不是配置类 @Configuration @Component @ComponentScan @Import 或者 存在 @bean方法
if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDefinition)) {
if(beanDefinition instanceof AnnotatedBeanDefinition){
AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) beanDefinition;
//把扫描到的类用递归 再走一次解析配置类的流程
parse(annotatedBeanDefinition.getMetadata(), holder.getBeanName());
}
}
}
}
// @Import 注解的解析
//先把 class 上面所有 @Import 注解里 value 里写的 class 都收集一下
Set<SourceClass> imports = getImports(sourceClass);
//开始处理 @Import 注解
processImports(configClass, sourceClass, imports, true);
// 然后处理 @Bean 注解
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
//先把所有的 @bean 标注的方法存起来
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}
//去解析配置类上的父类,
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
return sourceClass.getSuperClass();
}
}
return null;
}
看到这里你会发现, 不管是传入的类本身,还是 @Bean @Import 标注的候选人,最后都是生成了一个 configurationClass 对象放入了一个全局的缓存中
其实这个 全局缓存 Map<ConfigurationClass, ConfigurationClass> configurationClasses 就是代表了所有要注册进入 IOC 的bean对象
(2) 注册
上面提到,在加载结束后,生成了一个缓存列表 Map<ConfigurationClass, ConfigurationClass> configurationClasses,这个阶段要做的就是把这缓存里的
所有 ConfigurationClass 转成 BeanDefinition 注册进 IOC里面
源码如下: 来自 org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass
private void loadBeanDefinitionsForConfigurationClass(
ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
if (trackedConditionEvaluator.shouldSkip(configClass)) {
String beanName = configClass.getBeanName();
if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
}
this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
return;
}
//看这个 ConfigurationClass 是不是 @Import 注解解析出来的,是的话走 专门的 @Import注册
if (configClass.isImported()) {
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
//找到这个 ConfigurationClass 里面所有 被标注的 @Bean 的方法,注册进入 IOC
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
loadBeanDefinitionsForBeanMethod(beanMethod);
}
loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}
(3) 总结一下
- 2个阶段:
- 1阶段: 扫描了所有class,把拥有 注解
@Component的类注入进去 IOC容器,并且解析了 所有@Bean@Import候选人,全部存入缓存,包括他自己也存 - 2阶段: 根据上面的缓存 去把
@Bean@Import候选人 注册进入 IOC
- 1阶段: 扫描了所有class,把拥有 注解
看到这里,可能会有同学很奇怪,拥有 注解@Component 的类 都已经被注册进入 IOC了,为什么还要把自己缓存,这里先留个悬念到下面会解释
3. @Conditional 入场
上面花了大量篇幅讲了 spring-boot 注册bean流程 , 如果有细心的同学应该会发现,我上面的流程图上表红了一个 方法 shouldSkip , 同时这个方法
在 上面 阶段2 注册的源码里也有 出现
这个 shouldSkip 方法其实就是 @Conditional 的解析流程了, 这个bean 到底去还是留 就是这个方法决定的, 所以可能看出, 在2个阶段上,都有 @Conditional
的解析,那么 比如我们常用的 @ConditionalOnBean 是在哪一个阶段的shouldSkip 方法校验嗯?还是 2个阶段都有校验?
我们一起看源码解开答案
shouldSkip 源码如下 来自 org.springframework.context.annotation.ConditionEvaluator#shouldSkip
public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
return false;
}
//没设置是什么阶段,那么默认设置成 REGISTER_BEAN 阶段
if (phase == null) {
if (metadata instanceof AnnotationMetadata &&
ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
}
return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
}
List<Condition> conditions = new ArrayList<>();
//获取要解析的类上所有的 @Conditional 注解,这里包括 @Conditional 的子注解, 返回值是 @Conditional 注解中的 value 值
//因为 你可以同时 打 @ConditionalOnClass @ConditionalOnProperty 等多个注解所以返回是 [[className1,className2],[className3]] 2维数组的形式
//这里比较抽象,下面有例子详解
for (String[] conditionClasses : getConditionClasses(metadata)) {
for (String conditionClass : conditionClasses) {
Condition condition = getCondition(conditionClass, this.context.getClassLoader());
conditions.add(condition);
}
}
AnnotationAwareOrderComparator.sort(conditions);
for (Condition condition : conditions) {
ConfigurationPhase requiredPhase = null;
if (condition instanceof ConfigurationCondition) {
requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
}
// 如果 入参传入的阶段和实际 Condition 的阶段不同,那么就直接放行了
// condition.matches(this.context, metadata) 这个就是真正去校验 这个bean 到底去还是留
if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
//这个 bean 不要了
return true;
}
}
//放行
return false;
}
这里说一下 getConditionClasses(metadata) 这个函数, 我们先看一下 @ConditionalOnClass @ConditionalOnProperty
这些子注解是怎么定义的
ConditionalOnClass :
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({OnClassCondition.class})
public @interface ConditionalOnClass {
Class<?>[] value() default {};
String[] name() default {};
}
ConditionalOnProperty :
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional({OnPropertyCondition.class})
public @interface ConditionalOnProperty {
String[] value() default {};
String prefix() default "";
String[] name() default {};
String havingValue() default "";
boolean matchIfMissing() default false;
}
他们都是 @Conditional 注解的子注解,但是注意看, @Conditional 注解上传入了一个 class, 其实这个 class 才是真正去执行校验逻辑的执行类,
这些执行类都必须要实现接口 Condition
比如 :
@ConditionalOnProperty
@ConditionalOnBean
@Component
public class A {
}
这个类 会执行 getConditionClasses(metadata) 方法后,返回值是
[ OnClassCondition全路径 , OnPropertyCondition全路径 ]
到这里其实 shouldSkip 原理很简单了,他去拿你类上/方法上 标注的所有 @Conditional 的注解,收集所有实现了Condition 的执行器,然后执行所有的
执行器,只要有一个执行器返回 false 就不通过直接 剔除这个bean
同时 shouldSkip 还有一个参数 ConfigurationPhase,他暗示着这个执行器是阶段1执行还是阶段2执行
我们来看看这个 ConfigurationPhase 定义的状态
enum ConfigurationPhase {
PARSE_CONFIGURATION,
REGISTER_BEAN
}
PARSE_CONFIGURATION --> 解析配置文件的时候执行
REGISTER_BEAN --> 注册bean的时候执行
这就和我们上面讲的 spring-boot 加载的 2个阶段给对应起来了
回到上面 shouldSkip 这段代码,可以看到
requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase()
就是说,每一个执行器 ,都可以指定在哪一个阶段去执行,我们还是来看看 OnClassCondition 是怎么定义的

上图可以看出,他是 定义成 REGISTER_BEAN,那么就是在 注册bean 的时候再去判断,这个bean 到底留不留, 那么根据上面,spring-boot 加载流程所讲的
在阶段1 的时候其实以及把部分的bean给注册进入 IOC了, 那么到了阶段2的时候如果 OnClassCondition 校验没通过怎么办
if (trackedConditionEvaluator.shouldSkip(configClass)) {
String beanName = configClass.getBeanName();
if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
}
this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
return;
}
从这里可以看出,如果 Condition 验证不通过,那么还会去 IOC容器里把已经注册的 BeanDefinition 给删除了
4. 阶段的选择
上面可以看出, Condition 是有2个阶段校验的,那么我们自定义Condition应该选择什么阶段
-
阶段1(加载解析配置类)
- 优点: 阶段1的时候去校验
Condition,如果这个阶段校验不通过那么 这个 class上面的 所有@Bean @Import 都不会再去解析, 效率最高,剥除的最干净 - 缺点: 这时候很多类都还没加载, 比如
@ConditionalOnBean放在这个阶段会导致@Bean这种方式注入的对象没法 参与判断
- 优点: 阶段1的时候去校验
-
阶段2(注册bean的时候)
- 优点: 这时候可以顾及到
@Bean注入的对象, 同时还记得上面讲过 为什么配置类都已经注册进入了IOC还要存一个缓存,原因就是他还要在这里去执行一次
shouldSkip方法 - 缺点: 在阶段1的时候生成了一些而外的缓存对象.
- 优点: 这时候可以顾及到
不过一般来说 Spring-boot 还是推荐 在阶段2的时候校验,毕竟我也不缺这点内存
5. @ConditionalOnMissingBean 失效
先来一个例子,重现一下案发现场
@ConditionalOnMissingBean(B.class)
@Component
public class A {
public A(){
System.out.println("-------加载了A对象-------");
}
}
public class B {}
@Component
public class C {
@Bean
public B creatB(){
return new B();
}
}
我的本意是 如果 没有人注册了类型 B 的对象,那么 我就在容器里注册一个 A 对象, 这里 B 对象已经在 C 里通过 @Bean 的方式给注册了,
那么按照正常的逻辑,这里应该是不注册 A 对象的,实际上并不是这样的 A 被注册进去了,现在我们来 找找问题出在了哪里
其实通过看 OnClassCondition 很容易发现,他是去 ioc 容器里查找有没有注册过对应类型的 bean, 那么原因就很清楚了,在解析 A 的 @ConditionalOnMissingBean(B.class)
的时候 C 的 creatB 还没注册进 IOC 里
让我们看看他是怎么控制加载顺序的
public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
for (ConfigurationClass configClass : configurationModel) {
loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
}
}
这里 configurationModel 的数据结构是 LinkedHashSet 排序规则就是先来后到

从上图以及代码 可以看出, 排序的规则是 按照 用户自定义扫描类 > EnableAutoConfiguration 自动配置加载的类(通过spring.factories 加载EnableAutoConfiguration)
用户自定义类是按照包名和文件名排序的,这个没有任何干预的方法,@Order, Order接口 以及 @AutoConfigureAfter 都是无效的
自动配置加载的类 可以使用 @Order, Order接口, @AutoConfigureAfter 三种方式去改变加载的顺序
所以如果我要让 A 使用 @ConditionalOnMissingBean(B.class) 其实我只要把它 通过 spring.factories 加载EnableAutoConfiguration
方式加载就行,如果使用 加载EnableAutoConfiguration 加载 需要把 A 上的 @Component 给取消

6. 总结
其实 @Conditional 的这套机制很大程度上是用于 自动配置 上面的,这样就可以使用 order 等机制去调整bean 的加载顺序,自然不会出现@ConditionalOnMissingBean 失效的尴尬局面。
对于业务系统,建议不要直接使用@ConditionalOnMissingBean 等注解,因为这个注解本身的意义就是提供一个默认bean , 其实是有其他方式可以实现的,具体可以继续关注我的博客
本文详细解析Spring Boot中@Conditional注解的工作机制,包括其在自动配置中的关键作用、运行流程、条件评估阶段的选择以及常见问题分析。
6184

被折叠的 条评论
为什么被折叠?



