【springboot源碼分析】springboot 自動裝配(三):條件註解(二)

注:本系列源碼分析基於springboot 2.2.2.RELEASE,對應的spring版本爲5.2.2.RELEASE,源碼的gitee倉庫倉庫地址:funcy/spring-boot.

本文是springboot條件註解分析的第二篇,上文我們總結了springboot的幾個條件總結:

註解類型 註解類型 條件判斷類
class 條件註解 @ConditionalOnClass/@ConditionalOnMissingClass OnClassCondition
bean 條件註解 @ConditionalOnBean/@ConditionalOnMissingBean OnBeanCondition
屬性條件註解 @ConditionalOnProperty OnPropertyCondition
Resource 條件註解 @ConditionalOnResource OnResourceCondition
Web 應用條件註解 @ConditionalOnWebApplication / @ConditionalOnNotWebApplication OnWebApplicationCondition
spring表達式條件註解 @ConditionalOnExpression OnExpressionCondition

本文繼續分析條件判斷。

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 還 有其他條件註解:

這些註解的判斷方式與本文的方式相類似,就不一一進行分析了。


本文原文鏈接:https://my.oschina.net/funcy/blog/4921590 ,限於作者個人水平,文中難免有錯誤之處,歡迎指正!原創不易,商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

【springboot源碼分析】springboot源碼分析系列文章彙總

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章