SpringBoot环境属性占位符解析和类型转换

转自:https://www.cnblogs.com/throwable/p/9417827.html

前提

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

 1 protected String getPropertyAsRawString(String key) {
 2     return getProperty(key, String.class, false);
 3 }
 4 
 5 protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
 6     if (this.propertySources != null) {
 7         for (PropertySource<?> propertySource : this.propertySources) {
 8             if (logger.isTraceEnabled()) {
 9                 logger.trace("Searching for key '" + key + "' in PropertySource '" +
10                             propertySource.getName() + "'");
11             }
12             Object value = propertySource.getProperty(key);
13             if (value != null) {
14                 if (resolveNestedPlaceholders && value instanceof String) {
15                     //解析带有占位符的属性
16                     value = resolveNestedPlaceholders((String) value);
17                 }
18                 logKeyFound(key, propertySource, value);
19                 //需要时转换属性的类型
20                 return convertValueIfNecessary(value, targetValueType);
21             }
22         }
23     }
24     if (logger.isDebugEnabled()) {
25         logger.debug("Could not find key '" + key + "' in any property source");
26     }
27     return null;
28 }

属性占位符解析

属性占位符的解析方法是PropertySourcesPropertyResolver的父类AbstractPropertyResolver#resolveNestedPlaceholders

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

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

 1 //AbstractPropertyResolver中的属性:
 2 //ignoreUnresolvableNestedPlaceholders=true情况下创建的PropertyPlaceholderHelper实例
 3 @Nullable
 4 private PropertyPlaceholderHelper nonStrictHelper;
 5 
 6 //ignoreUnresolvableNestedPlaceholders=false情况下创建的PropertyPlaceholderHelper实例
 7 @Nullable
 8 private PropertyPlaceholderHelper strictHelper;
 9 
10 //是否忽略无法处理的属性占位符,这里是false,也就是遇到无法处理的属性占位符且没有默认值则抛出异常
11 private boolean ignoreUnresolvableNestedPlaceholders = false;
12 
13 //属性占位符前缀,这里是"${"
14 private String placeholderPrefix = SystemPropertyUtils.PLACEHOLDER_PREFIX;
15 
16 //属性占位符后缀,这里是"}"
17 private String placeholderSuffix = SystemPropertyUtils.PLACEHOLDER_SUFFIX;
18 
19 //属性占位符解析失败的时候配置默认值的分隔符,这里是":"
20 @Nullable
21 private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR;
22 
23 
24 public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
25     if (this.strictHelper == null) {
26         this.strictHelper = createPlaceholderHelper(false);
27     }
28     return doResolvePlaceholders(text, this.strictHelper);
29 }
30 
31 //创建一个新的PropertyPlaceholderHelper实例,这里ignoreUnresolvablePlaceholders为false
32 private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) {
33     return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix, this.valueSeparator, ignoreUnresolvablePlaceholders);
34 }
35 
36 //这里最终的解析工作委托到PropertyPlaceholderHelper#replacePlaceholders完成
37 private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
38     return helper.replacePlaceholders(text, this::getPropertyAsRawString);
39 }
最终只需要分析PropertyPlaceholderHelper#replacePlaceholders,这里需要重点注意:
  • 注意到这里的第一个参数text就是属性值的源字符串,例如我们需要处理的属性为myProperties: ${server.port}-${spring.application.name},这里的text就是${server.port}-${spring.application.name}。
  • replacePlaceholders方法的第二个参数placeholderResolver,这里比较巧妙,这里的方法引用this::getPropertyAsRawString相当于下面的代码:
 1 //PlaceholderResolver是一个函数式接口
 2 @FunctionalInterface
 3 public interface PlaceholderResolver {
 4   @Nullable
 5   String resolvePlaceholder(String placeholderName);  
 6 }
 7 //this::getPropertyAsRawString相当于下面的代码
 8 return new PlaceholderResolver(){
 9     
10     @Override
11     String resolvePlaceholder(String placeholderName){
12         //这里调用到的是PropertySourcesPropertyResolver#getPropertyAsRawString,有点绕
13         return getPropertyAsRawString(placeholderName);
14     }
15 }     

