转载

spring @Conditional 源码解析 以及@ConditionalOnMissingBean 失效之谜

作者 | 曹恒源

spring @Conditional 源码解析 以及@ConditionalOnMissingBean 失效之谜

为了防止这个世界被破坏,为了保护世界的和平,贯彻爱与真实的邪恶,可爱又迷人的程序员。

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 {
}

我们常用的  @Servic, @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 注解

spring @Conditional 源码解析 以及@ConditionalOnMissingBean 失效之谜 上图比较抽象,大概用语言描述一下:

上述流程来自  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

看到这里,可能会有同学很奇怪,拥有注解 @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  是怎么定义的。

spring @Conditional 源码解析 以及@ConditionalOnMissingBean 失效之谜 上图可以看出,他是定义成  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  这种方式注入的对象没法参与判断。

  • 阶段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  排序规则就是先来后到

spring @Conditional 源码解析 以及@ConditionalOnMissingBean 失效之谜 从上图以及代码可以看出,排序的规则是按照 用户自定义扫描类  >  EnableAutoConfiguration 自动配置加载的类 (通过spring.factories 加载EnableAutoConfiguration)

用户自定义类是按照包名和文件名排序的,这个没有任何干预的方法, @Order Order接口  以及  @AutoConfigureAfter  都是无效的。 自动配置 加载的类可以使用  @Order Order接口, @AutoConfigureAfter  三种方式去改变加载的顺序。

所以如果我要让  A  使用  @ConditionalOnMissingBean(B.class) 其实我只要把它通过 spring.factories 加载EnableAutoConfiguration 方式加载就行,如果使用 加载EnableAutoConfiguration 加载 需要把  A  上的  @Component  给取消。

spring @Conditional 源码解析 以及@ConditionalOnMissingBean 失效之谜

6. 总结

其实  @Conditional  的这套机制很大程度上是用于 自动配置 上面的,这样就可以使用  order  等机制去调整 bean  的加载顺序,自然不会出现 @ConditionalOnMissingBean  失效的尴尬局面。对于业务系统,建议不要直接使用 @ConditionalOnMissingBean  等注解,因为这个注解本身的意义就是提供一个 默认bean  , 其实是有其他方式可以实现的,具体可以继续关注我的博客。

全文完

以下文章您可能也会感兴趣:

  • 简单说说spring的循环依赖

  • Mysql redo log 漫游

  • 一个 AOP 缓存失效问题的排查

  • 小程序开发的几个好的实践

  • RabbitMQ 如何保证消息可靠性

  • 在 SpringBoot 中使用 STOMP 基于 WebSocket 建立 BS 双向通信

  • 聊聊Hystrix 命令执行流程

  • HIS 系统前端重构经验

  • SpringFox 源码分析(及 Yapi 问题的另一种解决方案)

  • Mysql 的字符集以及带来的一点存储影响

我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。

spring @Conditional 源码解析 以及@ConditionalOnMissingBean 失效之谜

杏仁技术站

长按左侧二维码关注我们,这里有一群热血青年期待着与您相会。

原文  http://mp.weixin.qq.com/s?__biz=MzUxOTE5MTY4MQ==&mid=2247484488&idx=1&sn=42aed74b03d7ca15ea1f0e232a846c4a
正文到此结束
Loading...