1. 程式人生 > >SpringBoot環境屬性佔位符解析和型別轉換

SpringBoot環境屬性佔位符解析和型別轉換

前提

前面寫過一篇關於Environment屬性載入的原始碼分析和擴充套件,裡面提到屬性的佔位符解析和型別轉換是相對複雜的,這篇文章就是要分析和解讀這兩個複雜的問題。關於這兩個問題,選用一個比較複雜的引數處理方法PropertySourcesPropertyResolver#getProperty,解析佔位符的時候依賴到PropertySourcesPropertyResolver#getPropertyAsRawString

protected String getPropertyAsRawString(String key) {
    return getProperty(key, String.class, false
); } protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) { if (this.propertySources != null) { for (PropertySource<?> propertySource : this.propertySources) { if (logger.isTraceEnabled()) { logger.trace("Searching for key '"
+ key + "' in PropertySource '" + propertySource.getName() + "'"); } Object value = propertySource.getProperty(key); if (value != null) { if (resolveNestedPlaceholders && value instanceof String) { //解析帶有佔位符的屬性
value = resolveNestedPlaceholders((String) value); } logKeyFound(key, propertySource, value); //需要時轉換屬性的型別 return convertValueIfNecessary(value, targetValueType); } } } if (logger.isDebugEnabled()) { logger.debug("Could not find key '" + key + "' in any property source"); } return null; }

屬性佔位符解析

屬性佔位符的解析方法是PropertySourcesPropertyResolver的父類AbstractPropertyResolver#resolveNestedPlaceholders

protected String resolveNestedPlaceholders(String value) {
    return (this.ignoreUnresolvableNestedPlaceholders ?
        resolvePlaceholders(value) : resolveRequiredPlaceholders(value));
}

ignoreUnresolvableNestedPlaceholders屬性預設為false,可以通過AbstractEnvironment#setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders)設定,當此屬性被設定為true,解析屬性佔位符失敗的時候(並且沒有為佔位符配置預設值)不會丟擲異常,返回屬性原樣字串,否則會丟擲IllegalArgumentException。我們這裡只需要分析AbstractPropertyResolver#resolveRequiredPlaceholders

//AbstractPropertyResolver中的屬性:
//ignoreUnresolvableNestedPlaceholders=true情況下建立的PropertyPlaceholderHelper例項
@Nullable
private PropertyPlaceholderHelper nonStrictHelper;

//ignoreUnresolvableNestedPlaceholders=false情況下建立的PropertyPlaceholderHelper例項
@Nullable
private PropertyPlaceholderHelper strictHelper;

//是否忽略無法處理的屬性佔位符,這裡是false,也就是遇到無法處理的屬性佔位符且沒有預設值則丟擲異常
private boolean ignoreUnresolvableNestedPlaceholders = false;

//屬性佔位符字首,這裡是"${"
private String placeholderPrefix = SystemPropertyUtils.PLACEHOLDER_PREFIX;

//屬性佔位符字尾,這裡是"}"
private String placeholderSuffix = SystemPropertyUtils.PLACEHOLDER_SUFFIX;

//屬性佔位符解析失敗的時候配置預設值的分隔符,這裡是":"
@Nullable
private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR;


public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
    if (this.strictHelper == null) {
        this.strictHelper = createPlaceholderHelper(false);
    }
    return doResolvePlaceholders(text, this.strictHelper);
}

//建立一個新的PropertyPlaceholderHelper例項,這裡ignoreUnresolvablePlaceholders為false
private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) {
    return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix, this.valueSeparator, ignoreUnresolvablePlaceholders);
}

//這裡最終的解析工作委託到PropertyPlaceholderHelper#replacePlaceholders完成
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
    return helper.replacePlaceholders(text, this::getPropertyAsRawString);
}

最終只需要分析PropertyPlaceholderHelper#replacePlaceholders,這裡需要重點注意:
- 注意到這裡的第一個引數text就是屬性值的源字串,例如我們需要處理的屬性為myProperties: server.port{spring.application.name},這裡的text就是server\.port{spring.application.name}。
- replacePlaceholders方法的第二個引數placeholderResolver,這裡比較巧妙,這裡的方法引用this::getPropertyAsRawString相當於下面的程式碼:

