springboot 自动装配之条件注解(二)

357 阅读3分钟

注:本系列源码分析基于springboot 2.2.2.RELEASE,对应的spring版本为5.2.2.RELEASE,源码的gitee仓库仓库地址:gitee.com/funcy/sprin….

本文是springboot条件注解分析的第二篇,上文我们总结了springboot的几个条件总结:

注解类型注解类型条件判断类
class 条件注解@ConditionalOnClass/@ConditionalOnMissingClassOnClassCondition
bean 条件注解@ConditionalOnBean/@ConditionalOnMissingBeanOnBeanCondition
属性条件注解@ConditionalOnPropertyOnPropertyCondition
Resource 条件注解@ConditionalOnResourceOnResourceCondition
Web 应用条件注解@ConditionalOnWebApplication / @ConditionalOnNotWebApplicationOnWebApplicationCondition
spring表达式条件注解@ConditionalOnExpressionOnExpressionCondition

本文继续分析条件判断。

5. @ConditionalOnPropertyOnPropertyCondition#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. @ConditionalOnResourceOnResourceCondition#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,通过调试方式发现当前的ResourceLoaderAnnotationConfigServletWebServerApplicationContext

获取到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.resourceLoadernull,进入父类的方法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

  • ClassPathContextResource
  • FileUrlResource
  • UrlResource

得到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 的判断,实际上 FileUrlResourceUrlResourceexist()方法都是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的判断还是有些复杂的,这里总结如下:

  1. 如果是classpath文件,通过classloader获取文件对应的url是否为null来判断文件是否存在;
  2. 如果是普通文件,则直接File#exists()方法判断文件是否存在;
  3. 如果是网络文件,先打开一个网络连接,判断文件是否存在。

7. @ConditionalOnWebApplicationOnWebApplicationCondition#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. @ConditionalOnExpressionOnExpressionCondition#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/… ,限于作者个人水平,文中难免有错误之处,欢迎指正!原创不易,商业转载请联系作者获得授权,非商业转载请注明出处。

【springboot源码分析】springboot源码分析系列文章汇总