前言
前面已经学习过 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 注解,都是基于一个注解衍生而来的。
表示只有在所有指定条件都匹配时,组件才有资格注册。
@Conditional注释可以通过以下任何一种方式使用:
作为任何直接或间接用@Component注释的类的类型级注释,包括@Configuration类
作为元注释,用于编写自定义构造型注释
作为任何@Bean方法的方法级注解
- 如果@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 为例。
-
编写 配置类、添加条件
这里就是注册了两个我们很常用的 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; } } -
在 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 是件很容易的事情。