SpringBoot環(huán)境屬性占位符解析和類型轉(zhuǎn)換方式
前提
前面寫過一篇關(guān)于Environment屬性加載的源碼分析和擴展,里面提到屬性的占位符解析和類型轉(zhuǎn)換是相對復(fù)雜的,這篇文章就是要分析和解讀這兩個復(fù)雜的問題。
關(guān)于這兩個問題,選用一個比較復(fù)雜的參數(shù)處理方法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);
//需要時轉(zhuǎn)換屬性的類型
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屬性默認(rèn)為false,可以通過AbstractEnvironment#setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders)設(shè)置,當(dāng)此屬性被設(shè)置為true,解析屬性占位符失敗的時候(并且沒有為占位符配置默認(rèn)值)不會拋出異常,返回屬性原樣字符串,否則會拋出IllegalArgumentException。
我們這里只需要分析AbstractPropertyResolver#resolveRequiredPlaceholders:
//AbstractPropertyResolver中的屬性:
//ignoreUnresolvableNestedPlaceholders=true情況下創(chuàng)建的PropertyPlaceholderHelper實例
@Nullable
private PropertyPlaceholderHelper nonStrictHelper;
//ignoreUnresolvableNestedPlaceholders=false情況下創(chuàng)建的PropertyPlaceholderHelper實例
@Nullable
private PropertyPlaceholderHelper strictHelper;
//是否忽略無法處理的屬性占位符,這里是false,也就是遇到無法處理的屬性占位符且沒有默認(rèn)值則拋出異常
private boolean ignoreUnresolvableNestedPlaceholders = false;
//屬性占位符前綴,這里是"${"
private String placeholderPrefix = SystemPropertyUtils.PLACEHOLDER_PREFIX;
//屬性占位符后綴,這里是"}"
private String placeholderSuffix = SystemPropertyUtils.PLACEHOLDER_SUFFIX;
//屬性占位符解析失敗的時候配置默認(rèn)值的分隔符,這里是":"
@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);
}
//創(chuàng)建一個新的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,這里需要重點注意:
- 注意到這里的第一個參數(shù)text就是屬性值的源字符串,例如我們需要處理的屬性為myProperties: server.port−server.port− {spring.application.name},這里的text就是 server\.port−server\.port
− {spring.application.name}。
- replacePlaceholders方法的第二個參數(shù)placeholderResolver,這里比較巧妙,這里的方法引用this::getPropertyAsRawString相當(dāng)于下面的代碼:
//PlaceholderResolver是一個函數(shù)式接口
@FunctionalInterface
public interface PlaceholderResolver {
@Nullable
String resolvePlaceholder(String placeholderName);
}
//this::getPropertyAsRawString相當(dāng)于下面的代碼
return new PlaceholderResolver(){
@Override
String resolvePlaceholder(String placeholderName){
//這里調(diào)用到的是PropertySourcesPropertyResolver#getPropertyAsRawString,有點繞
return getPropertyAsRawString(placeholderName);
}
} 接著看PropertyPlaceholderHelper#replacePlaceholders的源碼:
//基礎(chǔ)屬性
//占位符前綴,默認(rèn)是"${"
private final String placeholderPrefix;
//占位符后綴,默認(rèn)是"}"
private final String placeholderSuffix;
//簡單的占位符前綴,默認(rèn)是"{",主要用于處理嵌套的占位符如${xxxxx.{yyyyy}}
private final String simplePrefix;
//默認(rèn)值分隔符號,默認(rèn)是":"
@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.
// 遞歸調(diào)用,實際上就是解析嵌套的占位符,因為提取的原始字符串有可能還有一層或者多層占位符
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
// Now obtain the value for the fully resolved key...
// 遞歸調(diào)用完畢后,可以確定得到的字符串一定是不帶占位符,這個時候調(diào)用getPropertyAsRawString獲取key對應(yīng)的字符串值
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
// 如果字符串值為null,則進(jìn)行默認(rèn)值的解析,因為默認(rèn)值有可能也使用了占位符,如${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);
// 提取默認(rèn)值的字符串
String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
// 這里是把默認(rèn)值的表達(dá)式做一次解析,解析到null,則直接賦值為defaultValue
propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
if (propVal == null) {
propVal = defaultValue;
}
}
}
// 上一步解析出來的值不為null,但是它有可能是一個帶占位符的值,所以后面對值進(jìn)行遞歸解析
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,說明解析結(jié)束
startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
}
else if (this.ignoreUnresolvablePlaceholders) {
// 如果propVal為null并且ignoreUnresolvablePlaceholders設(shè)置為true,直接返回當(dāng)前的占位符之間的原始字符串尾的索引,也就是跳過解析
// Proceed with unprocessed value.
startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
}
else {
// 如果propVal為null并且ignoreUnresolvablePlaceholders設(shè)置為false,拋出異常
throw new IllegalArgumentException("Could not resolve placeholder '" +
placeholder + "'" + " in value \"" + value + "\"");
}
// 遞歸結(jié)束移除判重集合中的元素
visitedPlaceholders.remove(originalPlaceholder);
}
else {
// endIndex = -1說明解析結(jié)束
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指向"}",說明有可能到達(dá)占位符尾部或者嵌套占位符尾部
if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) {
//存在嵌套占位符,則返回字符串中占位符后綴的索引值
if (withinNestedPlaceholder > 0) {
withinNestedPlaceholder--;
index = index + this.placeholderSuffix.length();
}
else {
//不存在嵌套占位符,直接返回占位符尾部索引
return index;
}
}
//index指向"{",記錄嵌套占位符個數(shù)withinNestedPlaceholder加1,index更新為嵌套屬性的第一個字符的索引
else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) {
withinNestedPlaceholder++;
index = index + this.simplePrefix.length();
}
else {
//index不是"{"或者"}",則進(jìn)行自增
index++;
}
}
//這里說明解析索引已經(jīng)超出了原字符串
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;
}上面的過程相對比較復(fù)雜,因為用到了遞歸,我們舉個實際的例子說明一下整個解析過程,例如我們使用了四個屬性項,我們的目標(biāo)是獲取server.desc的值:
application.name=spring
server.port=9090
spring.application.name=${application.name}
server.desc=${server.port-${spring.application.name}}:${description:"hello"}屬性類型轉(zhuǎn)換
在上一步解析屬性占位符完畢之后,得到的是屬性字符串值,可以把字符串轉(zhuǎn)換為指定的類型,此功能由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);
}實際上轉(zhuǎn)換的邏輯是委托到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實例,其實這一步相對復(fù)雜,匹配兩個類型的時候,會解析整個類的層次進(jìn)行對比
GenericConverter converter = getConverter(sourceType, targetType);
if (converter != null) {
// 實際上就是調(diào)用轉(zhuǎn)換方法
Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
// 斷言最終結(jié)果和指定類型是否匹配并且返回
return handleResult(sourceType, targetType, result);
}
return handleConverterNotFound(source, sourceType, targetType);
}上面所有的可用的GenericConverter的實例可以在DefaultConversionService的addDefaultConverters中看到,默認(rèn)添加的轉(zhuǎn)換器實例已經(jīng)超過20個,有些情況下如果無法滿足需求可以添加自定義的轉(zhuǎn)換器,實現(xiàn)GenericConverter接口添加進(jìn)去即可。
總結(jié)
SpringBoot在抽象整個類型轉(zhuǎn)換器方面做的比較好,在SpringMVC應(yīng)用中,采用的是org.springframework.boot.autoconfigure.web.format.WebConversionService,兼容了Converter、Formatter、ConversionService等轉(zhuǎn)換器類型并且對外提供一套統(tǒng)一的轉(zhuǎn)換方法。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot+MyBatis+Redis實現(xiàn)分布式緩存
本文主要介紹了SpringBoot+MyBatis+Redis實現(xiàn)分布式緩存,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-01-01
詳解SpringBoot中RestTemplate的幾種實現(xiàn)
這篇文章主要介紹了詳解SpringBoot中RestTemplate的幾種實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
springboot?security使用jwt認(rèn)證方式
這篇文章主要介紹了springboot?security使用jwt認(rèn)證方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-04-04
java自定義注解實現(xiàn)前后臺參數(shù)校驗的實例
下面小編就為大家?guī)硪黄猨ava自定義注解實現(xiàn)前后臺參數(shù)校驗的實例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-11-11
Java中的關(guān)鍵字synchronized 詳解
這篇文章主要介紹了Java中的關(guān)鍵字synchronized,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03
SpringBoot整合dataworks的實現(xiàn)過程
這篇文章主要介紹了SpringBoot整合dataworks的實現(xiàn)過程,實現(xiàn)主要是編寫工具類,如果需要則可以配置成SpringBean,注入容器即可使用,需要的朋友可以參考下2022-08-08

