SpringBoot源码分析之条件注解[3]

  1. 什么是条件注解
  2. 条件注解机制
  3. 条件注解加载过程
  4. 总结
  5. 附录
    1. 条件注解大全

上一章介绍了Spring Boot的容器刷新,本章介绍Spring Boot的条件注解机制。

什么是条件注解

条件注解是注解的一种,可以作用于类或者方法上,被用来限制是否对该应用类或方法进行自动装配。

常见的条件注解包括:

  1. @ConditionalOnClass:类加载器中是否存在对应的类。
  2. @ConditionalOnBean:Spring容器中是否存在对应的实例。
  3. @ConditionalOnExpression:判断SpEL 表达式是否成立。
  4. @ConditionalOnMissingClass:类加载器中是否不存在对应的类。
  5. @ConditionalOnMissingBean:Spring容器中是否不存在对应的实例。

举例说明:

@ConditionalOnClass(value = {com.zhouyufan.springbootconditionalannotation.SpringbootConditionalAnnotationApplication.class})
@RestController
public class TestSkip {
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

当类加载器中包含SpringbootConditionalAnnotationApplication类对象时,TestSkip类实例才会在容器中被创建。

条件注解机制

以@ConditionalOnClass条件注解为例,其包含了两个域,一个用来指定必须存在的类,一个用来指定必须存在的类的全限定名。

此外,@ConditionalOnClass注解被@Conditional注解标注。

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
// @Conditional:指明一个组件必须匹配所有指定的条件才能被注册
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
    // 指定必须存在的类
    Class<?>[] value() default {};
    // 指定必须存在的类的全限定名
    String[] name() default {};
}

@Conditional注解指定了条件注解的处理类,@ConditionalOnClass对应的处理类是OnClassCondition类。

OnClassCondition类的继承关系如图所示。OnClassCondition类继承SpringBootCondition类,实现了Condition接口。Condition接口中定义了matches方法,用于匹配指定的条件。

条件类的继承关系

SpringBootCondition类中的matches方法用来对条件进行匹配,其被声明为final。

public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    String classOrMethodName = getClassOrMethodName(metadata);
    try {
        // getMatchOutcome是SpringBootCondition类的抽象方法,由子类实现
        ConditionOutcome outcome = getMatchOutcome(context, metadata);
        // 记录匹配结果
        logOutcome(classOrMethodName, outcome);
        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);
    }
}

以子类OnClassCondition为例,其实现了SpringBootCondition类的抽象方法getMatchOutcome方法:

public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
    ClassLoader classLoader = context.getClassLoader();
    ConditionMessage matchMessage = ConditionMessage.empty();
    // 在metadata中获取条件注解指定的所有类
    List<String> onClasses = getCandidates(metadata, ConditionalOnClass.class);
    // 如果onClasses不为null时
    if (onClasses != null) {
        // 在类加载器中查找所有缺失的类
        List<String> missing = filter(onClasses, ClassNameFilter.MISSING, classLoader);
        // 如果存在缺失的类,则不匹配
        if (!missing.isEmpty()) {
            return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnClass.class)
                    .didNotFind("required class", "required classes").items(Style.QUOTE, missing));
        }
        // 如果满足条件,则记录匹配消息
        matchMessage = matchMessage.andCondition(ConditionalOnClass.class)
                .found("required class", "required classes")
                .items(Style.QUOTE, filter(onClasses, ClassNameFilter.PRESENT, classLoader));
    }
    // 这个方法还能处理@ConditionOnMissingClass
    // 这说明@ConditionOnMissingClass中@Condition注解指定的也是OnClassCondition条件处理类
    List<String> onMissingClasses = getCandidates(metadata, ConditionalOnMissingClass.class);
    if (onMissingClasses != null) {
        List<String> present = filter(onMissingClasses, ClassNameFilter.PRESENT, classLoader);
        if (!present.isEmpty()) {
            return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnMissingClass.class)
                    .found("unwanted class", "unwanted classes").items(Style.QUOTE, present));
        }
        matchMessage = matchMessage.andCondition(ConditionalOnMissingClass.class)
                .didNotFind("unwanted class", "unwanted classes")
                .items(Style.QUOTE, filter(onMissingClasses, ClassNameFilter.MISSING, classLoader));
    }
    return ConditionOutcome.match(matchMessage);
}

条件注解加载过程

了解了条件注解的机制,再来看看是在何时进行条件注解的判断的。

在容器的准备和刷新阶段,所有与解析、注册bean有关的类都会持有ConditionEvaluator类的对象,并使用该对象完成条件注解的判断(判断是否需要跳过该类)。

读取、注册bean的类包括:

  1. AnnotatedBeanDefinitionReader
  2. ClassPathScanningCandidateComponentProvider
  3. ConfigurationClassBeanDefinitionReader
  4. ConfigurationClassParser

