前提

前面写过一篇关于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等转换器类型并且对外提供一套统一的转换方法。

(本文完)

SpringBoot环境属性占位符解析和类型转换的更多相关文章

  1. spring源码解析(一)---占位符解析替换

    一.结构类图 ①.PropertyResolver : Environment的顶层接口,主要提供属性检索和解析带占位符的文本.bean.xml配置中的所有占位符例如${}都由它解析 ②.Config ...

  2. Spring PropertyResolver 占位符解析(一)API 介绍

    Spring PropertyResolver 占位符解析(一)API 介绍 Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html ...

  3. Spring PropertyResolver 占位符解析(二)源码分析

    Spring PropertyResolver 占位符解析(二)源码分析 Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) ...

  4. Spring实战(八)bean装配的运行时值注入——属性占位符和SpEL

    前面涉及到依赖注入,我们一般哦都是将一个bean引用注入到另一个bean 的属性or构造器参数or Setter参数,即将为一个对象与另一个对象进行关联. bean装配的另一个方面是指将一个值注入到b ...

  5. 【mybatis源码学习】mybtias基础组件-占位符解析器

    一.占位符解析器源码 1.占位符解析器实现的目标 通过解析字符串中指定前后缀中的字符,并完成相应的功能. 在mybtias中的应用,主要是为了解析Mapper的xml中的sql语句#{}中的内容,识别 ...

  6. Spring(3.2.3) - Beans(12): 属性占位符

    使用属性占位符可以将 Spring 配置文件中的部分元数据放在属性文件中设置,这样可以将相似的配置(如 JDBC 的参数配置)放在特定的属性文件中,如果只需要修改这部分配置,则无需修改 Spring ...

  7. spring占位符解析器---PropertyPlaceholderHelper

    一.PropertyPlaceholderHelper 职责 扮演者占位符解析器的角色,专门用来负责解析路劲中or名字中的占位符的字符,并替换上具体的值 二.例子 public class Prope ...

  8. 8 -- 深入使用Spring -- 1...4 属性占位符配置器

    8.1.4 属性占位符配置器 PropertyPlaceholderConfigurer 是一个容器后处理器,负责读取Properties属性文件里的属性值,并将这些属性值设置成Spring配置文件的 ...

  9. Spring - IoC(12): 属性占位符

    使用属性占位符可以将 Spring 配置文件中的部分元数据放在属性文件中设置,这样可以将相似的配置(如 JDBC 的参数配置)放在特定的属性文件中,如果只需要修改这部分配置,则无需修改 Spring ...

随机推荐

  1. POJ 1860 Currency Exchange【SPFA判环】

    Several currency exchange points are working in our city. Let us suppose that each point specializes ...

  2. python 简单日志框架 自定义logger

    转载请注明: 仰望高端玩家的小清新 http://www.cnblogs.com/luruiyuan/ 通常我们在构建 python 系统时,往往需要一个简单的 logging 框架.python 自 ...

  3. 最正确的React事件绑定方式

    参考这篇文章:Choosing the Best Approach for React Event Handlers 1.function.bind()方式 2.inline arrow functi ...

  4. Python安装scrapy提示 error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++

    error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools&quo ...

  5. 【二分】【线段树】hdu6070 Dirt Ratio

    size(l,r)表示区间l,r权值的种类数,让你求min{size(l,r)/(r-l+1)}(1<=l<=r<=n). last[r]表示a[r]上一次出现的位置, 就是二分验证 ...

  6. 【后缀自动机】poj1509 Glass Beads

    字符串最小表示 后缀自动机 O(n) 把串复制一次,链接在后面之后,建立SAM,贪心地在SAM上转移,每次贪心地选择最小的字符,转移的长度为n时停止. 输出时由于要最靠前的,所以要在endpos集合中 ...

  7. bzoj 4602: [Sdoi2016]齿轮

    4602: [Sdoi2016]齿轮 Description 现有一个传动系统,包含了N个组合齿轮和M个链条.每一个链条连接了两个组合齿轮u和v,并提供了一个传动比x  : y.即如果只考虑这两个组合 ...

  8. JavaScript的深拷贝与浅拷贝

    深拷贝和浅拷贝是在面试中经常遇到的问题.今天在这里总结一下. 深拷贝与浅拷贝的问题,涉及到JavaScript的变量类型,先来说说变量的类型,变量类型包括基本类型和引用类型. 基本类型:Undefin ...

  9. JavaScript之引用类型(Object类型)

    ECMAScript提供了很多原生的引用类型,以便开发人员进行常见的计算任务. 对象是某一个特定引用类型的的实例. Object类型 用的最多.虽然这个Object实例不具备多少功能,但是在应用程序的 ...

  10. Codeforces Beta Round #2 B. The least round way dp

    B. The least round way 题目连接: http://www.codeforces.com/contest/2/problem/B Description There is a sq ...