Analysis of ConditionalOnXXX Conditional OnXXX Conditional Annotations Automatically Configured by SpringBoot

foreword

The principle of SpringBoot's automatic configuration is based on its large number of conditional annotations ConditionalOnXXX
insert image description here

There are many Condition-related annotations in the automatic configuration class, taking AOP as an example:

Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(Advice.class)
    static class AspectJAutoProxyingConfiguration {

        @Configuration(proxyBeanMethods = false)
        @EnableAspectJAutoProxy(proxyTargetClass = false)
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
                matchIfMissing = false)
        static class JdkDynamicAutoProxyConfiguration {

        }

        @Configuration(proxyBeanMethods = false)
        @EnableAspectJAutoProxy(proxyTargetClass = true)
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
                matchIfMissing = true)
        static class CglibAutoProxyConfiguration {

        }

    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingClass("org.aspectj.weaver.Advice")
    @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
            matchIfMissing = true)
    static class ClassProxyingConfiguration {

        ClassProxyingConfiguration(BeanFactory beanFactory) {
            if (beanFactory instanceof BeanDefinitionRegistry) {
                BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
                AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
                AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
            }
        }

    }

}

Here you can see @ConditionalOnProperty, @ConditionalOnClass, @ConditionalOnMissingClass, in addition to @ConditionalOnBean, @ConditionalOnMissingBean and many other conditional matching annotations. These annotations indicate that the bean will be loaded only when the conditions match. Taking @ConditionalOnProperty as an example, it indicates that the corresponding bean will be loaded only if the configuration file meets the conditions. prefix indicates the prefix in the configuration file, name indicates the name of the configuration, and havingValue indicates that the configuration is the The value is matched, and matchIfMissing indicates whether the corresponding bean is loaded by default without the configuration. Other annotations can be compared to understanding memory. The following mainly analyzes the implementation principle of this annotation.

If you click on the annotation here, you will find that each annotation is marked with the @Conditional annotation, and the value value corresponds to a class, such as OnBeanCondition, and these classes implement the Condition interface. Take a look at its inheritance system:

We can also implement this interface ourselves to extend the @Condition annotation.

There is a matches method in the Condition interface, which returns true to indicate a match. This method is called in many places in ConfigurationClassParser, which is the shouldSkip method I just reminded. The specific implementation is in the ConditionEvaluator class:

    public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
        if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
            return false;
        }

        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<>();
        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) && !condition.matches(this.context, metadata)) {
                return true;
            }
        }

        return false;
    }

Conditional annotations such as @ConditionalOnXXX are derived annotations, so what are derived annotations?
We open the source code of the ConditionalOnClass annotation, as follows:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {

    Class<?>[] value() default {};

    String[] name() default {};

}

You can see that the @ConditionalOnClass annotation is marked with the @Conditional(OnClassCondition.class) annotation, so @ConditionalOnClass is a derived annotation of @Conditional, and the @Conditional(OnClassCondition.class) and @ConditionalOnClass annotations are equivalent, that is, these two annotations The effect of annotating on a configuration class is equivalent.

The automatic configuration principle of SpringBoot is based on these large number of derived conditional annotations @ConditionalOnXXX, and the principle of these conditional annotations is related to Spring's Condition interface.

Condition interface source code

Before analyzing the source code of the Condition interface, let's look at how to customize the ConditionalOnXXX annotation. For example, if you customize a @ConditionalOnLinux annotation, the annotation will only create a related bean when its attribute environment is "linux". The following code is defined:

/**
 * 实现spring 的Condition接口,并且重写matches()方法,如果@ConditionalOnLinux的注解属性environment是linux就返回true
 *
 */
public class LinuxCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 获得注解@ConditionalOnLinux的所有属性
        List<AnnotationAttributes> allAnnotationAttributes = annotationAttributesFromMultiValueMap(
                metadata.getAllAnnotationAttributes(
                        ConditionalOnLinux.class.getName()));
        for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) {
            // 获得注解@ConditionalOnLinux的environment属性
            String environment = annotationAttributes.getString("environment");
            // 若environment等于linux,则返回true
            if ("linux".equals(environment)) {
                return true;
            }
        }
        return false;
    }
}
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(LinuxCondition.class)
public @interface ConditionalOnLinux {
    // 标注是哪个环境
    String environment() default "";

}
@Configuration
public class ConditionConfig {
        // 只有`@ConditionalOnLinux`的注解属性`environment`是"linux"时才会创建bean
    @Bean
    @ConditionalOnLinux(environment = "linux")
    public Environment linuxEnvironment() {
        return new LinuxEnvironment();
    }
}

