SpringBoot源码调试(三)---自动配置的条件注解原理揭秘
# SpringBoot的派生条件注解
SpringBoot自动配置是需要满足相应的条件才会自动配置,因此SpringBoot的自动配置大量应用了条件注解ConditionalOnXXX
。如下图:
# Condition接口
源码如下:
Condition接口主要有一个matches方法,该方法决定了是否要注册相应的bean对象。其中matches方法中有两个参数,参数类型分别是ConditionContext和AnnotatedTypeMetadata,这两个参数非常重要。它们分别用来获取一些环境信息和注解元数据从而用在matches方法中判断是否符合条件。
ConditionContext,顾名思义,主要是跟Condition的上下文有关,主要用来获取Registry,BeanFactory,Environment,ResourceLoader和ClassLoader等。那么获取这些用来干什么呢?举个栗子,比如OnResourceCondition需要靠ConditionContext来获取ResourceLoader来加载指定资源,OnClassCondition需要靠ConditionContext来获取ClassLoader来加载指定类等,下面看下其源码:
AnnotatedTypeMetadata,这个跟注解元数据有关,利用AnnotatedTypeMetadata可以拿到某个注解的一些元数据,而这些元数据就包含了某个注解里面的属性,比如前面的栗子,利用AnnotatedTypeMetadata可以拿到@ConditionalOnLinux的注解属性environment的值。下面看下其源码:
Condition接口的具体实现类的matches方法,若matches返回false,则跳过,不进行注册bean的操作;若matches返回true,则不跳过,进行注册bean的操作;
# OnResourceCondition源码分析
现在先来看下一个逻辑及其简单的注解条件类OnResourceCondition
,OnResourceCondition
继承了SpringBootCondition
父类,覆盖了其getMatchOutcome
方法,用于@ConditionalOnResource
注解指定的资源存在与否。OnResourceCondition
的判断逻辑非常简单,主要拿到@ConditionalOnResource
注解指定的资源路径后,然后用ResourceLoader
根据指定路径去加载看资源存不存在。下面直接看代码:
先来看下@ConditionalOnResource
的代码,
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnResourceCondition.class)
public @interface ConditionalOnResource {
/**
* The resources that must be present.
* @return the resource paths that must be present.
*/
String[] resources() default {};
}
再来看OnResourceCondition
的代码:
@Order(Ordered.HIGHEST_PRECEDENCE + 20)
class OnResourceCondition extends SpringBootCondition {
private final ResourceLoader defaultResourceLoader = new DefaultResourceLoader();
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
// 获得@ConditionalOnResource注解的属性元数据
MultiValueMap<String, Object> attributes = metadata
.getAllAnnotationAttributes(ConditionalOnResource.class.getName(), true);
// 获得资源加载器,若ConditionContext中有ResourceLoader则用ConditionContext中的,没有则用默认的
ResourceLoader loader = (context.getResourceLoader() != null)
? context.getResourceLoader() : this.defaultResourceLoader;
List<String> locations = new ArrayList<>();
// 将@ConditionalOnResource中定义的resources属性值取出来装进locations集合
collectValues(locations, attributes.get("resources"));
Assert.isTrue(!locations.isEmpty(),
"@ConditionalOnResource annotations must specify at "
+ "least one resource location");
// missing集合是装不存在指定资源的资源路径的
List<String> missing = new ArrayList<>();
// 遍历所有的资源路径,若指定的路径的资源不存在则将其资源路径存进missing集合中
for (String location : locations) {
// 这里针对有些资源路径是Placeholders的情况,即处理${}
String resource = context.getEnvironment().resolvePlaceholders(location);
if (!loader.getResource(resource).exists()) {
missing.add(location);
}
}
// 如果存在某个资源不存在,那么则报错
if (!missing.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnResource.class)
.didNotFind("resource", "resources").items(Style.QUOTE, missing));
}
// 所有资源都存在,那么则返回能找到就提的资源
return ConditionOutcome
.match(ConditionMessage.forCondition(ConditionalOnResource.class)
.found("location", "locations").items(locations));
}
// 将@ConditionalOnResource中定义的resources属性值取出来装进locations集合
private void collectValues(List<String> names, List<Object> values) {
for (Object value : values) {
for (Object item : (Object[]) value) {
names.add((String) item);
}
}
}
}
getMatchOutcome()方法实现SpringBootCondition
public final boolean matches(ConditionContext context,
AnnotatedTypeMetadata metadata) {
// 得到metadata的类名或方法名
String classOrMethodName = getClassOrMethodName(metadata);
try {
// 判断每个配置类的每个条件注解@ConditionalOnXXX是否满足条件,然后记录到ConditionOutcome结果中
// 注意getMatchOutcome是一个抽象模板方法,交给OnXXXCondition子类去实现
ConditionOutcome outcome = getMatchOutcome(context, metadata);
// 打印condition评估的日志,哪些条件注解@ConditionalOnXXX是满足条件的,哪些是不满足条件的,这些日志都打印出来
logOutcome(classOrMethodName, outcome);
// 除了打印日志外,这些是否匹配的信息还要记录到ConditionEvaluationReport中
recordEvaluation(context, classOrMethodName, outcome);
// 最后返回@ConditionalOnXXX是否满足条件
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);
}
}
public abstract ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata);
# 总结
- SpringBoot的所有@ConditionalOnXxx的条件类OnXxxCondition都是继承于SpringBootCondition基类,而SpringBootCondition又实现了Condition接口。
- SpringBootCondition基类主要用来打印一些条件注解评估报告的日志,这些条件评估信息全部来源于其子类注解条件类OnXxxCondition,因此其也抽象了一个模板方法getMatchOutcome留给子类去实现来评估其条件注解是否符合条件。