深入理解SpringBoot 条件注解


  • SpringBoot的条件注解

  • 实现原理

    • @Conditional 原理解析

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


springboot版本: 2.6.0-SNAPSHOT 紧随官网最新稳定版本

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

深入理解SpringBoot 条件注解
image-20211027134413141

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

SpringBoot的条件注解

深入理解SpringBoot 条件注解
image-20211027134502002
条件注解 作用 条件注解解析类
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

实现原理

单纯的注释肯定实现不了这个功能,一般都处理逻辑都在条件注解解析类中,我们来看看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.classexpected)
    .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-INFO路径看到这么一个文件:spring.factories

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h2jXdzCA-1635320720608)(https://gitee.com/weihu666/figure-bed/raw/master/img/image-20211027153145971.png)]

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

深入理解SpringBoot 条件注解
image-20211027153504836

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

深入理解SpringBoot 条件注解
image-20211027153537059

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

深入理解SpringBoot 条件注解
image-20211027153859007

有点人可能会想为什么不直接使用@Component注解注入bean,是因为我们很多三方jar整合的包不在Application下面的子包,SpringBoot默认是只会加载Application及Application子包下面的Bean


原文始发于微信公众号(小奏技术):深入理解SpringBoot 条件注解

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/30243.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!