LinuxCondition implements the Condition interface and implements the matches method, and the matches method determines whether the annotation attribute environment of @ConditionalOnLinux is "linux", and returns true if it is, otherwise false.
Then we define an annotation @ConditionalOnLinux, which is a derived annotation of @Conditional, which is equivalent to @Conditional(LinuxCondition.class). Note that the @ConditionalOnLinux annotation defines an attribute environment. And we can finally use the parameter AnnotatedTypeMetadata in the matches method of LinuxCondition to get the value of the annotation attribute environment of @ConditionalOnLinux, so as to judge whether the value is linux".
Finally, we define a configuration class ConditionConfig, which is marked on the linuxEnvironment method @ConditionalOnLinux(environment = "linux"). So here the bean will only be created if the matches method of the LinuxCondition returns true.

The source code of the Condition interface:

@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

The Condition interface mainly has a matches method, which determines whether to register the corresponding bean object. There are two parameters in the matches method. The parameter types are ConditionContext and AnnotatedTypeMetadata. These two parameters are very important. They are used to obtain some environmental information and annotation metadata respectively to judge whether the conditions are met in the matches method.

ConditionContext, as the name suggests, is mainly related to the context of Condition, mainly used to obtain Registry, BeanFactory, Environment, ResourceLoader and ClassLoader, etc. So what do you get these for? For example, OnResourceCondition needs to rely on ConditionContext to obtain ResourceLoader to load specified resources, OnClassCondition needs to rely on ConditionContext to obtain ClassLoader to load specified classes, etc. Let’s take a look at its source code:

public interface ConditionContext {
    BeanDefinitionRegistry getRegistry();
    @Nullable
    ConfigurableListableBeanFactory getBeanFactory();
    Environment getEnvironment();
    ResourceLoader getResourceLoader();
    @Nullable
    ClassLoader getClassLoader();
}

AnnotatedTypeMetadata, which is related to annotation metadata, can use AnnotatedTypeMetadata to get some metadata of a certain annotation, and these metadata include the attributes in a certain annotation, such as the previous chestnut, use AnnotatedTypeMetadata to get the annotation of @ConditionalOnLinux The value of the property environment. Take a look at its source code below:

public interface AnnotatedTypeMetadata {
    boolean isAnnotated(String annotationName);
    Map<String, Object> getAnnotationAttributes(String annotationName);
    Map<String, Object> getAnnotationAttributes(String annotationName, boolean classValuesAsString);
    MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName);
    MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName, boolean classValuesAsString);
}

What the @ConditionalOnLinux annotation really works is the concrete implementation of the Condition interface, the matches method of
LinuxCondition. The matches method of LinuxCondition is called in the shouldSkip method of ConditionEvaluator. Naturally, let's take a look at the logic performed by the shouldSkip method of ConditionEvaluator.

// 这个方法主要是如果是解析阶段则跳过,如果是注册阶段则不跳过
public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
    // 若没有被@Conditional或其派生注解所标注,则不会跳过
    if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
        return false;
    }
    // 没有指定phase,注意phase可以分为PARSE_CONFIGURATION或REGISTER_BEAN类型
    if (phase == null) {
        // 若标有@Component,@Import,@Bean或@Configuration等注解的话,则说明是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<>();
    // TODO 获得所有标有@Conditional注解或其派生注解里面的Condition接口实现类并实例化成对象。
    // 比如@Conditional(OnBeanCondition.class)则获得OnBeanCondition.class,OnBeanCondition.class往往实现了Condition接口
    for (String[] conditionClasses : getConditionClasses(metadata)) {
        // 将类实例化成对象
        for (String conditionClass : conditionClasses) {
            Condition condition = getCondition(conditionClass, this.context.getClassLoader());
            conditions.add(condition);
        }
    }
    // 排序,即按照Condition的优先级进行排序
    AnnotationAwareOrderComparator.sort(conditions);

    for (Condition condition : conditions) {
        ConfigurationPhase requiredPhase = null;
        if (condition instanceof ConfigurationCondition) {
            // 从condition中获得对bean是解析还是注册
            requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
        }
        // 若requiredPhase为null或获取的阶段类型正是当前阶段类型且不符合condition的matches条件,则跳过
        if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
            return true;
        }
    }

    return false;
}

The logic executed by the shouldSkip method is mainly to skip if it is the parsing stage, and not to skip if it is the registration stage; if it is in the registration stage, that is, the REGISTER_BEAN stage, then all the concrete implementation classes of the Condition interface will be obtained and instantiated. These implementation classes, and then execute the following key code to determine whether it needs to be skipped.

if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
return true;
}

The most important logic of the above code is to call the matches method of the concrete implementation class of the Condition interface. If matches returns false, it will be skipped and the operation of registering the bean will not be performed; if the matches return true, the operation of registering the bean will not be skipped. ;