接着看PropertyPlaceholderHelper#replacePlaceholders的源码:

  1 //基础属性
  2 //占位符前缀,默认是"${"
  3 private final String placeholderPrefix;
  4 //占位符后缀,默认是"}"
  5 private final String placeholderSuffix;
  6 //简单的占位符前缀,默认是"{",主要用于处理嵌套的占位符如${xxxxx.{yyyyy}}
  7 private final String simplePrefix;
  8 
  9 //默认值分隔符号,默认是":"
 10 @Nullable
 11 private final String valueSeparator;
 12 //替换属性占位符
 13 public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
 14     Assert.notNull(value, "'value' must not be null");
 15     return parseStringValue(value, placeholderResolver, new HashSet<>());
 16 }
 17 
 18 //递归解析带占位符的属性为字符串
 19 protected String parseStringValue(
 20         String value, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
 21     StringBuilder result = new StringBuilder(value);
 22     int startIndex = value.indexOf(this.placeholderPrefix);
 23     while (startIndex != -1) {
 24         //搜索第一个占位符后缀的索引
 25         int endIndex = findPlaceholderEndIndex(result, startIndex);
 26         if (endIndex != -1) {
 27             //提取第一个占位符中的原始字符串,如${server.port}->server.port
 28             String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
 29             String originalPlaceholder = placeholder;
 30             //判重
 31             if (!visitedPlaceholders.add(originalPlaceholder)) {
 32                 throw new IllegalArgumentException(
 33                         "Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
 34             }
 35             // Recursive invocation, parsing placeholders contained in the placeholder key.
 36             // 递归调用,实际上就是解析嵌套的占位符,因为提取的原始字符串有可能还有一层或者多层占位符
 37             placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
 38             // Now obtain the value for the fully resolved key...
 39             // 递归调用完毕后,可以确定得到的字符串一定是不带占位符,这个时候调用getPropertyAsRawString获取key对应的字符串值
 40             String propVal = placeholderResolver.resolvePlaceholder(placeholder);
 41             // 如果字符串值为null,则进行默认值的解析,因为默认值有可能也使用了占位符,如${server.port:${server.port-2:8080}}
 42             if (propVal == null && this.valueSeparator != null) {
 43                 int separatorIndex = placeholder.indexOf(this.valueSeparator);
 44                 if (separatorIndex != -1) {
 45                     String actualPlaceholder = placeholder.substring(0, separatorIndex);
 46                     // 提取默认值的字符串
 47                     String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
 48                     // 这里是把默认值的表达式做一次解析,解析到null,则直接赋值为defaultValue
 49                     propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
 50                     if (propVal == null) {
 51                         propVal = defaultValue;
 52                     }
 53                 }
 54             }
 55             // 上一步解析出来的值不为null,但是它有可能是一个带占位符的值,所以后面对值进行递归解析
 56             if (propVal != null) {
 57                 // Recursive invocation, parsing placeholders contained in the
 58                 // previously resolved placeholder value.
 59                 propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
 60                 // 这一步很重要,替换掉第一个被解析完毕的占位符属性,例如${server.port}-${spring.application.name} -> 9090--${spring.application.name}
 61                 result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
 62                 if (logger.isTraceEnabled()) {
 63                     logger.trace("Resolved placeholder '" + placeholder + "'");
 64                 }
 65                 // 重置startIndex为下一个需要解析的占位符前缀的索引,可能为-1,说明解析结束
 66                 startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
 67             }
 68             else if (this.ignoreUnresolvablePlaceholders) {
 69                 // 如果propVal为null并且ignoreUnresolvablePlaceholders设置为true,直接返回当前的占位符之间的原始字符串尾的索引,也就是跳过解析
 70                 // Proceed with unprocessed value.
 71                 startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
 72             }
 73             else {
 74                 // 如果propVal为null并且ignoreUnresolvablePlaceholders设置为false,抛出异常
 75                 throw new IllegalArgumentException("Could not resolve placeholder '" +
 76                             placeholder + "'" + " in value \"" + value + "\"");
 77             }
 78             // 递归结束移除判重集合中的元素
 79             visitedPlaceholders.remove(originalPlaceholder);
 80         }
 81         else {
 82             // endIndex = -1说明解析结束
 83             startIndex = -1;
 84         }
 85     }
 86     return result.toString();
 87 }
 88 
 89 //基于传入的起始索引,搜索第一个占位符后缀的索引,兼容嵌套的占位符
 90 private int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
 91     //这里index实际上就是实际需要解析的属性的第一个字符,如${server.port},这里index指向s
 92     int index = startIndex + this.placeholderPrefix.length();
 93     int withinNestedPlaceholder = 0;
 94     while (index < buf.length()) {
 95         //index指向"}",说明有可能到达占位符尾部或者嵌套占位符尾部
 96         if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) {
 97             //存在嵌套占位符,则返回字符串中占位符后缀的索引值
 98             if (withinNestedPlaceholder > 0) {
 99                 withinNestedPlaceholder--;
100                 index = index + this.placeholderSuffix.length();
101             }
102             else {
103                 //不存在嵌套占位符,直接返回占位符尾部索引
104                 return index;
105             }
106         }
107         //index指向"{",记录嵌套占位符个数withinNestedPlaceholder加1,index更新为嵌套属性的第一个字符的索引
108         else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) {
109             withinNestedPlaceholder++;
110             index = index + this.simplePrefix.length();
111         }
112         else {
113             //index不是"{"或者"}",则进行自增
114             index++;
115         }
116     }
117     //这里说明解析索引已经超出了原字符串
118     return -1;
119 }
120 
121 //StringUtils#substringMatch,此方法会检查原始字符串str的index位置开始是否和子字符串substring完全匹配
122 public static boolean substringMatch(CharSequence str, int index, CharSequence substring) {
123     if (index + substring.length() > str.length()) {
124         return false;
125     }
126     for (int i = 0; i < substring.length(); i++) {
127         if (str.charAt(index + i) != substring.charAt(i)) {
128             return false;
129         }
130     }
131     return true;
132 }

