【Spring】PropertyPlaceholderHelper —— 占位符解析工具类
前言
PropertyPlaceholderHelper
,位于 org.springframework.util
包下,用来解析 占位符
很好用的工具类,本文做一个简单的 源码解读
,并给出使用示例 demo
PropertyPlaceholderHelper
属性
private static final Map<String, String> wellKnownSimplePrefixes = new HashMap<>(4);
static {
wellKnownSimplePrefixes.put("}", "{");
wellKnownSimplePrefixes.put("]", "[");
wellKnownSimplePrefixes.put(")", "(");
}
private final String placeholderPrefix;
private final String placeholderSuffix;
private final String simplePrefix;
@Nullable
private final String valueSeparator;
private final boolean ignoreUnresolvablePlaceholders;
主要属性如上:
wellKnownSimplePrefixes
维护了一组简单的前后缀组合placeholderPrefix
,占位符前缀placeholderSuffix
,占位符后缀simplePrefix
,占位符简写,比如前缀${
的简写就是$
,设计意图存疑,见下文分析valueSeparator
,默认值分割符,譬如当其值为:
时,对应${a:b}
意味着当a
解析失败时取默认值b
ignoreUnresolvablePlaceholders
,解析失败时是否忽略
构造方法
public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix) {
this(placeholderPrefix, placeholderSuffix, null, true);
}
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
默认为null
ignoreUnresolvablePlaceholders
默认为true
- 解析对应的
simplePrefix
核心 API
replacePlaceholders
public String replacePlaceholders(String value, final Properties properties) {
Assert.notNull(properties, "'properties' must not be null");
return replacePlaceholders(value, properties::getProperty);
}
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
Assert.notNull(value, "'value' must not be null");
return parseStringValue(value, placeholderResolver, null);
}
- 核心
API
自然是解析占位符方法replacePlaceholders
,主要指定两个参数:待解析字符串
和解析方式
- 单独提供
API
支持Properties
数据源的解析,该方法收口到replacePlaceholders(String value, PlaceholderResolver placeholderResolver)
- 主要逻辑归于方法
parseStringValue
PlaceholderResolver
@FunctionalInterface
public interface PlaceholderResolver {
@Nullable
String resolvePlaceholder(String placeholderName);
}
- 一个内部类
函数式接口
,即指定从数据源获取对应属性的方式,比如Properties
获取属性的方法properties::getProperty
- 通常由我们自己来指定,最方便的莫过于
PropertySource::getProperty
了
parseStringValue
protected String parseStringValue(
String value, PlaceholderResolver placeholderResolver, @Nullable Set<String> visitedPlaceholders) {
// 如果不包含指定前缀,那就原样返回
int startIndex = value.indexOf(this.placeholderPrefix);
if (startIndex == -1) {
return value;
}
StringBuilder result = new StringBuilder(value);
while (startIndex != -1) {
// 先找到对应后缀的下标
int endIndex = findPlaceholderEndIndex(result, startIndex);
if (endIndex != -1) {
// 截取前后缀中间的目标字符串
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");
}
/**
* 然后开始递归解析目标字符串,因为目标字符串可能也包含占位符,
* 比如 ${a${b}}
*/
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
// 解析出来后,交给 placeholderResolver 获取对应属性
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
/**
* 如果解析结果是 null
* 那就看是有指定 默认值分割符,如果有且原始值包含该分割符
* 则先获取 分割符 前的 key,获取无果返回指定默认值
*/
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());
propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
if (propVal == null) {
propVal = defaultValue;
}
}
}
/**
* 如果获取成功,则再 解析 一次
* 这意味着如果最终解析出来的属性中仍然包含 占位符
* 是可以继续解析的
*/
if (propVal != null) {
propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
// 解析完后整体替换
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());
}
/**
* 到这里就是解析无果了,根据属性
* ignoreUnresolvablePlaceholders
* 决定是否抛出异常 IllegalArgumentException
*/
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();
}
这就是本类最核心最有技巧的方法了,结合注释,此处再做一个简单总结:
- 首先
占位符
是支持嵌套的,譬如${a${b}}
,因此截取出占位符
中间的目标字符串后,是需要递归解析的 - 其次,此处是不支持解析
循环占位符
的,比如当属性源为a -> ${a}
时,显然是无法解析的,此处通过一个Set<String> visitedPlaceholders
保证该逻辑 - 占位符解析完成后,就是从
属性源
中解析对应属性了,对于解析为null
的属性是支持指定 默认值 的(前提是你指定了 默认值分割符) - 如果仍然解析失败,根据属性
ignoreUnresolvablePlaceholders
决定是否抛出异常 - 如果解析成功了,对于解析的结果,如果仍然包含
占位符
,则再次进行解析 - 最后更新
startIndex
,便于解析后面的占位符
,如果都解析完了,就是-1
,然后把解析完的 占位符 移出visitedPlaceholders
- 最后,分析下用于获取
占位符
后缀下标的方法findPlaceholderEndIndex
findPlaceholderEndIndex
private int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
// 赋值 index
int index = startIndex + this.placeholderPrefix.length();
int withinNestedPlaceholder = 0;
// 从 index 处开始解析
while (index < buf.length()) {
/**
* 先匹配后缀,如果匹配到,先看下是不是嵌套的后缀
* 如果是嵌套后缀,嵌套层级 -1,重新计算 index
* 否则就是匹配到了,直接返回
*/
if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) {
if (withinNestedPlaceholder > 0) {
withinNestedPlaceholder--;
index = index + this.placeholderSuffix.length();
}
else {
return index;
}
}
/**
* 如果没匹配到,就看下是否匹配到 simplePrefix
* 如果匹配到了,说明有嵌套 占位符
* 嵌套层级 +1,重新计算 index
*/
else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) {
withinNestedPlaceholder++;
index = index + this.simplePrefix.length();
}
// 如果都没有,index + 1 即可
else {
index++;
}
}
return -1;
}
- 此处借助
StringUtils.substringMatch
来判断是否匹配前后缀 - 包含了对
嵌套占位符
逻辑的处理,主要是涉及index
的重新计算 - 值得一提的是,对于
嵌套占位符
前缀的匹配,为什么要使用simplePrefix
而不是直接使用placeholderPrefix
,本人暂未get
到(此处使用placeholderPrefix
是可以实现的,可以自行尝试),如果有了解的小伙伴,欢迎讨论
示例
源码解读到此为止,最后给出一段示例整体体会该类的作用
@Test
public void test() {
// 指定数据源为 MapPropertySource
MapPropertySource mapPropertySource
= new MapPropertySource(
"map"
, new HashMap<String, Object>() {
{
put("b", "1");
put("a1", "2");
put("c", "${d}");
put("d", "e");
}
}
);
// 指定 前缀 后缀 默认值分割符 不允许解析失败
PropertyPlaceholderHelper placeholderHelper
= new PropertyPlaceholderHelper("${", "}", ":", false);
Assertions.assertEquals(
"2"
, placeholderHelper.replacePlaceholders(
"${a${b}}"
, name -> (String) mapPropertySource.getProperty(name)
)
);
Assertions.assertEquals(
"e"
, placeholderHelper.replacePlaceholders(
"${c}"
, name -> (String) mapPropertySource.getProperty(name)
)
);
Assertions.assertEquals(
"f"
, placeholderHelper.replacePlaceholders(
"${e:f}"
, name -> (String) mapPropertySource.getProperty(name)
)
);
}
- 指定数据源为
MapPropertySource
,关于MapPropertySource
,下文给出相关链接 - 此处创建
PropertyPlaceholderHelper
特别指定了相关属性 - 解析时对应的
PlaceholderResolver
自然就是name -> (String) mapPropertySource.getProperty(name)
了
关于 MapPropertySource
相关,可见下文:
【Spring】PropertySource 的解读与示例(MapPropertySource CommandLinePropertySource)
总结
本文扣的比较细,对于 Spring
的源码我一般较少扣细节,而是喜欢去体会它的设计思路,但是本类对 占位符
解析的实现确实十分精巧,回归编程的本质,十分值得学习