Spring Boot v2.4.4源码解析(一)字符串占位符解析器 PropertyPlaceholderHelper

本文主要是介绍Spring Boot v2.4.4源码解析(一)字符串占位符解析器 PropertyPlaceholderHelper,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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 待解析的占位符的名称*/@NullableString 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}默认值为nameString 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) {// 内部嵌套占位符匹配成功一个, 数量减1withinNestedPlaceholder--;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;
}

这篇关于Spring Boot v2.4.4源码解析(一)字符串占位符解析器 PropertyPlaceholderHelper的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/965856

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

一篇文章彻底搞懂macOS如何决定java环境

《一篇文章彻底搞懂macOS如何决定java环境》MacOS作为一个功能强大的操作系统,为开发者提供了丰富的开发工具和框架,下面:本文主要介绍macOS如何决定java环境的相关资料,文中通过代码... 目录方法一:使用 which命令方法二:使用 Java_home工具(Apple 官方推荐)那问题来了,

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三

一文解析C#中的StringSplitOptions枚举

《一文解析C#中的StringSplitOptions枚举》StringSplitOptions是C#中的一个枚举类型,用于控制string.Split()方法分割字符串时的行为,核心作用是处理分割后... 目录C#的StringSplitOptions枚举1.StringSplitOptions枚举的常用

Python函数作用域与闭包举例深度解析

《Python函数作用域与闭包举例深度解析》Python函数的作用域规则和闭包是编程中的关键概念,它们决定了变量的访问和生命周期,:本文主要介绍Python函数作用域与闭包的相关资料,文中通过代码... 目录1. 基础作用域访问示例1:访问全局变量示例2:访问外层函数变量2. 闭包基础示例3:简单闭包示例4