服务器之家

服务器之家 > 正文

Spring Boot环境属性占位符解析及类型转换详解

时间:2021-05-24 13:25     来源/作者:throwable

前提

前面写过一篇关于environment属性加载的源码分析和扩展,里面提到属性的占位符解析和类型转换是相对复杂的,这篇文章就是要分析和解读这两个复杂的问题。关于这两个问题,选用一个比较复杂的参数处理方法propertysourcespropertyresolver#getproperty,解析占位符的时候依赖到

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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:

?
1
2
3
4
protected string resolvenestedplaceholders(string value) {
 return (this.ignoreunresolvablenestedplaceholders ?
  resolveplaceholders(value) : resolverequiredplaceholders(value));
}

ignoreunresolvablenestedplaceholders属性默认为false,可以通过abstractenvironment#setignoreunresolvablenestedplaceholders(boolean ignoreunresolvablenestedplaceholders)设置,当此属性被设置为true,解析属性占位符失败的时候(并且没有为占位符配置默认值)不会抛出异常,返回属性原样字符串,否则会抛出illegalargumentexception。我们这里只需要分析abstractpropertyresolver#resolverequiredplaceholders:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//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相当于下面的代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//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的源码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//基础属性
//占位符前缀,默认是"${"
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的值:

?
1
2
3
4
application.name=spring
server.port=9090
spring.application.name=${application.name}
server.desc=${server.port-${spring.application.name}}:${description:"hello"}

Spring Boot环境属性占位符解析及类型转换详解

属性类型转换

在上一步解析属性占位符完毕之后,得到的是属性字符串值,可以把字符串转换为指定的类型,此功能由abstractpropertyresolver#convertvalueifnecessary完成:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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等转换器类型并且对外提供一套统一的转换方法。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。

原文链接:https://www.cnblogs.com/throwable/p/9417827.html

相关文章

热门资讯

2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全
2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全 2019-12-26
yue是什么意思 网络流行语yue了是什么梗
yue是什么意思 网络流行语yue了是什么梗 2020-10-11
背刺什么意思 网络词语背刺是什么梗
背刺什么意思 网络词语背刺是什么梗 2020-05-22
苹果12mini价格表官网报价 iPhone12mini全版本价格汇总
苹果12mini价格表官网报价 iPhone12mini全版本价格汇总 2020-11-13
2021德云社封箱演出完整版 2021年德云社封箱演出在线看
2021德云社封箱演出完整版 2021年德云社封箱演出在线看 2021-03-15
返回顶部