👋 前言
前面几篇,我们已经描述了 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 核心接口与实现类
java
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. 获取资源的 URI
URI getURI() throws IOException;
// 4. 获取本地文件对象(仅本地资源可用,如 FileSystemResource)
File getFile() throws IOException;
// 5. 获取资源的描述(用于日志、异常信息,方便定位资源)
String getDescription();
// 6. 创建相对路径的资源(如基于当前资源,获取同级下的其他文件)
Resource createRelative(String relativePath) throws IOException;
// 7. 获取资源文件名(可为null,如字节流资源无文件名)
@Nullable
String getFilename();
}
2. 常用实现类(实际开发 / 源码中高频出现)
| 实现类 | 作用 | 典型场景 |
|---|---|---|
| ClassPathResource | 加载 classpath 下的资源 | 加载 resources 目录下的配置文件 |
| FileSystemResource | 加载本地文件系统的资源 | 加载本地磁盘上的文件(如 D:/config/application.properties) |
| UrlResource | 加载网络资源(HTTP/FTP 等) | 加载 http://xxx.com/config.properties |
| ByteArrayResource | 加载字节数组资源 | 内存中的字节流资源(如动态生成的配置) |
| ServletContextResource | 加载 Web 应用下的资源 | Web 环境中,加载 WEB-INF 下的资源 |
3. 核心结论
Spring 对所有资源的操作,都通过 Resource 接口统一封装,无论资源来自哪里,都能通过相同的 API 操作(获取输入流、判断是否存在等),这是 Spring 资源加载的核心设计思想。
二、资源加载的核心:ResourceLoader 体系
有了 Resource 对资源的抽象,还需要一个"加载器"来获取这些资源 ------ ResourceLoader 接口,它是 Spring 资源加载的核心入口,负责根据资源路径,创建对应的 Resource 实例。
1. ResourceLoader 核心接口
java
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)
java
@Override
public 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。
我们可以直接通过容器获取资源:
java
// 示例:通过 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)
SpringApplication#run()启动- 进入
prepareEnvironment阶段,准备应用环境 - 触发
EnvironmentPostProcessor扩展点,执行所有实现类 ConfigDataEnvironmentPostProcessor作为核心实现,开始加载配置;- 扫描默认路径(
classpath:/、classpath:/META-INF/config/、file:./、file:./config/等) - 按格式(properties/yml)调用对应加载器解析
- 封装为
PropertySource加入Environment
3. 核心源码(ConfigFileApplicationListener#load)
java
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 核心接口
java
public interface Environment extends PropertyResolver {
// 获取当前激活的环境(如 dev、test、prod)
String[] getActiveProfiles();
// 获取默认激活的环境
String[] getDefaultProfiles();
// 判断当前环境是否包含指定的环境
boolean acceptsProfiles(Profiles profiles);
}
它继承了 PropertyResolver 接口,核心方法(获取配置属性):
java
// 获取指定 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 初始化)
java
public class StandardEnvironment extends AbstractEnvironment {
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
// 添加系统属性 PropertySource
propertySources.addLast(new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
// 添加系统环境变量 PropertySource
propertySources.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)
java
// MutablePropertySources 是 PropertySource 的集合,维护加载顺序
private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
// 添加 PropertySource 时,可指定位置(addFirst 优先级最高,addLast 优先级最低)
public void addFirst(PropertySource<?> propertySource) {
removeIfPresent(propertySource);
this.propertySourceList.add(0, propertySource);
}
public void addLast(PropertySource<?> propertySource) {
removeIfPresent(propertySource);
this.propertySourceList.add(propertySource);
}
六、@PropertySource、@Value 注入原理
我们常用 @PropertySource 加载自定义配置文件,用 @Value 注入配置属性,这一部分拆解其底层实现。
1. @PropertySource 原理(加载自定义配置文件)
注解源码
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @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获取配置属性。
源码关键逻辑
java
// ConfigurationClassPostProcessor 需要进一步debug跟踪解析
public void postProcessBeanDefinitionRegistry(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)
java
// 解析 @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 配置优先级实战
结合前面的加载顺序,举几个实战场景,帮你彻底理解优先级:
场景 1:命令行参数覆盖配置文件
- 配置文件中
server.port=8080; - 启动命令
java -jar xxx.jar --server.port=8081; - 最终生效端口:8081(命令行参数优先级更高)。
场景 2:系统属性覆盖环境变量
- 系统环境变量
SERVER_PORT=8082; - 代码中
System.setProperty("server.port", "8083"); - 最终生效端口:8083(系统属性优先级更高)。
场景 3:带 profile 配置覆盖普通配置
application.properties中my.name=spring;application-dev.properties中 `my.name=spring-d