//PlaceholderResolver是一個函式式介面
@FunctionalInterface
public interface PlaceholderResolver {
  @Nullable
  String resolvePlaceholder(String placeholderName);  
}
//this::getPropertyAsRawString相當於下面的程式碼
return new PlaceholderResolver(){

    @Override
    String resolvePlaceholder(String placeholderName){
        //這裡呼叫到的是PropertySourcesPropertyResolver#getPropertyAsRawString,有點繞
        return getPropertyAsRawString(placeholderName);
    }
}       

接著看PropertyPlaceholderHelper#replacePlaceholders的原始碼:

//基礎屬性
//佔位符字首,預設是"${"
private final String placeholderPrefix;
//佔位符字尾,預設是"}"
private final String placeholderSuffix;
//簡單的佔位符字首,預設是"{",主要用於處理巢狀的佔位符如${xxxxx.{yyyyy}}
private final String simplePrefix;

//預設值分隔符號,預設是":"
@Nullable
private final String valueSeparator;
//替換屬性佔位符
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
    Assert.notNull(value, "'value' must not be null");
    return parseStringValue(value, placeholderResolver, new HashSet<>());
}

//遞迴解析帶佔位符的屬性為字串
protected String parseStringValue(
        String value, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
    StringBuilder result = new StringBuilder(value);
    int startIndex = value.indexOf(this.placeholderPrefix);
    while (startIndex != -1) {
        //搜尋第一個佔位符字尾的索引
        int endIndex = findPlaceholderEndIndex(result, startIndex);
        if (endIndex != -1) {
            //提取第一個佔位符中的原始字串,如${server.port}->server.port
            String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
            String originalPlaceholder = placeholder;
            //判重
            if (!visitedPlaceholders.add(originalPlaceholder)) {
                throw new IllegalArgumentException(
                        "Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
            }
            // Recursive invocation, parsing placeholders contained in the placeholder key.
            // 遞迴呼叫,實際上就是解析巢狀的佔位符,因為提取的原始字串有可能還有一層或者多層佔位符
            placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
            // Now obtain the value for the fully resolved key...
            // 遞迴呼叫完畢後,可以確定得到的字串一定是不帶佔位符,這個時候呼叫getPropertyAsRawString獲取key對應的字串值
            String propVal = placeholderResolver.resolvePlaceholder(placeholder);
            // 如果字串值為null,則進行預設值的解析,因為預設值有可能也使用了佔位符,如${server.port:${server.port-2:8080}}
            if (propVal == null && this.valueSeparator != null) {
                int separatorIndex = placeholder.indexOf(this.valueSeparator);
                if (separatorIndex != -1) {
                    String actualPlaceholder = placeholder.substring(0, separatorIndex);
                    // 提取預設值的字串
                    String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
                    // 這裡是把預設值的表示式做一次解析,解析到null,則直接賦值為defaultValue
                    propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
                    if (propVal == null) {
                        propVal = defaultValue;
                    }
                }
            }
            // 上一步解析出來的值不為null,但是它有可能是一個帶佔位符的值,所以後面對值進行遞迴解析
            if (propVal != null) {
                // Recursive invocation, parsing placeholders contained in the
                // previously resolved placeholder value.
                propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
                // 這一步很重要,替換掉第一個被解析完畢的佔位符屬性,例如${server.port}-${spring.application.name} -> 9090--${spring.application.name}
                result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
                if (logger.isTraceEnabled()) {
                    logger.trace("Resolved placeholder '" + placeholder + "'");
                }
                // 重置startIndex為下一個需要解析的佔位符字首的索引,可能為-1,說明解析結束
                startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
            }
            else if (this.ignoreUnresolvablePlaceholders) {
                // 如果propVal為null並且ignoreUnresolvablePlaceholders設定為true,直接返回當前的佔位符之間的原始字串尾的索引,也就是跳過解析
                // Proceed with unprocessed value.
                startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
            }
            else {
                // 如果propVal為null並且ignoreUnresolvablePlaceholders設定為false,丟擲異常
                throw new IllegalArgumentException("Could not resolve placeholder '" +
                            placeholder + "'" + " in value \"" + value + "\"");
            }
            // 遞迴結束移除判重集合中的元素
            visitedPlaceholders.remove(originalPlaceholder);
        }
        else {
            // endIndex = -1說明解析結束
            startIndex = -1;
        }
    }
    return result.toString();
}

//基於傳入的起始索引,搜尋第一個佔位符字尾的索引,相容巢狀的佔位符
private int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
    //這裡index實際上就是實際需要解析的屬性的第一個字元,如${server.port},這裡index指向s
    int index = startIndex + this.placeholderPrefix.length();
    int withinNestedPlaceholder = 0;
    while (index < buf.length()) {
        //index指向"}",說明有可能到達佔位符尾部或者巢狀佔位符尾部
        if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) {
            //存在巢狀佔位符,則返回字串中佔位符字尾的索引值
            if (withinNestedPlaceholder > 0) {
                withinNestedPlaceholder--;
                index = index + this.placeholderSuffix.length();
            }
            else {
                //不存在巢狀佔位符,直接返回佔位符尾部索引
                return index;
            }
        }
        //index指向"{",記錄巢狀佔位符個數withinNestedPlaceholder加1,index更新為巢狀屬性的第一個字元的索引
        else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) {
            withinNestedPlaceholder++;
            index = index + this.simplePrefix.length();
        }
        else {
            //index不是"{"或者"}",則進行自增
            index++;
        }
    }
    //這裡說明解析索引已經超出了原字串
    return -1;
}

