Spring Boot环境属性占位符解析及类型转换详解
前提
前面写过一篇关于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"}
属性类型转换
在上一步解析属性占位符完毕之后,得到的是属性字符串值,可以把字符串转换为指定的类型,此功能由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等转换器类型并且对外提供一套统一的转换方法。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。