SpringBoot的条件注解@ConditionalOnXxx

SpringBoot中有一个很重要的模块,那就是spring-boot-autoconfigure。可以说我们所有的三方依赖的自动配置AutoConfiguration(自动化配置类),比如KafkaAutoConfiguration、GsonAutoConfiguration、WebMvcAutoConfiguration、JsonbAutoConfiguration等。

这些AutoConfiguration都只会在特定情况下才会生效,具体是如何实现的呢?就是依赖于SpringBoot的条件注解。

SpringBoot的条件注解

条件注解 作用 条件注解解析类
ConditionalOnBean ApplicationContext存在某些bean时条件才成立 OnBeanCondition
ConditionalOnClass classPath中存在某些Class时条件才成立 OnClassCondition
ConditionalOnCloudPlatform 在某些云平台(Kubemetes、Heroku、Cloud Foundry)下条件才成立 OnCloudPlatformCondition
ConditionalOnExpression SPEL表达式成立时条件才成立 OnExpressionCondition
ConditionalOnJava JDK某些版本条件才成立 OnJavaCondition
ConditionalOnJndi JNDI路径下存在时条件才成立 OnJndiCondition
ConditionalOnMissingBean ApplicationContext不存在某些bean时条件才成立 OnBeanCondition
ConditionalOnMissingClass classpath中不存在某些class是条件才成立 OnClassCondition
ConditionalOnNotWebApplication 在非Web环境下条件才成立 OnWebApplicationCondition
ConditionalOnProperty Environment中存在某些配置项时条件才成立 OnPropertyCondition
ConditionalOnResource 存在某些资源时条件才成立 OnResourceCondition
ConditionalOnSingleCandidate ApplicationContext中存在且只有一个bean时条件成立 OnBeanCondition
ConditionalOnWebApplication 在Web环境下条件才成立 OnWebApplicationCondition

注意:一些如@ConditionalOnClass注解,提供了条件类型是Class或字符串两种方式,这里的字符串一般是类的全路径名(包名+类名)

实现原理

单纯的注释肯定实现不了这个功能,处理逻辑一般都在条件注解解析类中,我们来看看OnJndiCondition是如何实现的。

@Order(Ordered.HIGHEST_PRECEDENCE + 20)
class OnJavaCondition extends SpringBootCondition {

    private static final JavaVersion JVM_VERSION = JavaVersion.getJavaVersion();

    /**
	 * 根据 JavaVersion 获取当前jdk版本, JavaVersion 是SpringBoot 自己封装的
	 * JavaVersion中的实现主要是根据 jdk特有版本的一些方法来判断 jdk的版本的
	 * 比如: 判断Optional empty方法是否存在,来看这是不是 jdk8 判断 Optional stream方法是否存在,来看是不是 jdk9
	 * ConditionOutcome 包含两个属性 match:条件是否匹配的结果 ConditionMessage:条件的匹配结果的信息对象
	 * @param context the condition context
	 * @param metadata the annotation metadata
	 * @return
	 */
    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 获取 ConditionalOnJava 注解的属性
        Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnJava.class.getName());
        Range range = (Range) attributes.get("range");
        JavaVersion version = (JavaVersion) attributes.get("value");
        // 判断当前jdk版本是否满足条件
        return getMatchOutcome(range, JVM_VERSION, version);
    }

    protected ConditionOutcome getMatchOutcome(Range range, JavaVersion runningVersion, JavaVersion version) {
        boolean match = isWithin(runningVersion, range, version);
        String expected = String.format((range != Range.EQUAL_OR_NEWER) ? "(older than %s)" : "(%s or newer)", version);
        ConditionMessage message = ConditionMessage.forCondition(ConditionalOnJava.class, expected)
            .foundExactly(runningVersion);
        return new ConditionOutcome(match, message);
    }

    /**
	 * Determines if the {@code runningVersion} is within the specified range of versions.
	 * @param runningVersion the current version.
	 * @param range the range
	 * @param version the bounds of the range
	 * @return if this version is within the specified range
	 */
    private boolean isWithin(JavaVersion runningVersion, Range range, JavaVersion version) {
        if (range == Range.EQUAL_OR_NEWER) {
            return runningVersion.isEqualOrNewerThan(version);
        }
        if (range == Range.OLDER_THAN) {
            return runningVersion.isOlderThan(version);
        }
        throw new IllegalStateException("Unknown range " + range);
    }

}

其实我们看到上面的OnJavaCondition是继承了抽象类SpringBootCondition。

而抽象类SpringBootCondition实现了Condition接口

public abstract class SpringBootCondition implements Condition {

    private final Log logger = LogFactory.getLog(getClass());

