『深入学习 Spring Boot』(十四) Conditional 与 自定义 Starter

1,249 阅读3分钟

前言

前面已经学习过 Spring Boot 配置类、自动装配原理。

这一节我们来学习一下,如何编写一个 Spring Boot Starter。

@Conditional 注解

Spring Boot 自动装配,是自动将某些配置类加载到 Context 中。

那么有加载就必定有过滤的方法,因为我们不可能一次性就把所有的自动配置类加载上,而是按需加载。

所以,在开始创建 Starter 之前,我们需要先了解一下 加载配置类的条件注解 @Conditional

常用的 Conditional 注解

  • @ConditionalOnProperty

    根据某个属性判断是否加载该配置类。

    示例:@ConditionalOnProperty(prefix = “book”, name = “book_name”, havingValue = “lalala”)

    当环境上下文中,有book.book_name=lalala属性时,该类才会被加载。

  • @ConditionalOnBean

    某 Bean 存在时,才加载此类。

    与之相反的有:@ConditionalOnMissingBean

  • @ConditionalOnClass

    当某个 Class 存在时,才加载此类。

    与之相反的有:@ConditionalOnMissingClass

  • @ConditionalOnExpression

    根据表达式(SpEL)判断加载此类。

Conditional 的使用实例

下面是 Spring Data Redis 的自动配置类,当我们需要写自己的 Starter 时,也可以模仿着来写。

@Configuration
@ConditionalOnClass(RedisOperations.class) // 当RedisOperations.class存在时,才加载此类
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
 
    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate") // 如果 Context 中没有名为 redisTemplate 的 Bean,才加载此 Bean。
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
 
    @Bean
    @ConditionalOnMissingBean // 同上
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
 
}

Conditional 原理

以上各类 Conditional 注解,都是基于一个注解衍生而来的。

  1. 表示只有在所有指定条件都匹配时,组件才有资格注册。

  2. @Conditional注释可以通过以下任何一种方式使用:

  • 作为任何直接或间接用@Component注释的类的类型级注释,包括@Configuration类

  • 作为元注释,用于编写自定义构造型注释

  • 作为任何@Bean方法的方法级注解

  1. 如果@Configuration类被标记为@Conditional ,则与@Conditional所有@Bean方法、 @Import @Bean注释和@ComponentScan注释都将受条件约束。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
    Class<? extends Condition>[] value();
}

此注解接受一个 Condition 的子类,让我们看一下 Condition 接口:

必须匹配才能注册组件的单个condition 。

在注册 bean 定义之前立即检查条件,并且可以根据当时可以确定的任何标准自由否决注册。

@FunctionalInterface
public interface Condition {
  /* 确定条件是否匹配。 */
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

此接口只有一个方法,用于确定加载配置类的条件是否满足。

之前在《『深入学习 Spring Boot』(十二) Configuration 配置类解析》其中有一小节:

protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
        // 判断是否符合解析条件
        if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
            return;
        }
        ......
        // Recursively process the configuration class and its superclass hierarchy.
        // 递归处理配置类及其超类层次结构。
        // 等于说 doProcessConfigurationClass 是处理加载 Configuration 的核心递归逻辑。
        SourceClass sourceClass = asSourceClass(configClass);
        do {
            sourceClass = doProcessConfigurationClass(configClass, sourceClass);
        }
        while (sourceClass != null);
 
        this.configurationClasses.put(configClass, configClass);
    }

此处 if 语句就是调用 macth 方法的地方,如果 if 条件为true,则跳过解析这个配置类。

ConditionEvaluator

此类主要就是封装 macth 方法的。这里截取两个核心方法:

  /* 根据@Conditional注释确定是否应跳过某个项目。*/
    public boolean shouldSkip(AnnotatedTypeMetadata metadata) {
        return shouldSkip(metadata, null);
    }
 
    public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
     // 条件判断
        if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
            return false;
        }
 
    // 由于上面设置了 null,总会进入这个if逻辑体。
        if (phase == null) {
      // 如果是注解元数据 并且是配置类
            if (metadata instanceof AnnotationMetadata &&
                    ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
        // 设置ConfigurationPhase类型为解析配置类
                return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
            }
      // 否则设置ConfigurationPhase类型为注册Bean
            return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
        }
 
    // 获取 Condition 的实现
        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);
    // 执行 match 方法
        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;
    }

自定义 Starter

经过这么久的铺垫,我们再来自定义 Starter 就很简单的了。以 redis 的 autoConfiguration 为例。

  1. 编写 配置类、添加条件

    这里就是注册了两个我们很常用的 Template 。

    @Configuration
    // 当RedisOperations存在时,才加载此类
    @ConditionalOnClass(RedisOperations.class)
    // 显示指定将 RedisProperties 加载到上下文中
    @EnableConfigurationProperties(RedisProperties.class)
    // 导入其他的配置类
    @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
    public class RedisAutoConfiguration {
     
        @Bean
        @ConditionalOnMissingBean(name = "redisTemplate")
        public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
                throws UnknownHostException {
            RedisTemplate<Object, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(redisConnectionFactory);
            return template;
        }
     
        @Bean
        @ConditionalOnMissingBean
        public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
                throws UnknownHostException {
            StringRedisTemplate template = new StringRedisTemplate();
            template.setConnectionFactory(redisConnectionFactory);
            return template;
        }
     
    }
    
  2. 在 spring.factories 中添加配置类

    # Auto Configure
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
    

总结

这一小节学习了 Spring Boot 自动配置的另外一重要内容:Conditional 注解 以及 Conditional 接口。

此外,还简单学习了一下,自定义 Starter 。其实当我们将 Spring 配置类、Spring Boot 自动装配以及Conditional 了解完之后,写一个 自己的 Starter 是件很容易的事情。