Spring's built-in Condition interface implementation class

insert image description here

It is found that although there are many concrete implementation classes of Spring's built-in Condition interface, only ProfileCondition is not related to testing. Therefore, it can be said that the real concrete implementation class of the built-in Condition interface is only ProfileCondition, which is very, very few, which is derived from a large number of SpringBoot. Conditional annotations are a stark contrast. Everyone knows that ProfileCondition is related to the environment. For example, we usually have dev, test and prod environments, and ProfileCondition is to determine which environment our project is configured with. The following is the source code of ProfileCondition

class ProfileCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
        if (attrs != null) {
            for (Object value : attrs.get("value")) {
                if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }
}

OnXXXCondition inherits the
base class of SpringBootCondition SpringBoot conditional annotations and is in the center of the entire class diagram. It implements the Condition interface
. SpringBootConditon implements the Condition interface. As the parent class of SpringBoot's many conditional annotations, OnXXXCondtion, its main function is to print some conditional annotation evaluation reports. log, such as printing which configuration classes are eligible for conditional annotations and which are not.

Because SpringBootConditon implements the Condition interface and the matches method, this method is also called by the shouldSkip method of the ConditionEvaluator, so we use the matches method of SpringBootConditon as the entry to analyze. Go directly to the code:

// SpringBootCondition.java

public final boolean matches(ConditionContext context,
            AnnotatedTypeMetadata metadata) {
        // 得到metadata的类名或方法名
        String classOrMethodName = getClassOrMethodName(metadata);
        try {
            // 判断每个配置类的每个条件注解@ConditionalOnXXX是否满足条件,然后记录到ConditionOutcome结果中
            // 注意getMatchOutcome是一个抽象模板方法,交给OnXXXCondition子类去实现
            ConditionOutcome outcome = getMatchOutcome(context, metadata);
            // 打印condition评估的日志,哪些条件注解@ConditionalOnXXX是满足条件的,哪些是不满足条件的,这些日志都打印出来
            logOutcome(classOrMethodName, outcome);
            // 除了打印日志外,这些是否匹配的信息还要记录到ConditionEvaluationReport中
            recordEvaluation(context, classOrMethodName, outcome);
            // 最后返回@ConditionalOnXXX是否满足条件
            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);
        }
    }

The comments of the above code are very detailed. We know that SpringBootCondition abstracts the common logic of all its concrete implementation classes OnXXXCondition – condition evaluation information printing, and the most important thing is to encapsulate a template method getMatchOutcome(context, metadata), which is left to each OnXXXCondition specific Subclasses override and implement their own judgment logic, and then return the corresponding matching results to SpringBootCondition for log printing.

So we know that SpringBootCondition is actually used to print condition evaluation information. We don't have to go too deep into other side methods, so as not to lose the main line. Our focus now is on the template method getMatchOutcome(context, metadata); which is handed over to the OnXXXCondition subclass, because this method will be overridden by many OnXXXConditions and rewrite the judgment logic. Here is the focus of our next analysis.

Because SpringBootCondition has many specific implementation classes, only OnWebApplicationCondition is selected for explanation.

OnWebApplicationCondition also inherits the parent class of FilteringSpringBootCondition and overrides the getOutcomes method of the parent class FilteringSpringBootCondition. And FilteringSpringBootCondition is a subclass of SpringBootCondition. FilteringSpringBootCondition is related to automatic configuration class filtering, which will not be analyzed here. It is worth noting that OnWebApplicationCondition also rewrites the getMatchOutcome method of SpringBootCondition to determine whether the current application is a web application. Also OnWebApplicationCondition is a conditional class of @ConditionalOnWebApplication.

Similarly, let's first look at OnWebApplicationCondition's rewrite of SpringBootCondition's getMatchOutcome method:

public ConditionOutcome getMatchOutcome(ConditionContext context,
            AnnotatedTypeMetadata metadata) {
        // 配置类是否标注有@ConditionalOnWebApplication注解
        boolean required = metadata
                .isAnnotated(ConditionalOnWebApplication.class.getName());
        // 调用isWebApplication方法返回匹配结果
        ConditionOutcome outcome = isWebApplication(context, metadata, required);
        // 若有标注@ConditionalOnWebApplication但不符合条件,则返回不匹配
        if (required && !outcome.isMatch()) {
            return ConditionOutcome.noMatch(outcome.getConditionMessage());
        }
        // 若没有标注@ConditionalOnWebApplication但符合条件,则返回不匹配
        if (!required && outcome.isMatch()) {
            return ConditionOutcome.noMatch(outcome.getConditionMessage());
        }
        // 这里返回匹配的情况,TODO 不过有个疑问:如果没有标注@ConditionalOnWebApplication注解,又不符合条件的话,也会执行到这里,返回匹配?
        return ConditionOutcome.match(outcome.getConditionMessage());
    }

