前提

前面写过一篇关于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. Java GlassPane进度条遮罩

    package com.swing.demo; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Flo ...

  2. 洛谷——P2384 最短路

    P2384 最短路 题目背景 狗哥做烂了最短路,突然机智的考了Bosh一道,没想到把Bosh考住了...你能帮Bosh解决吗? 他会给你10000000000000000000000000000000 ...

  3. Codeforces Round #278 (Div. 1) Strip (线段树 二分 RMQ DP)

    Strip time limit per test 1 second memory limit per test 256 megabytes input standard input output s ...

  4. If you want to allow applications containing errors to be published on the server

    If you want to allow applications containing errors to be published on the server, enable the Allow ...

  5. Codeforces Round #404 (Div. 2) C 二分查找

    Codeforces Round #404 (Div. 2) 题意:对于 n and m (1 ≤ n, m ≤ 10^18)  找到 1) [n<= m] cout<<n; 2) ...

  6. Mac下配置PHP支持GD库FreeType

    一句话脚本 curl -s http://php-osx.liip.ch/install.sh | bash -s 5.6 记得要FQ哦. 或者下面代码保存成.sh ,代码从http://php-os ...

  7. 【扫描线】Gym - 101190E - Expect to Wait

    假设初始人数为0, 将每个时刻在等待的人数写下来,就是求个和. 如果纵坐标看成人数,横坐标看成时间,就是求个面积. 因为初始人数不一定为零,所以离线后扫描线即可回答所有询问. #include< ...

  8. 事件BOM DOM

    1.事件 1.1 事件绑定 # 1.写在html元素中 <button onclick='code'></button> # 2.把事件当作元素对象的方法 btnEle.onc ...

  9. [转] Log4j 配置 的webAppRootKey参数问题

    在tomcat下部署两个或多个项目时,web.xml文件中最好定义webAppRootKey参数,如果不定义,将会缺省为“webapp.root”,如下:<!-- 应用路径 --> < ...

  10. 牛x的面试知识点

    已经凌晨2点多了,看来今天是失眠了,反正睡不着,写篇日记总结一下我的第一次社招面试经历吧.2015年12月1日办理了离职手续,离开了万通中心,也算是和我的第一个东家正式说了再见,其实还是很喜欢国贸的, ...