第八篇・Spring 5 源码深度拆解:Spring 资源加载与 Environment 环境体系
👋 前言
前面几篇,我们已经描述了 Spring 核心底层与 Spring Boot 自动配置:IOC → Bean 生命周期 → 循环依赖 → AOP → 事务 → Spring MVC → Spring Boot 自动配置
那么 我们平时使用的 application.properties/yml、@Value 注入的属性、配置文件里的参数,到底是怎么被 Spring 加载、解析、生效的?
本篇,我们拆解 Spring 资源加载与环境体系的完整逻辑:
- ResourceLoader、Resource 体系如何加载各类资源(文件、classpath、网络资源);
- properties/yml 配置文件的加载原理,Spring 如何读取配置内容;
- Environment、PropertySource 的加载顺序,谁的优先级更高;
- @PropertySource、@Value 注解的底层注入原理;
- Spring Boot 配置优先级,不同配置方式的生效顺序。
了解 “配置从哪来” 的底层逻辑,让我们不仅会用配置,更懂配置的底层实现。
一、核心基础:Resource 体系(资源的统一抽象)
Spring 为了统一管理所有类型的资源(本地文件、classpath 资源、网络资源、字节流等),定义了 Resource 接口,它是 Spring 资源加载的核心抽象,类比 Java 的 File,但功能更强大、更灵活。
1. Resource 核心接口与实现类
public interface InputStreamSource {// 核心:获取资源的输入流(每次调用返回新的输入流)InputStream getInputStream() throws IOException;}
public interface Resource extends InputStreamSource {// 1. 核心:判断资源是否存在(最常用)boolean exists();// 2. 获取资源的 URL(如 classpath:/application.properties → URL 对象)URL getURL() throws IOException;// 3. 获取资源的 URIURI getURI() throws IOException;// 4. 获取本地文件对象(仅本地资源可用,如 FileSystemResource)File getFile() throws IOException;// 5. 获取资源的描述(用于日志、异常信息,方便定位资源)String getDescription();// 6. 创建相对路径的资源(如基于当前资源,获取同级下的其他文件)Resource createRelative(String relativePath) throws IOException;// 7. 获取资源文件名(可为null,如字节流资源无文件名)@NullableString getFilename();}
常用实现类(实际开发 / 源码中高频出现)
|
实现类 |
作用 |
典型场景 |
|
|
加载 classpath 下的资源 |
加载 resources 目录下的配置文件 |
|
|
加载本地文件系统的资源 |
加载本地磁盘上的文件(如 D:/config/application.properties) |
|
|
加载网络资源(HTTP/FTP 等) |
加载 http://xxx.com/config.properties |
|
|
加载字节数组资源 |
内存中的字节流资源(如动态生成的配置) |
|
|
加载 Web 应用下的资源 |
Web 环境中,加载 WEB-INF 下的资源 |
2. 核心结论
Spring 对所有资源的操作,都通过 Resource 接口统一封装,无论资源来自哪里,都能通过相同的 API 操作(获取输入流、判断是否存在等),这是 Spring 资源加载的核心设计思想。
二、资源加载的核心:ResourceLoader 体系
有了 Resource 对资源的抽象,还需要一个 “加载器” 来获取这些资源 ——ResourceLoader 接口,它是 Spring 资源加载的核心入口,负责根据资源路径,创建对应的 Resource 实例。
1. ResourceLoader 核心接口
public interface ResourceLoader {// 加载资源的核心方法:根据路径返回 Resource 实例Resource getResource(String location);// 获取类加载器ClassLoader getClassLoader();// 常量:classpath 资源前缀String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX; // "classpath:"}
2. 核心实现类:DefaultResourceLoader
Spring 容器(ApplicationContext)的父类 DefaultResourceLoader,实现了 ResourceLoader 接口,是 Spring 资源加载的默认实现。
核心源码(DefaultResourceLoader#getResource)
@Overridepublic Resource getResource(String location) {Assert.notNull(location, "Location must not be null");// 1. 优先处理带协议的路径(如 classpath:、file:、http:)if (location.startsWith("/")) {return getResourceByPath(location);} else if (location.startsWith(CLASSPATH_URL_PREFIX)) {// 2. 处理 classpath: 前缀的资源return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());} else {try {// 3. 尝试解析为 URL(如 http:、file:)URL url = new URL(location);return new UrlResource(url);} catch (MalformedURLException ex) {// 4. 以上都不是,按文件路径处理(默认)return getResourceByPath(location);}}}
关键逻辑(可直接断点验证)
- 如果路径以
classpath:开头,创建ClassPathResource(加载 classpath 资源); - 如果路径是 URL 格式(如
http://、file://),创建UrlResource(加载网络 / 本地文件资源); - 否则,按普通路径处理,默认创建
ClassPathResource
3. ApplicationContext 与 ResourceLoader 的关系
所有 ApplicationContext 的实现类都间接继承了 DefaultResourceLoader(因为 AbstractApplicationContext 继承自 DefaultResourceLoader),因此 Spring 容器本身就是一个 ResourceLoader。
我们可以直接通过容器获取资源:
// 示例:通过 Spring 容器加载 classpath 下的配置文件ApplicationContext context = new AnnotationConfigApplicationContext();Resource resource = context.getResource("classpath:application.properties");
三、配置文件加载原理(properties/yml)
我们日常开发中,最常用的资源就是 application.properties 和 application.yml,这一部分重点拆解:Spring 如何加载这些配置文件,如何将配置内容解析为可使用的属性。
1. 核心前提:Spring Boot 自动配置的加持
Spring 本身(纯 Spring)不会自动加载 application.properties/yml,是 Spring Boot 自动配置 帮我们做了这件事,核心类是 ConfigDataEnvironmentPostProcessor。
2. 配置文件加载完整流程(Spring Boot 2.7.x)
1. SpringApplication#run() 启动2. 进入 prepareEnvironment 阶段,准备应用环境3. 触发 EnvironmentPostProcessor 扩展点,执行所有实现类4. ConfigDataEnvironmentPostProcessor 作为核心实现,开始加载配置;5. 扫描默认路径(classpath:/、classpath:/META-INF/config/、file:./、file:./config/ 等)6. 按格式(properties/yml)调用对应加载器解析7. 封装为 PropertySource 加入 Environment
3. 核心源码(ConfigFileApplicationListener#load)
private void load(ConfigurableEnvironment environment, ResourceLoader resourceLoader,DeferredLog log, String[] profiles) {// 创建配置文件加载器ConfigFileApplicationListener.Loader loader = new ConfigFileApplicationListener.Loader(environment, resourceLoader, log);// 加载配置文件(默认加载 application.properties/yml)loader.load();}
配置文件解析细节
- properties 文件:通过
PropertiesPropertySourceLoader解析,将键值对存入Properties,再封装为PropertiesPropertySource; - yml 文件:通过
YamlPropertySourceLoader解析,将 YAML 结构转为 Map,再封装为MapPropertySource; - 两种格式的配置,最终都会被添加到
Environment中,统一管理。
4. 关键结论
Spring Boot 2.7.x 会自动加载默认路径下的 application.properties/yml,核心是 ConfigDataEnvironmentPostProcessor(EnvironmentPostProcessor 扩展点)在 prepareEnvironment 阶段执行,通过对应的加载器解析配置文件,最终将配置属性注入到 Environment 中。
四、Environment 环境体系(配置的统一管理)
Environment 是 Spring 环境体系的核心接口,它统一管理所有配置属性(配置文件、系统属性、环境变量、命令行参数等),提供了获取属性、判断环境等核心功能,是 Spring 中 “配置的入口”。
1. Environment 核心接口
public interface Environment extends PropertyResolver {// 获取当前激活的环境(如 dev、test、prod)String[] getActiveProfiles();// 获取默认激活的环境String[] getDefaultProfiles();// 判断当前环境是否包含指定的环境boolean acceptsProfiles(Profiles profiles);}
它继承了 PropertyResolver 接口,核心方法(获取配置属性):
// 获取指定 key 的属性值String getProperty(String key);// 获取指定 key 的属性值,指定默认值<T> T getProperty(String key, Class<T> type, T defaultValue);// 必须获取到属性值,否则抛异常String getRequiredProperty(String key) throws IllegalStateException;
2. 核心实现类:StandardEnvironment
Spring 默认使用 StandardEnvironment 作为 Environment 的实现,它管理着两类核心配置源:
systemProperties:系统属性(如System.getProperties());systemEnvironment:系统环境变量(如操作系统的环境变量)。
Spring Boot 中,会使用 StandardServletEnvironment(继承自 StandardEnvironment),额外添加了 servletConfigInitParams(Servlet 配置参数)、servletContextInitParams(ServletContext 配置参数)。
3. PropertySource:配置的底层存储
Environment 中的所有配置,都存储在 PropertySource 中 ——PropertySource 是配置的 “最小单元”,本质是一个键值对集合(类似 Map)。
核心逻辑
Environment内部维护一个MutablePropertySources对象,它是PropertySource的集合;- 所有配置(配置文件、系统属性、环境变量等),都会被封装为
PropertySource,添加到这个集合中; - 获取属性时,会按顺序遍历这个集合,找到第一个匹配 key 的属性值(优先级由此决定)。
源码级体现(StandardEnvironment 初始化)
public class StandardEnvironment extends AbstractEnvironment {@Overrideprotected void customizePropertySources(MutablePropertySources propertySources) {// 添加系统属性 PropertySourcepropertySources.addLast(new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));// 添加系统环境变量 PropertySourcepropertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));}}
五、PropertySource 加载顺序(面试高频)
不同来源的 PropertySource,加载顺序不同,后加载的 PropertySource 优先级更高(后加载的会覆盖先加载的同名属性)。
1. 纯 Spring 环境(无 Spring Boot)
加载顺序 从上面的源码看(从高到低,优先级递增):
- 系统属性(systemProperties);
- 系统环境变量(systemEnvironment)。
2. Spring Boot 环境(2.7.x)
加载顺序(从低到高,优先级递增,重点记):
defaultProperties:默认属性(通过SpringApplication.setDefaultProperties设置);- @PropertySource 加载的自定义配置文件;
- 配置文件(application.properties/yml):按路径优先级(classpath:/META-INF/config/ < classpath:/ < file:./config/ < file:./);
- 带 profile 的配置文件(application-{profile}.properties/yml);
- 系统属性(如
System.setProperty("server.port", "8081")); - 系统环境变量(如 OS 的环境变量);
- 命令行参数(高优先级,会覆盖前面所有同名属性)。
关键结论
Spring Boot 配置优先级:命令行参数 > 系统环境变量 >系统属性 > 带 profile 配置文件 > 普通配置文件 > @PropertySource 自定义配置 > 默认属性
3. 源码验证(MutablePropertySources)
// MutablePropertySources 是 PropertySource 的集合,维护加载顺序private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();// 添加 PropertySource 时,可指定位置(addFirst 优先级最高,addLast 优先级最低)publicvoidaddFirst(PropertySource<?> propertySource) {removeIfPresent(propertySource);this.propertySourceList.add(0, propertySource);}publicvoidaddLast(PropertySource<?> propertySource) {removeIfPresent(propertySource);this.propertySourceList.add(propertySource);}
六、@PropertySource、@Value 注入原理
我们常用 @PropertySource 加载自定义配置文件,用 @Value 注入配置属性,这一部分拆解其底层实现。
1. @PropertySource 原理(加载自定义配置文件)
注解源码
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface PropertySource {// 配置文件路径(支持 classpath: 前缀)String[] value();// 配置文件编码(如 UTF-8)String encoding() default "";// 忽略配置文件不存在的情况boolean ignoreResourceNotFound() default false;}
底层实现
@PropertySource 的解析,由 ConfigurationClassPostProcessor完成(一个 BeanFactoryPostProcessor),核心流程:
- 容器启动时,扫描所有带有
@PropertySource的配置类; - 根据注解中的路径,通过
ResourceLoader加载对应的配置文件; - 将配置文件解析为
PropertySource; - 将
PropertySource添加到Environment的MutablePropertySources中; - 后续可通过
@Value或Environment获取配置属性。
源码关键逻辑
// ConfigurationClassPostProcessor 需要进一步debug跟踪解析publicvoidpostProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {int registryId = System.identityHashCode(registry);if (this.registriesPostProcessed.contains(registryId)) {throw new IllegalStateException("postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);}if (this.factoriesPostProcessed.contains(registryId)) {throw new IllegalStateException("postProcessBeanFactory already called on this post-processor against " + registry);}this.registriesPostProcessed.add(registryId);processConfigBeanDefinitions(registry);}
2. @Value 注入原理
@Value 用于将 Environment 中的属性值注入到 Bean 的字段或方法参数中,底层由 AutowiredAnnotationBeanPostProcessor 完成解析。
核心流程
- 容器启动时,
AutowiredAnnotationBeanPostProcessor扫描所有带有@Value的字段 / 方法; - 解析
@Value中的表达式(如${server.port}); - 通过
Environment.getProperty()从PropertySource中获取对应属性值; - 将获取到的属性值,通过反射注入到 Bean 的字段中,或作为方法参数传入。
源码关键逻辑(AutowiredAnnotationBeanPostProcessor)
// 解析 @Value 注解private Object resolveFieldValue(Field field, Object bean, String beanName) {// 获取 @Value 注解的值(如 "${server.port}")String value = field.getAnnotation(Value.class).value();// 解析表达式,从 Environment 中获取属性值Object resolvedValue = resolveEmbeddedValue(value);// 类型转换(将 String 类型的属性值,转为字段的实际类型)resolvedValue = convertForField(resolvedValue, field);// 反射注入到字段ReflectionUtils.setField(field, bean, resolvedValue);return resolvedValue;}
关键注意点
@Value注入的属性,必须在Environment中存在(否则会抛异常,除非指定默认值,如${server.port:8080});- 注入时机:注解的处理时机是在
postProcessProperties()阶段,该方法在 Bean 的属性填充过程中被调用。 - 生命周期顺序:实例化 → 属性填充(处理
@Value、@Autowired等)→BeanPostProcessor.postProcessBeforeInitialization→afterPropertiesSet→ 自定义init-method→BeanPostProcessor.postProcessAfterInitialization
七、Spring Boot 配置优先级实战
结合前面的加载顺序,举几个实战场景,帮你彻底理解优先级:
- 命令行参数覆盖配置文件:
- 配置文件中
server.port=8080; - 启动命令
java -jar xxx.jar --server.port=8081; - 最终生效端口:8081(命令行参数优先级更高)。
- 系统属性覆盖环境变量:
- 系统环境变量
SERVER_PORT=8082; - 代码中
System.setProperty("server.port", "8083"); - 最终生效端口:8083(系统属性优先级更高)。
- 带 profile 配置覆盖普通配置:
○ application.properties 中 my.name=spring;
○ application-dev.properties 中 my.name=spring-dev;
○ 激活 dev 环境后,最终生效值:spring-dev(profile 配置优先级更高)。
- 自定义配置文件与默认配置文件:
application.properties中my.name=spring;@PropertySource("classpath:custom.properties")中my.name=springboot;- 最终生效值:
springboot(自定义配置文件后加载,优先级更高)。
八、总结
- 资源加载:
Resource抽象所有资源,ResourceLoader负责加载,ApplicationContext本身就是 ResourceLoader; - 配置文件加载:Spring Boot 中 ConfigDataEnvironmentPostProcessor(EnvironmentPostProcessor)在 prepareEnvironment 阶段加载默认路径的 properties/yml;
- Environment:统一管理所有配置,底层由
PropertySource集合存储,按顺序遍历获取属性; - 配置优先级:命令行参数 > 系统环境变量 > 系统属性 > 带 profile 配置文件 > 普通配置文件 > @PropertySource > 默认属性;
- @PropertySource:通过
ConfigurationClassPostProcessor加载自定义配置,添加到 Environment; - @Value:通过
AutowiredAnnotationBeanPostProcessor解析表达式,从 Environment 中获取属性并注入。
九、高频面试题
- 简述 Spring 的 Resource 体系?
答:Spring 用 Resource 接口统一抽象所有资源(classpath、本地文件、网络资源等),常用实现类有 ClassPathResource、FileSystemResource 等;ResourceLoader 负责加载资源,ApplicationContext 继承了 ResourceLoader,可直接加载资源。
- Spring Boot 如何加载 application.properties/yml?
答:Spring Boot 启动时在 prepareEnvironment 阶段,通过 ConfigDataEnvironmentPostProcessor(标准 EnvironmentPostProcessor 扩展点)加载默认路径(classpath:/、file:./config/ 等)的配置文件,通过对应的加载器解析为 PropertySource,添加到 Environment 中。
- Spring Boot 配置优先级顺序(从高到低)?
答:命令行参数 > 系统环境变量 > 系统属性 > 带 profile 配置文件 > 普通配置文件 > @PropertySource > 默认属性
- @PropertySource 和 @Value 的作用及原理?
答:@PropertySource 用于加载自定义配置文件,是由 ConfigurationClassPostProcessor 在容器刷新时解析,并将加载的 PropertySource 放入 Environment 的 PropertySources 中;@Value 用于注入配置属性,由 AutowiredAnnotationBeanPostProcessor 解析表达式,从 Environment 中获取属性并通过反射注入。
- Environment 是什么?它的核心作用是什么?
答:Environment 是 Spring 环境体系的核心接口,统一管理所有配置属性(配置文件、系统属性、环境变量等),核心作用是提供属性获取、环境判断等功能,是 Spring 中配置的统一入口。
下篇预告
第九篇:Spring 事件驱动模型(ApplicationEvent)
- 事件发布 / 监听完整流程
- ApplicationEvent、ApplicationListener 核心接口
- @EventListener 注解解析原理
- 异步事件、事务绑定事件的实现
- Spring 内置启动 / 关闭事件详解
夜雨聆风