Spring属性占位符解析器 PropertyPlaceholderHelper源码阅读
PropertyPlaceholderHelper
用于处理字符串中"${}"
这种占位符,比如通过@Value(“${}”)
注解获取对应属性文件中定义的属性值等(但不能处理@Value(“#{}”)
, 表示通过SpEL
表达式通常用来获取bean的属性)。
该类是一个单纯的工具类,没有继承没有实现,而且简单无依赖,没有依赖Spring框架其他的任何类。
一、实践
先看下该类如何使用。
构造函数
该类主要构造函数如下:
private static final Map<String, String> wellKnownSimplePrefixes = new HashMap<>(4);
static {
wellKnownSimplePrefixes.put("}", "{");
wellKnownSimplePrefixes.put("]", "[");
wellKnownSimplePrefixes.put(")", "(");
}
public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix,
@Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders) {
Assert.notNull(placeholderPrefix, "'placeholderPrefix' must not be null");
Assert.notNull(placeholderSuffix, "'placeholderSuffix' must not be null");
this.placeholderPrefix = placeholderPrefix;
this.placeholderSuffix = placeholderSuffix;
String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.placeholderSuffix);
if (simplePrefixForSuffix != null && this.placeholderPrefix.endsWith(simplePrefixForSuffix)) {
this.simplePrefix = simplePrefixForSuffix;
}
else {
this.simplePrefix = this.placeholderPrefix;
}
this.valueSeparator = valueSeparator;
this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
}
构造方法包含四个参数:
-
placeholderPrefix
,占位符前缀; -
placeholderSuffix
,占位符后缀; -
valueSeparator
,默认值分隔符,如果解析失败时取默认值。例如,如果该参数为:
时,对于待解析字符串${app.name:fsx}
,如果${app.name}
解析失败,则解析结果为fsx
; -
ignoreUnresolvablePlaceholders
,是否忽略解析失败的占位符;
另外在构造函数中,还计算了simplePrefix
, 如果后缀为"}"
,"]"
,")"
,并且前缀以"{"
, "["
,"("
结尾,则simplePrefix
为"{"
, "["
,"("
,否则为placeholderPrefix
。至于simplePrefix
作用,后文再做分析。
可以使用如上构造函数新建一个PropertyPlaceholderHelper
,
// 占位符前缀为"${", 后缀为"}", 默认值分隔符为 ":", 不忽略解析失败的占位符, 即解析失败时报错
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", ":", false);
核心方法
PropertyPlaceholderHelper
核心方法
/**
* 替换字符串value中所有格式为${name}的占位符, 占位符的值由placeholderResolver提供;
* @param value 包含需要需要待替换占位符的字符串
* @param placeholderResolver 提供占位符替换值
*/
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
Assert.notNull(value, "'value' must not be null");
return parseStringValue(value, placeholderResolver, null);
}
PropertyPlaceholderHelper
是一个单纯的工具类,不包含application.yml
等文件配置属性,所以待替换占位符的替换值就需要通过placeholderResolver
提供,看下PlaceholderResolver
类源码:
/**
* 用于解析字符串中占位符的替换值的策略接口
*/
@FunctionalInterface
public interface PlaceholderResolver {
/**
* 将提供的占位符名称解析为替换值
* @param placeholderName 待解析的占位符的名称
*/
@Nullable
String resolvePlaceholder(String placeholderName);
}
可以看出该接口是一个函数式接口,需要提供一个函数,将占位符名称解析成替换值;
例如:
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", ":", false);
Map<String, String> map = Maps.newHashMap();
map.put("app.name", "fsx");
map.put("user.home", "app.name");
map.put("app.key", "${user.home}");
// 占位符的值由map提供, 输出fsx
System.err.println(helper.replacePlaceholders("${app.name}", map::get));
// 输出fsx, 支持嵌套
System.err.println(helper.replacePlaceholders("${${user.home}}", map::get));
// 输出fsx+app.name, 与c语言printf类似,replacePlaceholders只替换占位符的值,其余字符原封不动输出
System.err.println(helper.replacePlaceholders("${app.name}+${user.home}", map::get));
// 输出app.name, 支持递归解析
System.err.println(helper.replacePlaceholders("${app.key}", map::get));
// 输出${app.user}, map中不包含app.user的值但ignoreUnresolvablePlaceholders为true, 对不能解析的占位符不做处理
System.err.println(new PropertyPlaceholderHelper("${", "}", ":", true).replacePlaceholders("${app.user}", map::get));
// 报错, map中不包含app.user的值且ignoreUnresolvablePlaceholders为false
System.err.println(helper.replacePlaceholders("${app.user}", map::get));
fsx
fsx
fsx+app.name
app.name
${app.user}
Exception in thread "main" java.lang.IllegalArgumentException: Could not resolve placeholder 'app.user' in value "${app.user}"
at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:178)
at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:124)
at com.zte.iscp.purchasecoordination.adapter.util.SupplierUtils.main(SupplierUtils.java:212)
二、源码阅读
下面瞻仰下Sping大佬源码。replacePlaceholders
函数内部调用了函数parseStringValue
;
protected String parseStringValue(
String value, PlaceholderResolver placeholderResolver, @Nullable Set<String> visitedPlaceholders) {
// 查找value中第一个占位符前缀
int startIndex = value.indexOf(this.placeholderPrefix);
// 如果没有占位符前缀, 说明value中不包含占位符, 无需替换, 直接返回
if (startIndex == -1) {
return value;
}
StringBuilder result = new StringBuilder(value);
while (startIndex != -1) {
// 关键, startIndex表示最外层占位符前缀索引, endIndex表示最外层占位符后缀索引
int endIndex = findPlaceholderEndIndex(result, startIndex);
if (endIndex != -1) {
// placeholder表示最外层占位符名称, 可能嵌套有占位符
String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
if (visitedPlaceholders == null) {
visitedPlaceholders = new HashSet<>(4);
}
// 防止占位符循环引用
if (!visitedPlaceholders.add(originalPlaceholder)) {
throw new IllegalArgumentException(
"Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
}
// 解析最外层占位符之前先要解析嵌套占位符, 这里递归解析最外层占位符名称中内部嵌套占位符
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
// 嵌套占位符解析完成后, 占位符名称placeholder中就不包含占位符, 可以直接从placeholderResolver获取替换值
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
// 有默认值情况
if (propVal == null && this.valueSeparator != null) {
int separatorIndex = placeholder.indexOf(this.valueSeparator);
if (separatorIndex != -1) {
// 如果包含默认值, 则placeholder中包含默认值分隔符和默认值;
// 例如对于${app.name:name}, placeholder为app.name:name, 上面代码会解析失败;
// 这样做的好处是,如果默认值中包含占位符,则属于嵌套占位符,上面递归解析会将其一起解析;
// 这里需要拿到实际占位符名称再次解析,actualPlaceholder即为实际占位符名称, 例子中值为app.name
String actualPlaceholder = placeholder.substring(0, separatorIndex);
// 获取默认值, 例如${app.name:name}默认值为name
String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
// 无替换值, 取默认值
if (propVal == null) {
propVal = defaultValue;
}
}
}
if (propVal != null) {
// 替换值可能还包含占位符, 还需要递归调用再次解析
// 例如, ${app.key}将app.key解析成${user.home}后, 该字符串仍然包含占位符, 需要再次解析
propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
if (logger.isTraceEnabled()) {
logger.trace("Resolved placeholder '" + placeholder + "'");
}
// 继续解析下一个最外层占位符
// 例如${app.name}+${user.home},
// 上面的程序只是把${app.name}解析完成, ${user.home}需要通过while循环继续解析
startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
}
// 忽略解析失败占位符, 继续解析下一个占位符
else if (this.ignoreUnresolvablePlaceholders) {
startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
}
// 不能忽略, 则报错
else {
throw new IllegalArgumentException("Could not resolve placeholder '" +
placeholder + "'" + " in value \"" + value + "\"");
}
visitedPlaceholders.remove(originalPlaceholder);
}
else {
startIndex = -1;
}
}
return result.toString();
}
总结
不得不说Spring大佬写的代码即简洁又优美,值得学习。上面源码有几个地方比较重要:
- 整体来看,采用循环+递归方式解析占位符,有点类似深度优先搜索了。首先待解析字符串可能包含多个需要解析的占位符,所以使用
while
对最外层每个占位符进行解析;其次,每个占位符名称中可能嵌套占位符,所以在解析外层占位符之前递归解析内层占位符; - 因为占位符替换值可能又包含占位符,如果包含的占位符和源占位符一样,那不就无限递归下去了,程序就会
stackoverflow
,就像深度优先搜索中的去重问题。例如如下代码:
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", ":", false);
Map<String, String> map = Maps.newHashMap();
map.put("app.name", "${app.name}+fsx");
System.err.println(helper.replacePlaceholders("${app.name}", map::get));
首先解析占位符${app.name}
,解析结果为${app.name}+fsx
,包含占位符,又需要解析${app.name}
,再解析${app.name}
。。。PropertyPlaceholderHelper
解决这个问题是再解析嵌套占位符值通过参数visitedPlaceholders
将外层占位符传递给内层,如果内层遇到相同占位符说明发生占位符循环引用,就报错;
- 对于默认值,
PropertyPlaceholderHelper
不是先占位符,解析失败后在取默认值。而是先将占位符名称和默认值一起解析,然后再解析实际占位符,如果实际占位符解析失败,再取默认值。这样做的好处是,占位符名称和默认值一起解析,会将默认值包含嵌套占位符解一起解析完成;所以默认值和占位符名称一样,也可以包含占位符,比如${app.user:${app.name}}
;
另外还有一个重要函数findPlaceholderEndIndex
没有分析,这个函数功能是查找和占位符前缀匹配的后缀索引,源码如下:
/**
* 在字符序列buf中查找和索引为startIndex的前缀匹配的后缀索引
*/
private int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
int index = startIndex + this.placeholderPrefix.length();
// 内部嵌套占位符未匹配前缀数量
int withinNestedPlaceholder = 0;
while (index < buf.length()) {
// buf以index开始的字符序列和占位符后缀匹配
if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) {
// 如果内部嵌套占位符大于0, 说明该占位符后缀属于内部嵌套占位符后缀
// 例如, ${${app.name}}, 在查找和第一个${匹配的后缀, 遇到的第一个}是的情况
if (withinNestedPlaceholder > 0) {
// 内部嵌套占位符匹配成功一个, 数量减1
withinNestedPlaceholder--;
index = index + this.placeholderSuffix.length();
}
// 内部嵌套占位符都已匹配完成, 则该后缀即为结果
else {
return index;
}
}
// 如果遇到占位符前缀, 该占位符为内部嵌套占位符
// 至于为什么使用simplePrefix, 而不是placeholderPrefix, 暂时还不是太清楚作者意图
else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) {
withinNestedPlaceholder++;
index = index + this.simplePrefix.length();
}
else {
index++;
}
}
return -1;
}