Spring 源码解读:SystemPropertyPlaceholderResolver 如何用系统属性解析 `${}` 占位符
发布时间:2026/9/13 13:51:17来源:尧图网络
Spring 源码解读SystemPropertyPlaceholderResolver 如何用系统属性解析${}占位符【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter在 Spring 的占位符解析体系中SystemPropertyUtils.SystemPropertyPlaceholderResolver承担着“从 JVM 系统属性与操作系统环境变量中取回占位符对应值”这一最终职责。本文基于 source-code-hunter 仓库中 Spring 源码笔记系列完整剖析该类的实现原理、调用链PropertyPlaceholderHelper#parseStringValue→PlaceholderResolver#resolvePlaceholder、回退策略以及与PropertyPlaceholderConfigurer中systemPropertiesMode三种模式的配合关系帮助读者彻底理解${user.dir}这类占位符在 Spring 中是如何被解析成真实值的。一、类定位与所属体系类全路径org.springframework.util.SystemPropertyUtils.SystemPropertyPlaceholderResolver所属工具类Spring-SystemPropertyUtilsorg.springframework.util.SystemPropertyUtils实现接口PropertyPlaceholderHelper.PlaceholderResolver该内部类本质上是PlaceholderResolver函数式接口的一个实现。PlaceholderResolver的职责非常单一将占位符中的内容替换成属性值。例如假设现有属性表user.dir c:\home传入参数user.dir就会获得c:\home。其接口定义如下完整源码见 Spring-PlaceholderResolverFunctionalInterface public interface PlaceholderResolver { /** * Resolve the supplied placeholder name to the replacement value. * param placeholderName the name of the placeholder to resolve * return the replacement value, or {code null} if no replacement is to be made */ Nullable String resolvePlaceholder(String placeholderName); }在 Spring 中PlaceholderResolver有多个实现各自从不同的数据源取值构成一张“占位符解析器家族”实现类数据源仓库文档SystemPropertyPlaceholderResolverJVM 系统属性 → 系统环境变量本文PropertyPlaceholderConfigurerResolverProperties 文件 系统属性按模式Spring-PropertyPlaceholderConfigurerResolverServletContextPlaceholderResolverServletContext init-parameter → 系统属性 → 环境变量Spring-ServletContextPlaceholderResolver二、核心源码逐行解读SystemPropertyPlaceholderResolver的完整实现如下private static class SystemPropertyPlaceholderResolver implements PropertyPlaceholderHelper.PlaceholderResolver { private final String text; public SystemPropertyPlaceholderResolver(String text) { this.text text; } Override Nullable public String resolvePlaceholder(String placeholderName) { try { String propVal System.getProperty(placeholderName); if (propVal null) { // Fall back to searching the system environment. // 获取系统属性 propVal System.getenv(placeholderName); } return propVal; } catch (Throwable ex) { System.err.println(Could not resolve placeholder placeholderName in [ this.text ] as system property: ex); return null; } } }解析过程可以拆解为三个步骤1. 记录原始文本text字段构造时传入的text是包含占位符的原始字符串。它的作用有两个在解析失败时用于输出错误上下文in [...]部分便于日志与排障时定位是哪一段文本中的占位符解析失败。2. 第一优先System.getProperty(placeholderName)首先通过System.getProperty(placeholderName)从JVM 系统属性中取值。常见的系统属性包括user.dir当前工作目录、java.home、os.name、file.separator等。这些属性由 JVM 启动时通过-D参数注入或在运行时由System.setProperty设置。3. 回退机制System.getenv(placeholderName)当系统属性中取不到值propVal null时自动回退到操作系统环境变量System.getenv。这是该解析器最值得注意的设计它天然实现了“JVM 属性优先、环境变量兜底”的双通道取值策略。这意味着同一个占位符名-Dkeyvalue的优先级高于操作系统的export keyvalue。4. 异常兜底catch (Throwable ex)在部分受限环境如 Java 安全管理器SecurityManager开启时访问系统属性可能抛出异常。此处捕获所有Throwable向标准错误输出一条提示信息并返回null从而保证解析流程不因单个占位符异常而中断。三、调用链占位符是如何一步步被解析的SystemPropertyPlaceholderResolver本身只负责“给定名称取回值”而整个占位符的查找、嵌套解析、默认值处理则发生在PropertyPlaceholderHelper#parseStringValue中。1. 常量定义与两个 HelperSystemPropertyUtils首先定义了占位符的语法常量详见 Spring-SystemPropertyUtils/** * Prefix for system property placeholders: ${. * 前缀占位符 */ public static final String PLACEHOLDER_PREFIX ${; /** * Suffix for system property placeholders: }. * 后缀占位符 */ public static final String PLACEHOLDER_SUFFIX }; /** * Value separator for system property placeholders: :. * 值分割符号 */ public static final String VALUE_SEPARATOR :; /** * 占位符解析类 */ private static final PropertyPlaceholderHelper strictHelper new PropertyPlaceholderHelper(PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, VALUE_SEPARATOR, false); /** * 占位符解析类 */ private static final PropertyPlaceholderHelper nonStrictHelper new PropertyPlaceholderHelper(PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, VALUE_SEPARATOR, true);可以看到系统属性占位符的语法约定为前缀${后缀}分隔符:用于提供默认值如${key:default}。同时SystemPropertyUtils内置了两个PropertyPlaceholderHelperstrictHelperignoreUnresolvablePlaceholders false遇到无法解析的占位符直接抛异常nonStrictHelperignoreUnresolvablePlaceholders true无法解析时保留原样不报错。2. 递归解析主流程PropertyPlaceholderHelper#parseStringValue是整个占位符解析的核心入口完整源码见 Spring-PropertyPlaceholderHelper其关键逻辑为protected String parseStringValue( String value, PlaceholderResolver placeholderResolver, Nullable SetString 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); } // Recursive invocation, parsing placeholders contained in the placeholder key. // 递归获取占位符内容 placeholder parseStringValue(placeholder, placeholderResolver, visitedPlaceholders); // Now obtain the value for the fully resolved key... // 解析占位符内容获得真正的属性值 String propVal placeholderResolver.resolvePlaceholder(placeholder); 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) { // Recursive invocation, parsing placeholders contained in the // previously resolved placeholder value. propVal parseStringValue(propVal, placeholderResolver, visitedPlaceholders); result.replace(startIndex, endIndex this.placeholderSuffix.length(), propVal); if (logger.isTraceEnabled()) { logger.trace(Resolved placeholder placeholder ); } startIndex result.indexOf(this.placeholderPrefix, startIndex propVal.length()); } else if (this.ignoreUnresolvablePlaceholders) { // Proceed with unprocessed value. 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(); }流程要点定位占位符用indexOf(placeholderPrefix)找到${的位置找不到直接返回原文查找结束符通过findPlaceholderEndIndex匹配配对的}该方法内部用withinNestedPlaceholder计数器支持嵌套占位符如${${key}}循环引用检测借助visitedPlaceholders集合若同一个占位符重复出现则抛出Circular placeholder reference异常递归解析占位符名占位符内容本身可能还是占位符先递归解析委托给PlaceholderResolver调用placeholderResolver.resolvePlaceholder(placeholder)——这正是SystemPropertyPlaceholderResolver被挂载的位置默认值支持当解析结果为null且存在:分隔符时取分隔符前的部分再解析仍为null则使用分隔符后的默认值二次递归解析出的值本身可能还包含占位符继续递归解析不可解析处理按ignoreUnresolvablePlaceholders决定是保留原文继续还是抛出IllegalArgumentException。3. 时序关系SystemPropertyUtils.resolvePlaceholders的时序流程可参考仓库中的时序图该时序图因递归调用而显得较长其核心方法最终都会指向PlaceholderResolver通过PlaceholderResolver获取属性值而SystemPropertyPlaceholderResolver正是SystemPropertyUtils内部对该接口的默认实现。四、与 Spring 容器配置体系的配合三种 systemPropertiesMode在更上层的PropertyPlaceholderConfigurer已废弃推荐使用PropertySourcesPlaceholderConfigurer中系统属性解析器与 Properties 文件解析器是协同工作的。核心逻辑见 Spring-PropertyPlaceholderConfigurerResolverNullable protected String resolvePlaceholder(String placeholder, Properties props, int systemPropertiesMode) { String propVal null; if (systemPropertiesMode SYSTEM_PROPERTIES_MODE_OVERRIDE) { propVal resolveSystemProperty(placeholder); } if (propVal null) { propVal resolvePlaceholder(placeholder, props); } if (propVal null systemPropertiesMode SYSTEM_PROPERTIES_MODE_FALLBACK) { propVal resolveSystemProperty(placeholder); } return propVal; } Nullable protected String resolvePlaceholder(String placeholder, Properties props) { return props.getProperty(placeholder); } Nullable protected String resolveSystemProperty(String key) { try { String value System.getProperty(key); if (value null this.searchSystemEnvironment) { value System.getenv(key); } return value; } catch (Throwable ex) { if (logger.isDebugEnabled()) { logger.debug(Could not access system property key : ex); } return null; } }这里定义了系统属性与 Properties 文件之间的优先级模式语义解析顺序SYSTEM_PROPERTIES_MODE_OVERRIDE系统属性覆盖配置文件先查系统属性再查 PropertiesSYSTEM_PROPERTIES_MODE_FALLBACK系统属性兜底先查 Properties再查系统属性SYSTEM_PROPERTIES_MODE_NEVER不使用系统属性仅查 Properties注意resolveSystemProperty内部同样遵循“System.getProperty优先、System.getenv兜底”的规则与SystemPropertyPlaceholderResolver的取值策略完全一致并且通过searchSystemEnvironment开关控制是否回退到环境变量。五、解析器的横向对比将三个PlaceholderResolver实现放在一起对比可以更清楚地看到 Spring 占位符解析的“数据源优先级”设计解析器数据源优先级适用场景SystemPropertyPlaceholderResolverJVM 系统属性 → 环境变量纯SystemPropertyUtils.resolvePlaceholders工具调用PropertyPlaceholderConfigurerResolver系统属性按 mode→ PropertiesXML 配置中的${...}占位符ServletContextPlaceholderResolverServletContext init-param → 系统属性 → 环境变量Web 环境下的占位符解析其中ServletContextPlaceholderResolver的回退链路最长先取servletContext.getInitParameter(placeholderName)取不到再回退到System.getProperty最后回退到System.getenv详见 Spring-ServletContextPlaceholderResolver。从源码结构可以推断这体现了 Spring 在“容器参数优先于全局配置”方面的设计取向。六、实战验证1. 通过系统属性/环境变量注入配置在 Spring Boot 或传统 Spring 工程中占位符${...}的解析链最终都会走到上述机制。常见用法# 方式一JVM 系统属性优先级最高 java -Dmy.app.namesource-code-hunter -jar app.jar # 方式二操作系统环境变量作为回退 export MY_APP_NAMEsource-code-hunter java -jar app.jar配置文件中对应的占位符# 优先从 -Dmy.app.name 取值取不到再尝试环境变量 my.app.name app.name${my.app.name:unknown-app}其中的:unknown-app正是利用VALUE_SEPARATOR :提供的默认值能力当系统属性与环境变量都取不到时parseStringValue会使用默认值unknown-app。2. 在代码中直接使用 SystemPropertyUtilsSystemPropertyUtils是org.springframework.util包下的纯工具类不依赖 Spring 容器可直接调用import org.springframework.util.SystemPropertyUtils; String resolved SystemPropertyUtils.resolvePlaceholders(${user.dir}/logs); System.out.println(resolved); // 输出当前工作目录/logs执行时resolvePlaceholders内部会创建SystemPropertyPlaceholderResolver并委托strictHelper完成解析user.dir通过System.getProperty(user.dir)直接命中。如果占位符在系统属性和环境变量中都不存在默认的严格模式会抛出IllegalArgumentException而resolvePlaceholders的宽松变体则保留原样。七、总结SystemPropertyPlaceholderResolver虽然只是 Spring util 包下一个十几行的私有内部类但它完整呈现了 Spring 占位符体系中“解析器Helper与取值器Resolver分离”的核心设计职责单一PropertyPlaceholderHelper只负责语法前缀、后缀、嵌套、默认值、循环引用具体数据源由PlaceholderResolver实现决定双通道取值JVM 系统属性优先操作系统环境变量兜底异常时安全返回null可组合扩展通过替换PlaceholderResolver实现即可对接 Properties、ServletContext、环境变量等不同数据源这正是 Spring 设计模式在源码层面的典型体现。相关文档链接Spring-SystemPropertyUtils工具类与常量定义Spring-PlaceholderResolver函数式接口定义Spring-PropertyPlaceholderHelperparseStringValue 解析主流程Spring-PropertyPlaceholderConfigurerResolverProperties 系统属性模式Spring-ServletContextPlaceholderResolverWeb 环境解析器【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网