上面的过程相对比较复杂,因为用到了递归,我们举个实际的例子说明一下整个解析过程,例如我们使用了四个属性项,我们的目标是获取server.desc的值:

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

属性类型转换

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

 1 protected <T> T convertValueIfNecessary(Object value, @Nullable Class<T> targetType) {
 2     if (targetType == null) {
 3         return (T) value;
 4     }
 5     ConversionService conversionServiceToUse = this.conversionService;
 6     if (conversionServiceToUse == null) {
 7         // Avoid initialization of shared DefaultConversionService if
 8         // no standard type conversion is needed in the first place...
 9         // 这里一般只有字符串类型才会命中
10         if (ClassUtils.isAssignableValue(targetType, value)) {
11             return (T) value;
12         }
13         conversionServiceToUse = DefaultConversionService.getSharedInstance();
14     }
15     return conversionServiceToUse.convert(value, targetType);
16 }

实际上转换的逻辑是委托到DefaultConversionService的父类方法GenericConversionService#convert

 1 public <T> T convert(@Nullable Object source, Class<T> targetType) {
 2     Assert.notNull(targetType, "Target type to convert to cannot be null");
 3     return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType));
 4 }
 5 
 6 public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
 7     Assert.notNull(targetType, "Target type to convert to cannot be null");
 8     if (sourceType == null) {
 9         Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
10         return handleResult(null, targetType, convertNullSource(null, targetType));
11     }
12     if (source != null && !sourceType.getObjectType().isInstance(source)) {
13         throw new IllegalArgumentException("Source to convert from must be an instance of [" +
14                     sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
15     }
16     // 从缓存中获取GenericConverter实例,其实这一步相对复杂,匹配两个类型的时候,会解析整个类的层次进行对比
17     GenericConverter converter = getConverter(sourceType, targetType);
18     if (converter != null) {
19         // 实际上就是调用转换方法
20         Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
21         // 断言最终结果和指定类型是否匹配并且返回
22         return handleResult(sourceType, targetType, result);
23     }
24     return handleConverterNotFound(source, sourceType, targetType);
25 }

上面所有的可用的GenericConverter的实例可以在DefaultConversionService的addDefaultConverters中看到,默认添加的转换器实例已经超过20个,有些情况下如果无法满足需求可以添加自定义的转换器,实现GenericConverter接口添加进去即可。

小结

SpringBoot在抽象整个类型转换器方面做的比较好,在SpringMVC应用中,采用的是org.springframework.boot.autoconfigure.web.format.WebConversionService,兼容了Converter、Formatter、ConversionService等转换器类型并且对外提供一套统一的转换方法。

 

posted @ 2022-10-17 20:47  Boblim  阅读(518)  评论(0编辑  收藏  举报