ConditionEvaluator的shouldSkip方法用来判断被@Conditional注解标记的item是否需要被跳过

public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
    // 判断是否被@Conditional注解标记,未被标记直接返回false
    if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
        return false;
    }
    // 判断阶段是否为null
    // 如果为null,则赋予phase一个值,重新进入该方法
    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<>();
    // 获取metadata中所有的condition类,例如OnClassCondition
    for (String[] conditionClasses : getConditionClasses(metadata)) {
        for (String conditionClass : conditionClasses) {
            // 根据类限定名构造类实例
            Condition condition = getCondition(conditionClass, this.context.getClassLoader());
            conditions.add(condition);
        }
    }
    AnnotationAwareOrderComparator.sort(conditions);
    // 遍历编程阶段指定的所有condition
    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;
}

总结

条件注解可以在程序运行时动态判定指定的类或者方法是否需要被加载。

使用条件注解可以减少代码耦合性,使设计更加通用。

不同的条件注解内部指定了具体的条件类(实现了Condition接口),不同的条件类实现了SpringBootCondition类的抽象方法getMatchOutcome方法的具体判断逻辑。

在容器的准备和刷新阶段中,负责解析和注册bean的类中持有ConditionEvaluator类的对象,ConditionEvaluator类中的shouldSkip方法会调用到条件类的matches方法进行匹配。

附录

条件注解大全

条件注解 对应的Condition处理类 处理逻辑
@ConditionalOnBean OnBeanCondition Spring容器中是否存在对应的实例。可以通过实例的类型、类名、注解、昵称去容器中查找(可以配置从当前容器中查找或者父容器中查找或者两者一起查找)这些属性都是数组,通过”与”的关系进行查找
@ConditionalOnClass OnClassCondition 类加载器中是否存在对应的类。可以通过Class指定(value属性)或者Class的全名指定(name属性)。如果是多个类或者多个类名的话,关系是”与”关系,也就是说这些类或者类名都必须同时在类加载器中存在
@ConditionalOnExpression OnExpressionCondition 判断SpEL 表达式是否成立
@ConditionalOnJava OnJavaCondition 指定Java版本是否符合要求。内部有2个属性value和range。value表示一个枚举的Java版本,range表示比这个老或者新于等于指定的Java版本(默认是新于等于)。内部会基于某些jdk版本特有的类去类加载器中查询,比如如果是jdk9,类加载器中需要存在java.security.cert.URICertStoreParameters;如果是jdk8,类加载器中需要存在java.util.function.Function;如果是jdk7,类加载器中需要存在java.nio.file.Files;如果是jdk6,类加载器中需要存在java.util.ServiceLoader
@ConditionalOnMissingBean OnBeanCondition Spring容器中是否缺少对应的实例。可以通过实例的类型、类名、注解、昵称去容器中查找(可以配置从当前容器中查找或者父容器中查找或者两者一起查找)这些属性都是数组,通过”与”的关系进行查找。还多了2个属性ignored(类名)和ignoredType(类名),匹配的过程中会忽略这些bean
@ConditionalOnMissingClass OnClassCondition 跟ConditionalOnClass的处理逻辑一样,只是条件相反,在类加载器中不存在对应的类
@ConditionalOnNotWebApplication OnWebApplicationCondition 应用程序是否是非Web程序,没有提供属性,只是一个标识。会从判断Web程序特有的类是否存在,环境是否是Servlet环境,容器是否是Web容器等
@ConditionalOnProperty OnPropertyCondition 应用环境中的屬性是否存在。提供prefix、name、havingValue以及matchIfMissing属性。prefix表示属性名的前缀,name是属性名,havingValue是具体的属性值,matchIfMissing是个boolean值,如果属性不存在,这个matchIfMissing为true的话,会继续验证下去,否则属性不存在的话直接就相当于匹配不成功
@ConditionalOnResource OnResourceCondition 是否存在指定的资源文件。只有一个属性resources,是个String数组。会从类加载器中去查询对应的资源文件是否存在
@ConditionalOnSingleCandidate OnBeanCondition Spring容器中是否存在且只存在一个对应的实例。只有3个属性value、type、search。跟ConditionalOnBean中的这3种属性值意义一样
@ConditionalOnWebApplication OnWebApplicationCondition 应用程序是否是Web程序,没有提供属性,只是一个标识。会从判断Web程序特有的类是否存在,环境是否是Servlet环境,容器是否是Web容器等

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 504537531@qq.com

文章标题:SpringBoot源码分析之条件注解[3]

文章字数:2k

本文作者:YF

发布时间:2019-10-10, 15:59:32

最后更新:2019-10-15, 17:07:04

原始链接:https://zhouyufan0568.github.io/2019/10/10/springboot-conditional-annotation/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录