乐于分享
好东西不私藏

第八篇・Spring 5 源码深度拆解:Spring 资源加载与 Environment 环境体系

第八篇・Spring 5 源码深度拆解:Spring 资源加载与 Environment 环境体系

👋 前言

前面几篇,我们已经描述了 Spring 核心底层与 Spring Boot 自动配置:IOC → Bean 生命周期 → 循环依赖 → AOP → 事务 → Spring MVC → Spring Boot 自动配置

那么 我们平时使用的 application.properties/yml、@Value 注入的属性、配置文件里的参数,到底是怎么被 Spring 加载、解析、生效的?

本篇,我们拆解 Spring 资源加载与环境体系的完整逻辑:

  1. ResourceLoader、Resource 体系如何加载各类资源(文件、classpath、网络资源);
  2. properties/yml 配置文件的加载原理,Spring 如何读取配置内容;
  3. Environment、PropertySource 的加载顺序,谁的优先级更高;
  4. @PropertySource、@Value 注解的底层注入原理;
  5. 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. 获取资源的 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();}

常用实现类(实际开发 / 源码中高频出现)

实现类

作用

典型场景

ClassPathResource

加载 classpath 下的资源

加载 resources 目录下的配置文件

FileSystemResource

加载本地文件系统的资源

加载本地磁盘上的文件(如 D:/config/application.properties)

UrlResource

加载网络资源(HTTP/FTP 等)

加载 http://xxx.com/config.properties

ByteArrayResource

加载字节数组资源

内存中的字节流资源(如动态生成的配置)

ServletContextResource

加载 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);        }    }}