    @Override
    public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String classOrMethodName = getClassOrMethodName(metadata);
        try {
            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);
        }
    }

    private String getName(AnnotatedTypeMetadata metadata) {
        if (metadata instanceof AnnotationMetadata) {
            return ((AnnotationMetadata) metadata).getClassName();
        }
        if (metadata instanceof MethodMetadata) {
            MethodMetadata methodMetadata = (MethodMetadata) metadata;
            return methodMetadata.getDeclaringClassName() + "." + methodMetadata.getMethodName();
        }
        return metadata.toString();
    }

    private static String getClassOrMethodName(AnnotatedTypeMetadata metadata) {
        if (metadata instanceof ClassMetadata) {
            ClassMetadata classMetadata = (ClassMetadata) metadata;
            return classMetadata.getClassName();
        }
        MethodMetadata methodMetadata = (MethodMetadata) metadata;
        return methodMetadata.getDeclaringClassName() + "#" + methodMetadata.getMethodName();
    }

    protected final void logOutcome(String classOrMethodName, ConditionOutcome outcome) {
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(getLogMessage(classOrMethodName, outcome));
        }
    }

    private StringBuilder getLogMessage(String classOrMethodName, ConditionOutcome outcome) {
        StringBuilder message = new StringBuilder();
        message.append("Condition ");
        message.append(ClassUtils.getShortName(getClass()));
        message.append(" on ");
        message.append(classOrMethodName);
        message.append(outcome.isMatch() ? " matched" : " did not match");
        if (StringUtils.hasLength(outcome.getMessage())) {
            message.append(" due to ");
            message.append(outcome.getMessage());
        }
        return message;
    }

    private void recordEvaluation(ConditionContext context, String classOrMethodName, ConditionOutcome outcome) {
        if (context.getBeanFactory() != null) {
            ConditionEvaluationReport.get(context.getBeanFactory()).recordConditionEvaluation(classOrMethodName, this,
                                                                                              outcome);
        }
    }

    /**
	 * Determine the outcome of the match along with suitable log output.
	 * @param context the condition context
	 * @param metadata the annotation metadata
	 * @return the condition outcome
	 */
    public abstract ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata);

    /**
	 * Return true if any of the specified conditions match.
	 * @param context the context
	 * @param metadata the annotation meta-data
	 * @param conditions conditions to test
	 * @return {@code true} if any condition matches.
	 */
    protected final boolean anyMatches(ConditionContext context, AnnotatedTypeMetadata metadata,
                                       Condition... conditions) {
        for (Condition condition : conditions) {
            if (matches(context, metadata, condition)) {
                return true;
            }
        }
        return false;
    }

    /**
	 * Return true if any of the specified condition matches.
	 * @param context the context
	 * @param metadata the annotation meta-data
	 * @param condition condition to test
	 * @return {@code true} if the condition matches.
	 */
    protected final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata, Condition condition) {
        if (condition instanceof SpringBootCondition) {
            return ((SpringBootCondition) condition).getMatchOutcome(context, metadata).isMatch();
        }
        return condition.matches(context, metadata);
    }

}

@Conditional 原理解析

需要注意的是@Conditional不是springboot提供的注解,是spring提供的,所以Springboot上面的接口都是基于Spring的@Conditional接口扩展的。

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

    /**
     * All {@link Condition} classes that must {@linkplain Condition#matches match}
     * in order for the component to be registered.
     */
    Class<? extends Condition>[] value();

}

可以看到 Conditional源码中的属性需要传递一个实现了接口Condition的类。

我们简单来使用下,我们随便定义一个自己的Condition实现类。

public class MyConditional implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return false;
    }
}

这里我们设置 matches 方法返回的是 false。

然后我们再定义一个配置类

@Component
@Conditional(MyConditional.class)
public class TestConfig {


    public TestConfig() {
        System.out.println("测试我加载了");
    }
}

我们在启动的时候可以看到TestConfig类是不会被加载,如果将matches方法改为true就会加载了

条件注解解析类(类似OnJavaCondition)到底怎么自动加载的

我们知道了条件解析类的实现类如OnJavaCondition,那它是如何被SpringBoot加载的呢?

这里我们就需要了解SpringBoot的工作加载机制了。

一般我们可以看到一些SpringBoot整合的三方jar通常会在META-INF路径看到这么一个文件:spring.factories

SpringBoot应用在启动的时候都会加@SpringBootApplication注解,而@SpringBootApplication注解内部又包含EnableAutoConfiguration注解。

EnableAutoConfiguration注解里面有导入了AutoConfigurationImportSelector.class类

在 AutoConfigurationImportSelector类中又通过Spring的SpringFactoriesLoader取加载类,即Spring的Spi机制

 

posted @ 2022-01-18 12:57  残城碎梦  阅读(582)  评论(0编辑  收藏  举报