source-code-hunter 源码解析:Spring MapPropertySource 与 PropertySource 环境抽象体系
【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter
导读
org.springframework.core.env.MapPropertySource是 Spring 环境抽象(Environment/PropertySource)中最基础、使用最频繁的实现类之一——它以Map<String, Object>为内部数据结构,把"属性名到属性值"的映射直接暴露给 Spring 的占位符解析与属性注入机制。本文以本仓库 Spring-MapPropertySource.md 为核心,逐方法拆解其源码实现,并结合EnumerablePropertySource、PropertiesPropertySource、ResourcePropertySource等兄弟/子类及 Spring Boot 启动加载链路,讲透这一"最不起眼却无处不在"的配置载体。读完本文,你将掌握 PropertySource 家族的继承体系、Map 型属性源的读写语义,以及它在 Spring/Spring Boot 配置解析中的真实调用场景。
图片说明:Spring 环境抽象中
PropertySource及其相关类的整体类图,MapPropertySource位于该体系中。
一、类定位:MapPropertySource在 Spring 环境抽象中的角色
MapPropertySource的完整类路径为org.springframework.core.env.MapPropertySource,它继承自EnumerablePropertySource<Map<String, Object>>,而EnumerablePropertySource又继承自抽象基类PropertySource<T>。三者构成了 Springcore.env包中"属性源"最核心的一条继承链:
PropertySource<T>:抽象基类,持有name(属性源名称)与source(底层数据对象)两个 final 字段,定义抽象方法getProperty(String name);EnumerablePropertySource<T>:在基类之上补充了可枚举性——新增抽象方法getPropertyNames(),要求实现类能够返回全部属性名(详见仓库 Spring-EnumerablePropertySource.md);MapPropertySource:将底层数据对象固定为Map<String, Object>,所有属性操作都翻译为对 Map 的读写。
用一句话概括:MapPropertySource就是"给一个 Map 起个名字,然后把它包装成 Spring 能识别的属性源"。正是这一层轻量包装,让系统属性、环境变量、Properties文件内容、命令行参数等一切"键值对形态"的配置,都能统一进入 Spring 的配置体系。
二、源码逐方法解析:三个方法即全部
MapPropertySource的实现极其精简,只有构造函数加三个方法。完整源码如下(与仓库 Spring-MapPropertySource.md 一致):
public class MapPropertySource extends EnumerablePropertySource<Map<String, Object>> { public MapPropertySource(String name, Map<String, Object> source) { super(name, source); } @Override @Nullable public Object getProperty(String name) { // 从 map 中获取 name 对应的 value return this.source.get(name); } @Override public boolean containsProperty(String name) { // 判断是否存在 name 属性 return this.source.containsKey(name); } @Override public String[] getPropertyNames() { // 获取 map 的所有 key return StringUtils.toStringArray(this.source.keySet()); } }2.1 构造函数:绑定名字与数据源
public MapPropertySource(String name, Map<String, Object> source) { super(name, source); }构造函数仅做透传。在PropertySource基类中(见仓库 Spring-PropertySources.md),name与source都会被存储为 final 字段,并带有两项校验:
Assert.hasText(name, "Property source name must contain at least one character"); Assert.notNull(source, "Property source must not be null");即属性源名称不能为空字符串,底层 Map 不能为 null。name的作用非常关键:它是该属性源在MutablePropertySources列表中的唯一标识,后续通过PropertySource.named(name)进行查找、替换、排序都依赖它。
2.2 getProperty:Map 取值
@Override @Nullable public Object getProperty(String name) { return this.source.get(name); }直接委托给Map#get,语义与 Map 完全一致:
- 命中 key 时返回对应 value;
- 未命中时返回
null(方法标注@Nullable)。
需要注意的是,value 类型是Object而非String。虽然大部分配置值是字符串,但 Map 型属性源并不做类型限制,真正的类型转换发生在更上层的PropertyResolver/ConversionService阶段。此外,由于底层可能是HashMap,get的时间复杂度为 O(1),这也使 MapPropertySource 成为所有属性源中查询效率最高的一类。
2.3 containsProperty:判断属性是否存在
@Override public boolean containsProperty(String name) { return this.source.containsKey(name); }与基类PropertySource#containsProperty的默认实现(通过getProperty(name) != null判断)不同,MapPropertySource用containsKey直接判断,更精确:即使某个 key 对应的 value 为null,containsKey依然返回true,而基类的默认实现会返回false。这是 Map 型属性源在语义上的一个重要细节。
2.4 getPropertyNames:枚举全部属性名
@Override public String[] getPropertyNames() { return StringUtils.toStringArray(this.source.keySet()); }将Map的 key 集合转换为String[]返回,这是EnumerablePropertySource对"可枚举"这一能力的落地实现。它使得上层可以对属性源做全量遍历——例如AbstractEnvironment在打印配置信息、PropertySourcesPropertyResolver在生成占位符无法解析的报错信息时都会用到它。
2.5 父类 EnumerablePropertySource 的配合
MapPropertySource之所以能"只写三个方法",是因为父类EnumerablePropertySource用getPropertyNames()反哺了containsProperty的通用实现(见仓库 Spring-EnumerablePropertySource.md):
@Override public boolean containsProperty(String name) { return ObjectUtils.containsElement(getPropertyNames(), name); } public abstract String[] getPropertyNames();也就是说:凡是可枚举的属性源,即使不重写containsProperty,也能通过遍历属性名完成存在性判断。MapPropertySource出于性能考虑选择直接用containsKey覆盖了该实现,而CommandLinePropertySource、ServletContextPropertySource等兄弟类则各自实现了自己的语义(详见下文)。
三、家族图谱:从 MapPropertySource 出发看 PropertySource 体系
围绕MapPropertySource,Spring 在其上派生了一条完整的实现链,仓库中对应文档均已收录:
PropertySource<T> (抽象基类:name + source) └── EnumerablePropertySource<T> (抽象:getPropertyNames()) ├── MapPropertySource (本文主角:Map<String,Object> 包装) │ └── PropertiesPropertySource (Properties 包装) │ └── ResourcePropertySource (资源文件加载) ├── CommandLinePropertySource<T> (命令行参数) ├── ServletContextPropertySource (Web 应用初始化参数) └── SystemEnvironmentPropertySource (环境变量,Spring 5.1+ 内部实现)3.1 PropertiesPropertySource:MapPropertySource 的第一个子类
仓库 Spring-PropertiesPropertySource.md 记录了这一层关系:java.util.Properties本身继承自Hashtable<Object,Object>,本质就是一个 Map 结构,因此PropertiesPropertySource直接继承MapPropertySource复用其全部逻辑:
public class PropertiesPropertySource extends MapPropertySource { @SuppressWarnings({"rawtypes", "unchecked"}) public PropertiesPropertySource(String name, Properties source) { super(name, (Map) source); } @Override public String[] getPropertyNames() { synchronized (this.source) { return super.getPropertyNames(); } } }值得注意的差异点:PropertiesPropertySource重写了getPropertyNames(),用synchronized (this.source)包裹——因为Hashtable是线程安全容器,遍历其 keySet 时加锁可以避免并发修改导致ConcurrentModificationException。getProperty与containsProperty则原样继承MapPropertySource(Hashtable#get/containsKey本身线程安全)。
3.2 ResourcePropertySource:加载配置文件到 Map
ResourcePropertySource继承PropertiesPropertySource,将"资源文件"转化为 Map 属性源(见仓库 Spring-ResourcePropertySource.md):
- 通过
PropertiesLoaderUtils.loadProperties(resource)把.properties文件读成Properties,最终落到底层的 Map 结构; - 支持
classpath:前缀与ClassLoader参数,例如new ResourcePropertySource("myConfig", "classpath:app.properties", classLoader); getNameForResource(Resource)用resource.getDescription()作为属性源名称,描述为空时退化为短类名@identityHashCode;withName(String)可在不改动底层数据的前提下重新命名属性源,便于在MutablePropertySources中调整优先级。
application.properties/application.yml加载进 Spring 环境后,本质上就是ResourcePropertySource或其变体在发挥作用。
3.3 兄弟实现:命令行与 Servlet 上下文
- CommandLinePropertySource(仓库 Spring-CommandLinePropertySource.md):同样继承
EnumerablePropertySource,但底层是命令行参数解析器而非 Map。其getProperty支持多值参数——例如命令行传入--foo=bar --foo=baz,以foo查询会得到逗号分隔的bar,baz;同时通过nonOptionArgs特例处理无选项参数。这说明了EnumerablePropertySource的"可枚举"接口能适配完全不同的底层数据结构。 - ServletContextPropertySource(仓库 Spring-ServletContextPropertySource.md):底层是
ServletContext接口,getProperty委托给getInitParameter、getPropertyNames委托给getInitParameterNames。Web 环境下的web.xml初始化参数正是经它进入 Spring 环境的。
四、实战印证:Spring Boot 启动链路中的 Map 型属性源
回到本仓库,Spring Boot 的启动加载文档 SpringBoot-application-load.md 中出现了MapPropertySource家族在生产环境中的真实用法:
propertySources.add(new OriginTrackedMapPropertySource(name + documentNumber, ...OriginTrackedMapPropertySource是 Spring Boot 对MapPropertySource的扩展子类(位于org.springframework.boot.env包),它在继承 Map 型属性源全部能力的同时,额外记录了每个属性的来源出处(如配置文件的具体路径与行列),用于启动失败时的诊断信息定位。从源码调用链可以推断,Spring Boot 解析application.properties时,会为每个配置文档(document)创建一个命名的 Map 型属性源,通过addLast/addFirst按优先级顺序加入MutablePropertySources,从而支撑@Value("${...}")、@ConfigurationProperties等注解的取值。
这也是 MapPropertySource 在整个框架中价值的缩影:它不直接参与业务,却是所有"键值对配置"进入 Spring 统一环境抽象的唯一通道。无论是系统属性(System.getProperties())、环境变量,还是*.properties文件、命令行参数,最终都会以 Map 形态被包装成某个MapPropertySource及其子类实例,挂载到Environment上。
五、小结
| 方法 | 底层实现 | 关键语义 |
|---|---|---|
getProperty(name) | source.get(name) | 未命中返回null,O(1) 查询 |
containsProperty(name) | source.containsKey(name) | 比基类默认实现更精确,value 为 null 也算存在 |
getPropertyNames() | StringUtils.toStringArray(source.keySet()) | 实现EnumerablePropertySource的可枚举契约 |
| 构造 | super(name, source) | 名称非空、Map 非空双重校验 |
MapPropertySource用最朴素的方式诠释了 Spring 的设计哲学:通过统一的PropertySource抽象 + 可枚举扩展点,将千差万别的配置来源收敛为同一种"名称 → 属性源 → 属性"的访问模型。理解它,是读懂 Spring 环境抽象(Environment、PropertyResolver、占位符解析)的第一步,也是排查"配置为何没生效""属性为何解析不到"这类问题的基础功。想继续深入,可以沿着本仓库 Spring-PropertySources.md 继续阅读MutablePropertySources的优先级排序机制,以及 PropertySource 家族其余文档 中的各实现类。
【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考