The logic of the above code is very simple, mainly calling the isWebApplication method to determine whether the current application is a web application. Therefore, let's look at the isWebApplication method again:

private ConditionOutcome isWebApplication(ConditionContext context,
            AnnotatedTypeMetadata metadata, boolean required) {
        // 调用deduceType方法判断是哪种类型,其中有SERVLET,REACTIVE和ANY类型,其中ANY表示了SERVLET或REACTIVE类型
        switch (deduceType(metadata)) {
        // SERVLET类型
        case SERVLET:
            return isServletWebApplication(context);
        // REACTIVE类型
        case REACTIVE:
            return isReactiveWebApplication(context);
        default:
            return isAnyWebApplication(context, required);
        }
    }

In the isWebApplication method, first get what type it defines from the @ConditionalOnWebApplication annotation, and then enter different judgment logic according to different types. Here we only look at the judgment and processing of the SERVLET situation, look at the code:

private ConditionOutcome isServletWebApplication(ConditionContext context) {
        ConditionMessage.Builder message = ConditionMessage.forCondition("");
        // 若classpath中不存在org.springframework.web.context.support.GenericWebApplicationContext.class,则返回不匹配
        if (!ClassNameFilter.isPresent(SERVLET_WEB_APPLICATION_CLASS,
                context.getClassLoader())) {
            return ConditionOutcome.noMatch(
                    message.didNotFind("servlet web application classes").atAll());
        }
        // 若classpath中存在org.springframework.web.context.support.GenericWebApplicationContext.class,那么又分为以下几种匹配的情况
        // session
        if (context.getBeanFactory() != null) {
            String[] scopes = context.getBeanFactory().getRegisteredScopeNames();
            if (ObjectUtils.containsElement(scopes, "session")) {
                return ConditionOutcome.match(message.foundExactly("'session' scope"));
            }
        }
        // ConfigurableWebEnvironment
        if (context.getEnvironment() instanceof ConfigurableWebEnvironment) {
            return ConditionOutcome
                    .match(message.foundExactly("ConfigurableWebEnvironment"));
        }
        // WebApplicationContext
        if (context.getResourceLoader() instanceof WebApplicationContext) {
            return ConditionOutcome.match(message.foundExactly("WebApplicationContext"));
        }
        // 若以上三种都不匹配的话,则说明不是一个servlet web application
        return ConditionOutcome.noMatch(message.because("not a servlet web application"));
    }

For the case of SERVLET, first, according to whether there is org.springframework.web.context.support.GenericWebApplicationContext.class in the classpath, if the class does not exist, it will directly return a mismatch; if it exists, it will be divided into the following types of matching Condition:

session
ConfigurableWebEnvironment
WebApplicationContext
If the above three conditions do not match, it means that it is not a servlet web application.

Extend SpringBootCondition


 
import java.lang.annotation.*;
 
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(MyConditional.class)
public @interface  MyConditionalIAnnotation {
    String key();
    String value();
}

import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.core.type.AnnotatedTypeMetadata;
 
import java.util.Map;
 
 
public class MyConditional extends SpringBootCondition {
 
    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(MyConditionalIAnnotation.class.getName());
        Object key = annotationAttributes.get("key");//
        Object value = annotationAttributes.get("value");
        if(key == null || value == null){
            return new ConditionOutcome(false, "error");
        }
 
        //获取environment中的值
        String key1 = context.getEnvironment().getProperty(key.toString());
        if (value.equals(key1)) {
            //如果environment中的值与指定的value一致,则返回true
            return new ConditionOutcome(true, "ok");
        }
        return new ConditionOutcome(false, "error");
 
    }
}

import org.apache.log4j.Logger;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class MyConditionalConfig {
    public static Logger logger=Logger.getLogger(MyConditionalService.class);
 
    /**
     * 判断MyConditional 是否符合条件,是则运行MyConditionalService
     * @return
     */
    @MyConditionalIAnnotation(key = "com.example.conditional", value = "lbl")
    @ConditionalOnClass(MyConditionalService.class)
    @Bean
    public MyConditionalService initMyConditionService() {
        logger.info("MyConditionalService已加载。");
        return new MyConditionalService();
    }
}

Configuration file: application.propeties
spring.application.name=gateway
server.port=8084
#conditional dynamic configuration, determine whether the value is equal to lbl, if it is, create a MyConditionalService instance
com.example.conditional=lbl #support
custom aop
spring.aop .auto=true

Guess you like

Origin blog.csdn.net/liuerchong/article/details/123934113