Bootstrap

SpringBoot系列(十一)条件注解的底层实现

前言

  • SpringBoot内部提供了特有的注解:条件注解(Conditional Annotation)。比如@ConditionalOnBean、@ConditionalOnClass、@ConditionalOnExpression、@ConditionalOnMissingBean等。

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


一、条件注解内部

在分析条件注解的底层实现之前,我们先来看一下这些条件注解的定义。以@ConditionalOnClass注解为例,它的定义如下:

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

它有2个属性,分别是类数组和字符串数组(作用一样,类型不一样),而且被@Conditional注解所修饰,这个@Conditional注解有个名为values的Class< ? extends Condition >[]类型的属性。 这个Condition是个接口,用于匹配组件是否有资格被容器注册,定义如下:

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

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

Condition接口有个子接口ConfigurationCondition:

public interface ConfigurationCondition extends Condition {

  ConfigurationPhase getConfigurationPhase();

  public static enum ConfigurationPhase {

    PARSE_CONFIGURATION,

    REGISTER_BEAN
  }

}

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

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);
  }
}

二、基于Class的条件注解

SpringBoot提供了两个基于Class的条件注解:@ConditionalOnClass(类加载器中存在指明的类)或者@ConditionalOnMissingClass(类加载器中不存在指明的类)。

@ConditionalOnClass或者@ConditionalOnMissingClass注解对应的条件类是OnClassCondition,定义如下:

@Order(Ordered.HIGHEST_PRECEDENCE) // 优先级、最高级别
class OnClassCondition extends SpringBootCondition {

  @Override
  public ConditionOutcome getMatchOutcome(ConditionContext context,
        AnnotatedTypeMetadata metadata) {

    StringBuffer matchMessage = new StringBuffer(); // 记录匹配信息

    MultiValueMap<String, Object> onClasses = getAttributes(metadata,
            ConditionalOnClass.class); // 得到@ConditionalOnClass注解的属性
    if (onClasses != null) { // 如果属性存在
        List<String> missing = getMatchingClasses(onClasses, MatchType.MISSING,
                context); // 得到在类加载器中不存在的类
        if (!missing.isEmpty()) { // 如果存在类加载器中不存在对应的类,返回一个匹配失败的ConditionalOutcome
            return ConditionOutcome
                    .noMatch("required @ConditionalOnClass classes not found: "
                            + StringUtils.collectionToCommaDelimitedString(missing));
        }
                // 如果类加载器中存在对应的类的话,匹配信息进行记录
        matchMessage.append("@ConditionalOnClass classes found: "
                + StringUtils.collectionToCommaDelimitedString(
                        getMatchingClasses(onClasses, MatchType.PRESENT, context)));
    }
        // 对@ConditionalOnMissingClass注解做相同的逻辑处理(说明@ConditionalOnClass和@ConditionalOnMissingClass可以一起使用)
    MultiValueMap<String, Object> onMissingClasses = getAttributes(metadata,
            ConditionalOnMissingClass.class);
    if (onMissingClasses != null) {
        List<String> present = getMatchingClasses(onMissingClasses, MatchType.PRESENT,
                context);
        if (!present.isEmpty()) {
            return ConditionOutcome
                    .noMatch("required @ConditionalOnMissing classes found: "
                            + StringUtils.collectionToCommaDelimitedString(present));
        }
        matchMessage.append(matchMessage.length() == 0 ? "" : " ");
        matchMessage.append("@ConditionalOnMissing classes not found: "
                + StringUtils.collectionToCommaDelimitedString(getMatchingClasses(
                        onMissingClasses, MatchType.MISSING, context)));
    }
        // 返回全部匹配成功的ConditionalOutcome
    return ConditionOutcome.match(matchMessage.toString());
    }

  private enum MatchType { // 枚举:匹配类型。用于查询类名在对应的类加载器中是否存在。

    PRESENT { // 匹配成功
        @Override
        public boolean matches(String className, ConditionContext context) {
            return ClassUtils.isPresent(className, context.getClassLoader());
        }
    },

    MISSING { // 匹配不成功
        @Override
        public boolean matches(String className, ConditionContext context) {
            return !ClassUtils.isPresent(className, context.getClassLoader());
        }
    };

    public abstract boolean matches(String className, ConditionContext context);

  }

}