关键逻辑(可直接断点验证)

  1. 如果路径以 classpath: 开头,创建 ClassPathResource(加载 classpath 资源);
  2. 如果路径是 URL 格式(如 http://file://),创建 UrlResource(加载网络 / 本地文件资源);
  3. 否则,按普通路径处理,默认创建 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 的实现,它管理着两类核心配置源:

  1. systemProperties:系统属性(如 System.getProperties());
  2. 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 {    @Override    protected void customizePropertySources(MutablePropertySources propertySources) {        // 添加系统属性 PropertySource        propertySources.addLast(new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAMEgetSystemProperties()));        // 添加系统环境变量 PropertySource        propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAMEgetSystemEnvironment()));    }}

五、PropertySource 加载顺序(面试高频)

不同来源的 PropertySource,加载顺序不同,后加载的 PropertySource 优先级更高(后加载的会覆盖先加载的同名属性)。

1. 纯 Spring 环境(无 Spring Boot)

加载顺序 从上面的源码看(从高到低,优先级递增):

  1. 系统属性(systemProperties);
  2. 系统环境变量(systemEnvironment)。

2. Spring Boot 环境(2.7.x)

加载顺序(从低到高,优先级递增,重点记):

  1. defaultProperties:默认属性(通过 SpringApplication.setDefaultProperties 设置);
  2. @PropertySource 加载的自定义配置文件;
  3. 配置文件(application.properties/yml):按路径优先级(classpath:/META-INF/config/ < classpath:/ < file:./config/ < file:./);
  4. 带 profile 的配置文件(application-{profile}.properties/yml);
  5. 系统属性(如 System.setProperty("server.port", "8081"));
  6. 系统环境变量(如 OS 的环境变量);
  7. 命令行参数(高优先级,会覆盖前面所有同名属性)。

关键结论

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),核心流程:

  1. 容器启动时,扫描所有带有 @PropertySource 的配置类;
  2. 根据注解中的路径,通过 ResourceLoader 加载对应的配置文件;
  3. 将配置文件解析为 PropertySource
  4. 将 PropertySource 添加到 Environment 的 MutablePropertySources 中;
  5. 后续可通过 @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 完成解析。

核心流程

  1. 容器启动时,AutowiredAnnotationBeanPostProcessor 扫描所有带有 @Value 的字段 / 方法;
  2. 解析 @Value 中的表达式(如 ${server.port});
  3. 通过 Environment.getProperty() 从 PropertySource 中获取对应属性值;
  4. 将获取到的属性值,通过反射注入到 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 配置优先级实战

结合前面的加载顺序,举几个实战场景,帮你彻底理解优先级:

  1. 命令行参数覆盖配置文件:
    • 配置文件中 server.port=8080
    • 启动命令 java -jar xxx.jar --server.port=8081
    • 最终生效端口:8081(命令行参数优先级更高)。
  1. 系统属性覆盖环境变量:
    • 系统环境变量 SERVER_PORT=8082
    • 代码中 System.setProperty("server.port", "8083")
    • 最终生效端口:8083(系统属性优先级更高)。
  1. 带 profile 配置覆盖普通配置:

   ○ application.properties 中 my.name=spring;

   ○ application-dev.properties 中 my.name=spring-dev;

   ○ 激活 dev 环境后,最终生效值:spring-dev(profile 配置优先级更高)。

  1. 自定义配置文件与默认配置文件:
    • application.properties 中 my.name=spring
    • @PropertySource("classpath:custom.properties") 中 my.name=springboot
    • 最终生效值:springboot(自定义配置文件后加载,优先级更高)。

八、总结

  1. 资源加载:Resource 抽象所有资源,ResourceLoader 负责加载,ApplicationContext 本身就是 ResourceLoader;
  2. 配置文件加载:Spring Boot 中 ConfigDataEnvironmentPostProcessor(EnvironmentPostProcessor)在 prepareEnvironment 阶段加载默认路径的 properties/yml;
  3. Environment:统一管理所有配置,底层由 PropertySource 集合存储,按顺序遍历获取属性;
  4. 配置优先级:命令行参数 > 系统环境变量 > 系统属性 > 带 profile 配置文件 > 普通配置文件 > @PropertySource > 默认属性;
  5. @PropertySource:通过 ConfigurationClassPostProcessor加载自定义配置,添加到 Environment;
  6. @Value:通过 AutowiredAnnotationBeanPostProcessor 解析表达式,从 Environment 中获取属性并注入。

九、高频面试题

  1. 简述 Spring 的 Resource 体系?

答:Spring 用 Resource 接口统一抽象所有资源(classpath、本地文件、网络资源等),常用实现类有 ClassPathResourceFileSystemResource 等;ResourceLoader 负责加载资源,ApplicationContext 继承了 ResourceLoader,可直接加载资源。

  1. Spring Boot 如何加载 application.properties/yml?

答:Spring Boot 启动时在 prepareEnvironment 阶段,通过 ConfigDataEnvironmentPostProcessor(标准 EnvironmentPostProcessor 扩展点)加载默认路径(classpath:/、file:./config/ 等)的配置文件,通过对应的加载器解析为 PropertySource,添加到 Environment 中。

  1. Spring Boot 配置优先级顺序(从高到低)?

答:命令行参数 > 系统环境变量 > 系统属性 > 带 profile 配置文件 > 普通配置文件 > @PropertySource > 默认属性

  1. @PropertySource 和 @Value 的作用及原理?

答:@PropertySource 用于加载自定义配置文件,是由 ConfigurationClassPostProcessor 在容器刷新时解析,并将加载的 PropertySource 放入 Environment 的 PropertySources 中@Value 用于注入配置属性,由 AutowiredAnnotationBeanPostProcessor 解析表达式,从 Environment 中获取属性并通过反射注入。

  1. Environment 是什么?它的核心作用是什么?

答:Environment 是 Spring 环境体系的核心接口,统一管理所有配置属性(配置文件、系统属性、环境变量等),核心作用是提供属性获取、环境判断等功能,是 Spring 中配置的统一入口。

下篇预告

第九篇:Spring 事件驱动模型(ApplicationEvent)

  • 事件发布 / 监听完整流程
  • ApplicationEvent、ApplicationListener 核心接口
  • @EventListener 注解解析原理
  • 异步事件、事务绑定事件的实现
  • Spring 内置启动 / 关闭事件详解