【Spring】PropertyPlaceholderHelper —— 占位符解析工具类

本文详细介绍了Spring中的PropertyPlaceholderHelper类,用于解析配置文件中的占位符。该类提供了核心API`replacePlaceholders`,通过 PlaceholderResolver 接口实现属性的动态获取。文章深入探讨了类的属性、构造方法和核心方法`parseStringValue`的逻辑,以及如何处理未解析的占位符。最后,通过示例展示了如何使用PropertyPlaceholderHelper进行占位符替换。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

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;
	}
  • 必须指定 placeholderPrefixplaceholderSuffix
  • 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 的源码我一般较少扣细节,而是喜欢去体会它的设计思路,但是本类对 占位符 解析的实现确实十分精巧,回归编程的本质,十分值得学习

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值