注:本系列源码分析基于springboot 2.2.2.RELEASE,对应的spring版本为5.2.2.RELEASE,源码的gitee仓库仓库地址:gitee.com/funcy/sprin….
本文是springboot条件注解分析的第二篇,上文我们总结了springboot的几个条件总结:
| 注解类型 | 注解类型 | 条件判断类 |
|---|---|---|
| class 条件注解 | @ConditionalOnClass/@ConditionalOnMissingClass | OnClassCondition |
| bean 条件注解 | @ConditionalOnBean/@ConditionalOnMissingBean | OnBeanCondition |
| 属性条件注解 | @ConditionalOnProperty | OnPropertyCondition |
| Resource 条件注解 | @ConditionalOnResource | OnResourceCondition |
| Web 应用条件注解 | @ConditionalOnWebApplication / @ConditionalOnNotWebApplication | OnWebApplicationCondition |
| spring表达式条件注解 | @ConditionalOnExpression | OnExpressionCondition |
本文继续分析条件判断。
5. @ConditionalOnProperty:OnPropertyCondition#getMatchOutcome
我们再来看看@ConditionalOnProperty的处理,进入OnPropertyCondition#getMatchOutcome方法:
class OnPropertyCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
// 获取 @ConditionalOnProperty 的属性值
List<AnnotationAttributes> allAnnotationAttributes = annotationAttributesFromMultiValueMap(
metadata.getAllAnnotationAttributes(ConditionalOnProperty.class.getName()));
List<ConditionMessage> noMatch = new ArrayList<>();
List<ConditionMessage> match = new ArrayList<>();
for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) {
// 在 determineOutcome(...) 方法中进行判断,注意参数:context.getEnvironment()
ConditionOutcome outcome = determineOutcome(annotationAttributes,
context.getEnvironment());
(outcome.isMatch() ? match : noMatch).add(outcome.getConditionMessage());
}
if (!noMatch.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage.of(noMatch));
}
return ConditionOutcome.match(ConditionMessage.of(match));
}
...
}
这个方法还是比较简单的,先是获取 @ConditionalOnProperty 的属性值,再调用determineOutcome(...)方法进行处理,让我们再进行OnPropertyCondition#determineOutcome方法:
/**
* 处理结果
* 注意:resolver 传入的的是 Environment,这就是 applicationContext 中的 Environment
*/
private ConditionOutcome determineOutcome(AnnotationAttributes annotationAttributes,
PropertyResolver resolver) {
Spec spec = new Spec(annotationAttributes);
List<String> missingProperties = new ArrayList<>();
List<String> nonMatchingProperties = new ArrayList<>();
// 处理操作
spec.collectProperties(resolver, missingProperties, nonMatchingProperties);
// 判断结果
if (!missingProperties.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnProperty.class, spec)
.didNotFind("property", "properties").items(Style.QUOTE, missingProperties));
}
// 判断结果
if (!nonMatchingProperties.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnProperty.class, spec)
.found("different value in property", "different value in properties")
.items(Style.QUOTE, nonMatchingProperties));
}
// 判断结果
return ConditionOutcome.match(ConditionMessage
.forCondition(ConditionalOnProperty.class, spec).because("matched"));
}
/**
* 处理属性
*/
private void collectProperties(PropertyResolver resolver, List<String> missing,
List<String> nonMatching) {
for (String name : this.names) {
String key = this.prefix + name;
// resolver 传入的 environment
// properties 条件判断就是判断 environment 里有没有相应属性
if (resolver.containsProperty(key)) {
if (!isMatch(resolver.getProperty(key), this.havingValue)) {
nonMatching.add(name);
}
}
else {
if (!this.matchIfMissing) {
missing.add(name);
}
}
}
}
可以看到,@ConditionalOnProperty 最终是通过判断environment中是否有该属性来处理条件判断的。
6. @ConditionalOnResource:OnResourceCondition#getMatchOutcome
我们再来看看@ConditionalOnResource的处理,一般我们这样使用:
@Bean
@ConditionalOnResource(resources = "classpath:config.properties")
public Config config() {
return config;
}
表示当classpath中存在config.properties时,config才会被初始化springbean。
再进入OnResourceCondition#getOutcomes方法:
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attributes = metadata
.getAllAnnotationAttributes(ConditionalOnResource.class.getName(), true);
// 获取 ResourceLoader
ResourceLoader loader = context.getResourceLoader();
List<String> locations = new ArrayList<>();
collectValues(locations, attributes.get("resources"));
Assert.isTrue(!locations.isEmpty(),
"@ConditionalOnResource annotations must specify at least one resource location");
List<String> missing = new ArrayList<>();
// 遍历判断资源是否存在
for (String location : locations) {
// location 中可能有占位符,在这里处理
String resource = context.getEnvironment().resolvePlaceholders(location);
// 判断 resource 是否存在
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));
}
先是通过OnResourceCondition#getOutcomes方法来获取ResourceLoader,通过调试方式发现当前的ResourceLoader为AnnotationConfigServletWebServerApplicationContext:
获取到ResourceLoader后,调用ResourceLoader#getResource(String) 来获取资源,然后调用Resource#exists来判断资源是否存在,最后处理匹配结果。
整个流程的关键是在ResourceLoader#getResource(String),我们来看看该方法的处理,进入到GenericApplicationContext#getResource 方法:
@Override
public Resource getResource(String location) {
if (this.resourceLoader != null) {
return this.resourceLoader.getResource(location);
}
return super.getResource(location);
}
这里的this.resourceLoader为null,进入父类的方法DefaultResourceLoader#getResource:
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
Resource resource = protocolResolver.resolve(location, this);
if (resource != null) {
return resource;
}
}
// 处理/开头的资源
if (location.startsWith("/")) {
return getResourceByPath(location);
}
else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
// 处理classpath开头的资源
return new ClassPathResource(
location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
}
else {
try {
// 以上都不满足,使用 url 来解析
URL url = new URL(location);
return (ResourceUtils.isFileURL(url)
? new FileUrlResource(url) : new UrlResource(url));
}
catch (MalformedURLException ex) {
// url解析出了问题,最终还是用 getResourceByPath(...) 来解析
return getResourceByPath(location);
}
}
}
/**
* 通过路径得到 Resource
*/
protected Resource getResourceByPath(String path) {
return new ClassPathContextResource(path, getClassLoader());
}
可以看到,DefaultResourceLoader#getResource通过判断location的前缀,得到了4种Resource:
ClassPathContextResourceFileUrlResourceUrlResource
得到Resource后,接着就是判断该Resource是否存在了,我们先来看看ClassPathContextResource#exist方法,该方法在ClassPathResource#exists:
/**
* 判断 Resource 是否存在
*/
@Override
public boolean exists() {
return (resolveURL() != null);
}
/**
* 资源能获取到,则返回资源对应的url,否则返回null
*/
@Nullable
protected URL resolveURL() {
if (this.clazz != null) {
// 使用当前的 class 对应的 classLoader 来获取
return this.clazz.getResource(this.path);
}
else if (this.classLoader != null) {
// 使用指定的 classLoader 来获取
return this.classLoader.getResource(this.path);
}
else {
// 获取系统类加载器获取
return ClassLoader.getSystemResource(this.path);
}
}
从代码可以看到,最终是通过classLoader获取文件的url,通过判断文件url是否为null来判断resource是否存在。
再来看看 FileUrlResource 的判断,实际上 FileUrlResource 与 UrlResource 的exist()方法都是AbstractFileResolvingResource#exists,这里统一分析就可以了,该方法内容如下:
public boolean exists() {
try {
URL url = getURL();
if (ResourceUtils.isFileURL(url)) {
// 如果是文件,直接判断文件是否存在
return getFile().exists();
}
else {
// 否则使用网络文件来处理
URLConnection con = url.openConnection();
customizeConnection(con);
HttpURLConnection httpCon =
(con instanceof HttpURLConnection ? (HttpURLConnection) con : null);
// 如果是http,则判断看看链接返回的状态码
if (httpCon != null) {
int code = httpCon.getResponseCode();
if (code == HttpURLConnection.HTTP_OK) {
return true;
}
else if (code == HttpURLConnection.HTTP_NOT_FOUND) {
return false;
}
}
// 连接 contentLengthLong 大于0,也当成是true
if (con.getContentLengthLong() > 0) {
return true;
}
if (httpCon != null) {
httpCon.disconnect();
return false;
}
else {
getInputStream().close();
return true;
}
}
}
catch (IOException ex) {
return false;
}
}
如果是本地文件,直接使用File#exists()方法判断文件是否存在,否则就判断网络文件是否存在,判断方式这里就不细说了。
总的来说,springboot 对@ConditionalOnResource的判断还是有些复杂的,这里总结如下:
- 如果是
classpath文件,通过classloader获取文件对应的url是否为null来判断文件是否存在; - 如果是普通文件,则直接
File#exists()方法判断文件是否存在; - 如果是网络文件,先打开一个网络连接,判断文件是否存在。
7. @ConditionalOnWebApplication:OnWebApplicationCondition#getMatchOutcome
我们再来看看@ConditionalOnWebApplication的处理,进入OnWebApplicationCondition#getOutcomes方法:
@Override
protected ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
AutoConfigurationMetadata autoConfigurationMetadata) {
ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length];
for (int i = 0; i < outcomes.length; i++) {
String autoConfigurationClass = autoConfigurationClasses[i];
if (autoConfigurationClass != null) {
// 处理结果
outcomes[i] = getOutcome(autoConfigurationMetadata.get(autoConfigurationClass,
"ConditionalOnWebApplication"));
}
}
return outcomes;
}
/**
* 处理结果
* springboot支持的web类型有两种:SERVLET,REACTIVE
*/
private ConditionOutcome getOutcome(String type) {
if (type == null) {
return null;
}
ConditionMessage.Builder message = ConditionMessage
.forCondition(ConditionalOnWebApplication.class);
// 如果指定的类型是 SERVLET
if (ConditionalOnWebApplication.Type.SERVLET.name().equals(type)) {
if (!ClassNameFilter.isPresent(SERVLET_WEB_APPLICATION_CLASS, getBeanClassLoader())) {
return ConditionOutcome.noMatch(
message.didNotFind("servlet web application classes").atAll());
}
}
// 如果指定的类型是 REACTIVE
if (ConditionalOnWebApplication.Type.REACTIVE.name().equals(type)) {
if (!ClassNameFilter.isPresent(REACTIVE_WEB_APPLICATION_CLASS, getBeanClassLoader())) {
return ConditionOutcome.noMatch(
message.didNotFind("reactive web application classes").atAll());
}
}
// 如果没有指定web类型
if (!ClassNameFilter.isPresent(SERVLET_WEB_APPLICATION_CLASS, getBeanClassLoader())
&& !ClassUtils.isPresent(REACTIVE_WEB_APPLICATION_CLASS, getBeanClassLoader())) {
return ConditionOutcome.noMatch(
message.didNotFind("reactive or servlet web application classes").atAll());
}
return null;
}
这个方法很简单,处理逻辑为:根据@ConditionalOnWebApplication中指定的类型,判断对应的类是否存在,判断方式与@ConditionalOnClass判断类是否存在一致,而两种类型对应的类如下:
- Servlet:
org.springframework.web.context.support.GenericWebApplicationContext - Reactive:
org.springframework.web.reactive.HandlerResult
8. @ConditionalOnExpression:OnExpressionCondition#getMatchOutcome
我们再来看看@ConditionalOnExpression的处理,进入OnExpressionCondition#getOutcomes方法:
/**
* 处理匹配结果
*/
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
// 获取表达式
String expression = (String) metadata.getAnnotationAttributes(
ConditionalOnExpression.class.getName()).get("value");
expression = wrapIfNecessary(expression);
ConditionMessage.Builder messageBuilder = ConditionMessage
.forCondition(ConditionalOnExpression.class, "(" + expression + ")");
// 处理占位符
expression = context.getEnvironment().resolvePlaceholders(expression);
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
if (beanFactory != null) {
// 计算表达式的值
boolean result = evaluateExpression(beanFactory, expression);
return new ConditionOutcome(result, messageBuilder.resultedIn(result));
}
return ConditionOutcome.noMatch(messageBuilder.because("no BeanFactory available."));
}
/**
* 计算表达式的值
*/
private Boolean evaluateExpression(ConfigurableListableBeanFactory beanFactory,
String expression) {
BeanExpressionResolver resolver = beanFactory.getBeanExpressionResolver();
if (resolver == null) {
resolver = new StandardBeanExpressionResolver();
}
// 在这里解析表达式的值
BeanExpressionContext expressionContext = new BeanExpressionContext(beanFactory, null);
Object result = resolver.evaluate(expression, expressionContext);
return (result != null && (boolean) result);
}
可以看到,springboot最终是通过 BeanExpressionResolver#evaluate 方法来计算表达式结果,关于spring表达式,本文就不展开分析了。
好了,spring条件注解的分析就到这里了,需要说明的是,springboot 还 有其他条件注解:
这些注解的判断方式与本文的方式相类似,就不一一进行分析了。
本文原文链接:my.oschina.net/funcy/blog/… ,限于作者个人水平,文中难免有错误之处,欢迎指正!原创不易,商业转载请联系作者获得授权,非商业转载请注明出处。