本文由 简悦 SimpRead 转码, 原文地址 blog.csdn.net
概述
想要搞懂 Spring Boot 自动配置,绕不过条件注解,即 @Conditional,可用于根据某个特定的条件来判断是否需要创建某个特定的 Bean。本文分析基于 spring-boot-autoconfigure-3.2.4 版本。
@Conditional 注解可以添加在被 @Configuration、@Component、@Service 等修饰的类,或在被 @Bean 修饰的方法上,用于控制类或方法对应的 Bean 是否需要创建。
@Conditional 注解需要和 Condition 接口搭配一起使用。通过对应 Condition 接口来告知是否满足匹配条件。
扩展注解
| 条件注解 | 对应 Condition 处理类 | 解释 |
|---|---|---|
| ConditionalOnClass | OnClassCondition | 类加载器中存在指定类 |
| ConditionalOnMissingClass | OnClassCondition | 类加载器中不存在指定类 |
| ConditionalOnBean | OnBeanCondition | Spring 容器中存在指定 Bean |
| ConditionalOnMissingBean | OnBeanCondition | Spring 容器中不存在指定 Bean |
| ConditionalOnSingleCandidate | OnBeanCondition | Spring 容器中是否存在且只存在一个对应的实例,或虽然有多个但是指定首选的 Bean 生效 |
| ConditionalOnJava | OnJavaCondition | 指定 Java 版本符合要求生效 |
| ConditionalOnJndi | OnJndiCondition | 存在 JNDI |
| ConditionalOnCloudPlatform | OnCloudPlatformCondition | 云平台,支持:CLOUD_FOUNDRY、HEROKU、SAP、NOMAD、KUBERNETES |
| ConditionalOnCheckpointRestore | 无 | 存在类orc.crac.Resource |
| ConditionalOnWebApplication | OnWebApplicationCondition | Web 应用生效 |
| ConditionalOnNotWebApplication | OnWebApplicationCondition | 不是 Web 应用生效 |
| ConditionalOnWarDeployment | OnWarDeploymentCondition | War 应用生效 |
| ConditionalOnNotWarDeployment | OnWarDeploymentCondition | 不是 War 应用生效 |
| ConditionalOnResource | OnResourceCondition | 当指定资源文件出现则生效 |
| ConditionalOnProperty | OnPropertyCondition | 应用环境中的属性满足条件生效 |
| ConditionalOnExpression | OnExpressionCondition | 判断 SpEL 表达式成立生效 |
| ConditionalOnThreading | OnThreadingCondition | 指定线程处于 active 状态 |
ConditionalOnCheckpointRestore 源码如下:
@ConditionalOnClass(name = {"org.crac.Resource"})
public @interface ConditionalOnCheckpointRestore {
}
CRaC 是 OpenJDK 项目,有兴趣可延伸阅读。
原理
条件注解存在的意义在于动态识别,即代码自动化执行。如 @ConditionalOnClass 会检查类加载器中是否存在对应的类,如果有的话被注解修饰的类就有资格被 Spring 容器所注册,否则会被 skip。
如 FreemarkerAutoConfiguration 这个自动化配置类的定义如下:
@AutoConfiguration
@ConditionalOnClass({ freemarker.template.Configuration.class, FreeMarkerConfigurationFactory.class })
@EnableConfigurationProperties(FreeMarkerProperties.class)
@Import({ FreeMarkerServletWebConfiguration.class, FreeMarkerReactiveWebConfiguration.class, FreeMarkerNonWebConfiguration.class })
public class FreeMarkerAutoConfiguration {
}
这个自动化配置类被 @ConditionalOnClass 条件注解修饰,判断类加载器中是否存在freemarker.template.Configuration和 FreeMarkerConfigurationFactory 这两个类,如果都存在的话会在 Spring 容器中加载这个 FreeMarkerAutoConfiguration 配置类;否则不会加载。
@Conditional 源码:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}
需要传入一个 Class 数组,数组类型是 Condition。而 Condition 是个接口,用于匹配组件是否有资格被容器注册:
@FunctionalInterface
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 中定义的两种阶段下才会生效:
- PARSE_CONFIGURATION
- REGISTER_BEAN
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 的条件注解
有两个
- @ConditionalOnClass
- @ConditionalOnMissingClass
@ConditionalOnClass 注解定义如下:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
Class<?>[] value() default {}; // 需要匹配的类
String[] name() default {}; // 需要匹配的类名
}
它有 2 个属性,分别是类数组和字符串数组(作用一样,类型不一样),而且被 @Conditional 注解所修饰。
对应条件类是 OnClassCondition:
@Order(Ordered.HIGHEST_PRECEDENCE) // 优先级最高级别
class OnClassCondition extends FilteringSpringBootCondition {
@Override
protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
AutoConfigurationMetadata autoConfigurationMetadata) {
// Split the work and perform half in a background thread if more than one
// processor is available. Using a single additional thread seems to offer the
// best performance. More threads make things worse.
if (autoConfigurationClasses.length > 1 && Runtime.getRuntime().availableProcessors() > 1) {
return resolveOutcomesThreaded(autoConfigurationClasses, autoConfigurationMetadata);
}
else {
OutcomesResolver outcomesResolver = new StandardOutcomesResolver(autoConfigurationClasses, 0,
autoConfigurationClasses.length, autoConfigurationMetadata, getBeanClassLoader());
return outcomesResolver.resolveOutcomes();
}
}
}
比如 FreemarkerAutoConfiguration 中的 @ConditionalOnClass 注解中有 value 属性是freemarker.template.Configuration.class和FreeMarkerConfigurationFactory.class。在 OnClassCondition 执行过程中得到的最终 ConditionalOutcome 中的 log message 如下:
@ConditionalOnClass classes found: freemarker.template.Configuration,org.springframework.ui.freemarker.FreeMarkerConfigurationFactory
基于 Bean 的条件注解
有 3 个:
- @ConditionalOnBean
- @ConditionalOnMissingBean
- @ConditionalOnSingleCandidate
和基于类的条件注解比较类似。
激活机制
这部分有点难,想通过阅读源码来理清楚前后调用及解析关系。好在我们可以断点调试。通过断点调试发现关键类和方法:
- ConfigurationClassParser
- ConditionEvaluator
- ComponentScanAnnotationParser
SpringBoot 使用 ConditionEvaluator 这个内部类完成条件注解的解析和判断。在 Spring 容器的 refresh 过程中,只有跟解析或者注册 bean 有关系的类都会使用 ConditionEvaluator 完成条件注解的判断,这个过程中一些类不满足条件的话就会被 skip。这些类比如有 AnnotatedBeanDefinitionReader、ConfigurationClassBeanDefinitionReader、ConfigurationClassParse、ClassPathScanningCandidateComponentProvider 等。
比如 ConfigurationClassParser 的构造函数会初始化内部属性 conditionEvaluator:
public ConfigurationClassParser(MetadataReaderFactory metadataReaderFactory,
ProblemReporter problemReporter, Environment environment, ResourceLoader resourceLoader,
BeanNameGenerator componentScanBeanNameGenerator, BeanDefinitionRegistry registry) {
this.metadataReaderFactory = metadataReaderFactory;
this.problemReporter = problemReporter;
this.environment = environment;
this.resourceLoader = resourceLoader;
this.registry = registry;
this.componentScanParser = new ComponentScanAnnotationParser(resourceLoader, environment, componentScanBeanNameGenerator, registry);
// 构造ConditionEvaluator用于处理条件注解
this.conditionEvaluator = new ConditionEvaluator(registry, environment, resourceLoader);
}
ConfigurationClassParser 对每个配置类进行解析的时候都会使用 ConditionEvaluator:
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}
ConditionEvaluator 的 skip 方法:
public boolean shouldSkip(AnnotatedTypeMetadata metadata, ConfigurationPhase phase) {
// 如果这个类没有被@Conditional注解所修饰,不会skip
if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
return false;
}
// 如果参数中沒有设置条件注解的生效阶段
if (phase == null) {
// 是配置类的话直接使用PARSE_CONFIGURATION阶段
if (metadata instanceof AnnotationMetadata && ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
}
// 否则使用REGISTER_BEAN阶段
return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
}
// 要解析的配置类的条件集合
List<Condition> conditions = new ArrayList<Condition>();
// 获取配置类的条件注解得到条件数据,并添加到集合中
for (String[] conditionClasses : getConditionClasses(metadata)) {
for (String conditionClass : conditionClasses) {
Condition condition = getCondition(conditionClass, this.context.getClassLoader());
conditions.add(condition);
}
}
// 对条件集合做个排序
AnnotationAwareOrderComparator.sort(conditions);
// 遍历条件集合
for (Condition condition : conditions) {
ConfigurationPhase requiredPhase = null;
if (condition instanceof ConfigurationCondition) {
requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
}
// 没有这个解析类不需要阶段的判断或者解析类和参数中的阶段一致才会继续进行
if (requiredPhase == null || requiredPhase == phase) {
// 阶段一致切不满足条件的话,返回true并跳过这个bean的解析
if (!condition.matches(this.context, metadata)) {
return true;
}
}
}
return false;
}
SpringBoot 在条件注解的解析 log 记录在 ConditionEvaluationReport 类中,可通过 BeanFactory 获取。BeanFactory 是有父子关系的;每个 BeanFactory 都存有一份 ConditionEvaluationReport,互不相干:
ConditionEvaluationReport conditionEvaluationReport = beanFactory.getBean("autoConfigurationReport", ConditionEvaluationReport.class);
Map<String, ConditionEvaluationReport.ConditionAndOutcomes> result = conditionEvaluationReport.getConditionAndOutcomesBySource();
for(String key : result.keySet()) {
ConditionEvaluationReport.ConditionAndOutcomes conditionAndOutcomes = result.get(key);
Iterator<ConditionEvaluationReport.ConditionAndOutcome> iterator = conditionAndOutcomes.iterator();
while(iterator.hasNext()) {
ConditionEvaluationReport.ConditionAndOutcome conditionAndOutcome = iterator.next();
System.out.println(key + " -- " + conditionAndOutcome.getCondition().getClass().getSimpleName() + " -- " + conditionAndOutcome.getOutcome());
}
}
打印出条件注解下的类加载信息:
...
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: freemarker.template.Configuration,org.springframework.ui.freemarker.FreeMarkerConfigurationFactory
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: groovy.text.markup.MarkupTemplateEngine
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: com.google.gson.Gson
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: org.h2.server.web.WebServlet
org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: org.springframework.hateoas.Resource,org.springframework.plugin.core.Plugin
org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: com.hazelcast.core.HazelcastInstance
...
实战
自定义
需要自定义一个 condition 类实现 Condition 接口,假设根据系统类型来加载不同的 Bean:
public class OnSystemCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(ConditionalOnSystem.class.getName());
if (annotationAttributes == null) {
return false;
}
ConditionalOnSystem.SystemType systemType = (ConditionalOnSystem.SystemType) annotationAttributes.get("type");
switch (systemType) {
case WINDOWS:
return context.getEnvironment().getProperty("os.name").contains("Windows");
case LINUX:
return context.getEnvironment().getProperty("os.name").contains("Linux ");
case MAC:
return context.getEnvironment().getProperty("os.name").contains("Mac ");
}
return false;
}
}
自定义条件注解并指定对应的处理 condition 类:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional(OnSystemCondition.class)
public @interface ConditionalOnSystem {
/**
* 指定系统
*/
SystemType type() default SystemType.WINDOWS;
/**
* 系统类型
*/
enum SystemType {
WINDOWS,
LINUX,
MAC;
}
}