SpringBoot还提供了其他比如ConditionalOnBean、ConditionalOnJava、ConditionalOnNotWebApplication、ConditionalOnWebApplication、ConditionalOnResource、ConditionalOnProperty、ConditionalOnExpression等条件注解,有兴趣的读者可以自行查看它们的底层处理逻辑。


三、各种条件注解的总结

条件注解对应的Condition处理类处理逻辑
@ConditionalOnBeanOnBeanConditionSpring容器中是否存在对应的实例。可以通过实例的类型、类名、注解、昵称去容器中查找(可以配置从当前容器中查找或者父容器中查找或者两者一起查找)这些属性都是数组,通过”与”的关系进行查找
@ConditionalOnClassOnClassCondition类加载器中是否存在对应的类。可以通过Class指定(value属性)或者Class的全名指定(name属性)。如果是多个类或者多个类名的话,关系是”与”关系,也就是说这些类或者类名都必须同时在类加载器中存在
@ConditionalOnExpressionOnExpressionCondition判断SpEL 表达式是否成立
@ConditionalOnJavaOnJavaCondition指定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
@ConditionalOnMissingBeanOnBeanConditionSpring容器中是否缺少对应的实例。可以通过实例的类型、类名、注解、昵称去容器中查找(可以配置从当前容器中查找或者父容器中查找或者两者一起查找)这些属性都是数组,通过”与”的关系进行查找。还多了2个属性ignored(类名)和ignoredType(类名),匹配的过程中会忽略这些bean
@ConditionalOnMissingClassOnClassCondition跟ConditionalOnClass的处理逻辑一样,只是条件相反,在类加载器中不存在对应的类
@ConditionalOnNotWebApplicationOnWebApplicationCondition应用程序是否是非Web程序,没有提供属性,只是一个标识。会从判断Web程序特有的类是否存在,环境是否是Servlet环境,容器是否是Web容器等
@ConditionalOnPropertyOnPropertyCondition应用环境中的屬性是否存在。提供prefix、name、havingValue以及matchIfMissing属性。prefix表示属性名的前缀,name是属性名,havingValue是具体的属性值,matchIfMissing是个boolean值,如果属性不存在,这个matchIfMissing为true的话,会继续验证下去,否则属性不存在的话直接就相当于匹配不成功
@ConditionalOnResourceOnResourceCondition是否存在指定的资源文件。只有一个属性resources,是个String数组。会从类加载器中去查询对应的资源文件是否存在
@ConditionalOnSingleCandidateOnBeanConditionSpring容器中是否存在且只存在一个对应的实例。只有3个属性value、type、search。跟ConditionalOnBean中的这3种属性值意义一样
@ConditionalOnWebApplicationOnWebApplicationCondition应用程序是否是Web程序,没有提供属性,只是一个标识。会从判断Web程序特有的类是否存在,环境是否是Servlet环境,容器是否是Web容器等

例子意义
@ConditionalOnBean(javax.sql.DataSource.class)Spring容器或者所有父容器中需要存在至少一个javax.sql.DataSource类的实例
@ConditionalOnClass({ Configuration.class,FreeMarkerConfigurationFactory.class })类加载器中必须存在Configuration和FreeMarkerConfigurationFactory这两个类
@ConditionalOnExpression(“’${server.host}’==’localhost’”)server.host配置项的值需要是localhost
ConditionalOnJava(JavaVersion.EIGHT)Java版本至少是8
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)Spring当前容器中不存在ErrorController类型的bean
@ConditionalOnMissingClass(“GenericObjectPool”)类加载器中不能存在GenericObjectPool这个类
@ConditionalOnNotWebApplication必须在非Web应用下才会生效
@ConditionalOnProperty(prefix = “spring.aop”, name = “auto”, havingValue = “true”, matchIfMissing = true)应用程序的环境中必须有spring.aop.auto这项配置,且它的值是true或者环境中不存在spring.aop.auto配置(matchIfMissing为true)
@ConditionalOnResource(resources=”mybatis.xml”)类加载路径中必须存在mybatis.xml文件
@ConditionalOnSingleCandidate(PlatformTransactionManager.class)Spring当前或父容器中必须存在PlatformTransactionManager这个类型的实例,且只有一个实例
@ConditionalOnWebApplication必须在Web应用下才会生效

四、SpringBoot条件注解的激活机制

想了解SpringBoot条件注解的激活机制的可以参考原文章;

本文转载自:https://www.jianshu.com/p/c4df7be75d6e

;