• Spring Boot系列之条件注解


    概述

    想要搞懂Spring Boot自动配置,绕不过条件注解,即@Conditional,可用于根据某个特定的条件来判断是否需要创建某个特定的Bean。本文分析基于spring-boot-autoconfigure-3.2.4版本。

    @Conditional注解可以添加在被@Configuration、@Component、@Service等修饰的类,或在被@Bean修饰的方法上,用于控制类或方法对应的Bean是否需要创建。

    @Conditional注解需要和Condition接口搭配一起使用。通过对应Condition接口来告知是否满足匹配条件。

    扩展注解

    条件注解对应Condition处理类解释
    ConditionalOnClassOnClassCondition类加载器中存在指定类
    ConditionalOnMissingClassOnClassCondition类加载器中不存在指定类
    ConditionalOnBeanOnBeanConditionSpring容器中存在指定Bean
    ConditionalOnMissingBeanOnBeanConditionSpring容器中不存在指定Bean
    ConditionalOnSingleCandidateOnBeanConditionSpring容器中是否存在且只存在一个对应的实例,或虽然有多个但是指定首选的Bean生效
    ConditionalOnJavaOnJavaCondition指定Java版本符合要求生效
    ConditionalOnJndiOnJndiCondition存在JNDI
    ConditionalOnCloudPlatformOnCloudPlatformCondition云平台,支持:CLOUD_FOUNDRY、HEROKU、SAP、NOMAD、KUBERNETES
    ConditionalOnCheckpointRestore存在类orc.crac.Resource
    ConditionalOnWebApplicationOnWebApplicationConditionWeb应用生效
    ConditionalOnNotWebApplicationOnWebApplicationCondition不是Web应用生效
    ConditionalOnWarDeploymentOnWarDeploymentConditionWar应用生效
    ConditionalOnNotWarDeploymentOnWarDeploymentCondition不是War应用生效
    ConditionalOnResourceOnResourceCondition当指定资源文件出现则生效
    ConditionalOnPropertyOnPropertyCondition应用环境中的属性满足条件生效
    ConditionalOnExpressionOnExpressionCondition判断SpEL表达式成立生效
    ConditionalOnThreadingOnThreadingCondition指定线程处于active状态

    ConditionalOnCheckpointRestore源码如下:

    @ConditionalOnClass(name = {"org.crac.Resource"})
    public @interface ConditionalOnCheckpointRestore {
    }
    
    • 1
    • 2
    • 3

    CRaC是OpenJDK项目,有兴趣可延伸阅读。

    原理

    条件注解存在的意义在于动态识别,即代码自动化执行。如@ConditionalOnClass会检查类加载器中是否存在对应的类,如果有的话被注解修饰的类就有资格被Spring容器所注册,否则会被skip。

    如FreemarkerAutoConfiguration这个自动化配置类的定义如下:

    @AutoConfiguration
    @ConditionalOnClass({ freemarker.template.Configuration.class, FreeMarkerConfigurationFactory.class })
    @EnableConfigurationProperties(FreeMarkerProperties.class)
    @Import({ FreeMarkerServletWebConfiguration.class, FreeMarkerReactiveWebConfiguration.class, FreeMarkerNonWebConfiguration.class })
    public class FreeMarkerAutoConfiguration {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这个自动化配置类被@ConditionalOnClass条件注解修饰,判断类加载器中是否存在freemarker.template.Configuration和FreeMarkerConfigurationFactory这两个类,如果都存在的话会在Spring容器中加载这个FreeMarkerAutoConfiguration配置类;否则不会加载。

    @Conditional源码:

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Conditional {
    	Class<? extends Condition>[] value();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    需要传入一个Class数组,数组类型是Condition。而Condition是个接口,用于匹配组件是否有资格被容器注册:

    @FunctionalInterface
    public interface Condition {
    	// ConditionContext内部会存储Spring容器、应用程序环境信息、资源加载器、类加载器
    	boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    @Conditional注解属性中可以持有多个Condition接口的实现类,所有的Condition接口需要全部匹配成功后这个@Conditional修饰的组件才有资格被注册。

    Condition有个子接口ConfigurationCondition:

    public interface ConfigurationCondition extends Condition {
    	ConfigurationPhase getConfigurationPhase();
        public static enum ConfigurationPhase {
            PARSE_CONFIGURATION,
            REGISTER_BEAN
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这个子接口是一种特殊的条件接口,多一个getConfigurationPhase方法,也就是条件注解的生效阶段。只有在ConfigurationPhase中定义的两种阶段下才会生效:

    • PARSE_CONFIGURATION
    • REGISTER_BEAN

    Condition接口有个抽象类SpringBootCondition,SpringBoot中所有条件注解对应的条件类都继承这个抽象类,并需要实现matches方法:

    @Override
    public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String classOrMethodName = getClassOrMethodName(metadata); // 得到类名或者方法名(条件注解可以作用的类或者方法上)
        try {
            ConditionOutcome outcome = getMatchOutcome(context, metadata); // 抽象方法,具体子类实现。ConditionOutcome记录了匹配结果boolean和log信息
            logOutcome(classOrMethodName, outcome); // log记录一下匹配信息
            recordEvaluation(context, classOrMethodName, outcome); // 报告记录一下匹配信息
            return outcome.isMatch(); // 返回是否匹配
        } catch (NoClassDefFoundError ex) {
            throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to " + ex.getMessage() + " not found. Make sure your own configuration does not rely on that class. This can also happen if you are @ComponentScanning a springframework package (e.g. if you put a @ComponentScan in the default package by mistake)", ex);
        } catch (RuntimeException ex) {
            throw new IllegalStateException("Error processing condition on " + getName(metadata), ex);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    基于Class的条件注解

    有两个

    • @ConditionalOnClass
    • @ConditionalOnMissingClass

    @ConditionalOnClass注解定义如下:

    @Target({ ElementType.TYPE, ElementType.METHOD })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Conditional(OnClassCondition.class)
    public @interface ConditionalOnClass {
    	Class<?>[] value() default {}; // 需要匹配的类
    	String[] name() default {}; // 需要匹配的类名
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    它有2个属性,分别是类数组和字符串数组(作用一样,类型不一样),而且被@Conditional注解所修饰。

    对应条件类是OnClassCondition:

    @Order(Ordered.HIGHEST_PRECEDENCE) // 优先级最高级别
    class OnClassCondition extends FilteringSpringBootCondition {
    	@Override
    	protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
    			AutoConfigurationMetadata autoConfigurationMetadata) {
    		// Split the work and perform half in a background thread if more than one
    		// processor is available. Using a single additional thread seems to offer the
    		// best performance. More threads make things worse.
    		if (autoConfigurationClasses.length > 1 && Runtime.getRuntime().availableProcessors() > 1) {
    			return resolveOutcomesThreaded(autoConfigurationClasses, autoConfigurationMetadata);
    		}
    		else {
    			OutcomesResolver outcomesResolver = new StandardOutcomesResolver(autoConfigurationClasses, 0,
    					autoConfigurationClasses.length, autoConfigurationMetadata, getBeanClassLoader());
    			return outcomesResolver.resolveOutcomes();
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    比如FreemarkerAutoConfiguration中的@ConditionalOnClass注解中有value属性是freemarker.template.Configuration.classFreeMarkerConfigurationFactory.class。在OnClassCondition执行过程中得到的最终ConditionalOutcome中的log message如下:
    @ConditionalOnClass classes found: freemarker.template.Configuration,org.springframework.ui.freemarker.FreeMarkerConfigurationFactory

    基于Bean的条件注解

    有3个:

    • @ConditionalOnBean
    • @ConditionalOnMissingBean
    • @ConditionalOnSingleCandidate

    和基于类的条件注解比较类似。

    激活机制

    这部分有点难,想通过阅读源码来理清楚前后调用及解析关系。好在我们可以断点调试。通过断点调试发现关键类和方法:

    • ConfigurationClassParser
    • ConditionEvaluator
    • ComponentScanAnnotationParser

    SpringBoot使用ConditionEvaluator这个内部类完成条件注解的解析和判断。在Spring容器的refresh过程中,只有跟解析或者注册bean有关系的类都会使用ConditionEvaluator完成条件注解的判断,这个过程中一些类不满足条件的话就会被skip。这些类比如有AnnotatedBeanDefinitionReader、ConfigurationClassBeanDefinitionReader、ConfigurationClassParse、ClassPathScanningCandidateComponentProvider等。

    比如ConfigurationClassParser的构造函数会初始化内部属性conditionEvaluator:

    public ConfigurationClassParser(MetadataReaderFactory metadataReaderFactory,
        ProblemReporter problemReporter, Environment environment, ResourceLoader resourceLoader,
        BeanNameGenerator componentScanBeanNameGenerator, BeanDefinitionRegistry registry) {
    	this.metadataReaderFactory = metadataReaderFactory;
    	this.problemReporter = problemReporter;
    	this.environment = environment;
    	this.resourceLoader = resourceLoader;
    	this.registry = registry;
    	this.componentScanParser = new ComponentScanAnnotationParser(resourceLoader, environment, componentScanBeanNameGenerator, registry);
    	// 构造ConditionEvaluator用于处理条件注解
    	this.conditionEvaluator = new ConditionEvaluator(registry, environment, resourceLoader);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    ConfigurationClassParser对每个配置类进行解析的时候都会使用ConditionEvaluator:

    if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
    	return;
    }
    
    • 1
    • 2
    • 3

    ConditionEvaluator的skip方法:

    public boolean shouldSkip(AnnotatedTypeMetadata metadata, ConfigurationPhase phase) {
    	// 如果这个类没有被@Conditional注解所修饰,不会skip
        if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
            return false;
        }
        // 如果参数中沒有设置条件注解的生效阶段
        if (phase == null) {
            // 是配置类的话直接使用PARSE_CONFIGURATION阶段
            if (metadata instanceof AnnotationMetadata && ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
                return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
            }
            // 否则使用REGISTER_BEAN阶段
            return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
        }
        // 要解析的配置类的条件集合
        List<Condition> conditions = new ArrayList<Condition>();
        // 获取配置类的条件注解得到条件数据,并添加到集合中
        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();
            }
            // 没有这个解析类不需要阶段的判断或者解析类和参数中的阶段一致才会继续进行
            if (requiredPhase == null || requiredPhase == phase) {
                // 阶段一致切不满足条件的话,返回true并跳过这个bean的解析
                if (!condition.matches(this.context, metadata)) {
                    return true;
                }
            }
        }
        return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    SpringBoot在条件注解的解析log记录在ConditionEvaluationReport类中,可通过BeanFactory获取。BeanFactory是有父子关系的;每个BeanFactory都存有一份ConditionEvaluationReport,互不相干:

    ConditionEvaluationReport conditionEvaluationReport = beanFactory.getBean("autoConfigurationReport", ConditionEvaluationReport.class);
    Map<String, ConditionEvaluationReport.ConditionAndOutcomes> result = conditionEvaluationReport.getConditionAndOutcomesBySource();
    for(String key : result.keySet()) {
        ConditionEvaluationReport.ConditionAndOutcomes conditionAndOutcomes = result.get(key);
        Iterator<ConditionEvaluationReport.ConditionAndOutcome> iterator = conditionAndOutcomes.iterator();
        while(iterator.hasNext()) {
            ConditionEvaluationReport.ConditionAndOutcome conditionAndOutcome = iterator.next();
            System.out.println(key + " -- " + conditionAndOutcome.getCondition().getClass().getSimpleName() + " -- " + conditionAndOutcome.getOutcome());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    打印出条件注解下的类加载信息:

    ...
    org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: freemarker.template.Configuration,org.springframework.ui.freemarker.FreeMarkerConfigurationFactory
    org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: groovy.text.markup.MarkupTemplateEngine
    org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: com.google.gson.Gson
    org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: org.h2.server.web.WebServlet
    org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: org.springframework.hateoas.Resource,org.springframework.plugin.core.Plugin
    org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: com.hazelcast.core.HazelcastInstance
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    实战

    自定义

    需要自定义一个condition类实现Condition接口,假设根据系统类型来加载不同的Bean:

    public class OnSystemCondition implements Condition {
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(ConditionalOnSystem.class.getName());
            if (annotationAttributes == null) {
                return false;
            }
            ConditionalOnSystem.SystemType systemType = (ConditionalOnSystem.SystemType) annotationAttributes.get("type");
            switch (systemType) {
                case WINDOWS:
                    return context.getEnvironment().getProperty("os.name").contains("Windows");
                case LINUX:
                    return context.getEnvironment().getProperty("os.name").contains("Linux ");
                case MAC:
                    return context.getEnvironment().getProperty("os.name").contains("Mac ");
            }
            return false;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    自定义条件注解并指定对应的处理condition类:

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Documented
    @Conditional(OnSystemCondition.class)
    public @interface ConditionalOnSystem {
        /**
         * 指定系统
         */
        SystemType type() default SystemType.WINDOWS;
        /**
         * 系统类型
         */
        enum SystemType {
            WINDOWS,
            LINUX,
            MAC;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    参考

  • 相关阅读:
    谈谈电商App的压测
    【深度学习】基于YOLOV5模型的图像识别-目标检测的性能指标详解与计算方法
    观测云产品更新 | Pipelines、智能监控、日志数据访问等
    mysql 的存储引擎
    分布式事务理论
    存一个滤波器的截图。免的过段时间忘了
    prometheus监控之postgresql
    一级建造师从业者面试需要注意什么问题?
    lua学习笔记
    threejs的DragControls当click时会触发dragstart和dragend问题解决
  • 原文地址:https://blog.csdn.net/lonelymanontheway/article/details/128423648