//StringUtils#substringMatch,此方法會檢查原始字串str的index位置開始是否和子字串substring完全匹配
public static boolean substringMatch(CharSequence str, int index, CharSequence substring) {
    if (index + substring.length() > str.length()) {
        return false;
    }
    for (int i = 0; i < substring.length(); i++) {
        if (str.charAt(index + i) != substring.charAt(i)) {
            return false;
        }
    }
    return true;
}

上面的過程相對比較複雜,因為用到了遞迴,我們舉個實際的例子說明一下整個解析過程,例如我們使用了四個屬性項,我們的目標是獲取server.desc的值:

application.name=spring
server.port=9090
spring.application.name=${application.name}
server.desc=${server.port-${spring.application.name}}:${description:"hello"}

spec-1

屬性型別轉換

在上一步解析屬性佔位符完畢之後,得到的是屬性字串值,可以把字串轉換為指定的型別,此功能由AbstractPropertyResolver#convertValueIfNecessary完成:

protected <T> T convertValueIfNecessary(Object value, @Nullable Class<T> targetType) {
    if (targetType == null) {
        return (T) value;
    }
    ConversionService conversionServiceToUse = this.conversionService;
    if (conversionServiceToUse == null) {
        // Avoid initialization of shared DefaultConversionService if
        // no standard type conversion is needed in the first place...
        // 這裡一般只有字串型別才會命中
        if (ClassUtils.isAssignableValue(targetType, value)) {
            return (T) value;
        }
        conversionServiceToUse = DefaultConversionService.getSharedInstance();
    }
    return conversionServiceToUse.convert(value, targetType);
}

實際上轉換的邏輯是委託到DefaultConversionService的父類方法GenericConversionService#convert

public <T> T convert(@Nullable Object source, Class<T> targetType) {
    Assert.notNull(targetType, "Target type to convert to cannot be null");
    return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType));
}

public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
    Assert.notNull(targetType, "Target type to convert to cannot be null");
    if (sourceType == null) {
        Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
        return handleResult(null, targetType, convertNullSource(null, targetType));
    }
    if (source != null && !sourceType.getObjectType().isInstance(source)) {
        throw new IllegalArgumentException("Source to convert from must be an instance of [" +
                    sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
    }
    // 從快取中獲取GenericConverter例項,其實這一步相對複雜,匹配兩個型別的時候,會解析整個類的層次進行對比
    GenericConverter converter = getConverter(sourceType, targetType);
    if (converter != null) {
        // 實際上就是呼叫轉換方法
        Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
        // 斷言最終結果和指定型別是否匹配並且返回
        return handleResult(sourceType, targetType, result);
    }
    return handleConverterNotFound(source, sourceType, targetType);
}

上面所有的可用的GenericConverter的例項可以在DefaultConversionService的addDefaultConverters中看到,預設新增的轉換器例項已經超過20個,有些情況下如果無法滿足需求可以新增自定義的轉換器,實現GenericConverter介面新增進去即可。

小結

SpringBoot在抽象整個型別轉換器方面做的比較好,在SpringMVC應用中,採用的是org.springframework.boot.autoconfigure.web.format.WebConversionService,相容了Converter、Formatter、ConversionService等轉換器型別並且對外提供一套統一的轉換方法。

(本文完)