Spring源码 第八篇: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 核心接口与实现类

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.propertiesapplication.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)

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,核心是 ConfigDataEnvironmentPostProcessorEnvironmentPostProcessor 扩展点)在 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)

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

  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)

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

  1. 容器启动时,扫描所有带有 @PropertySource 的配置类;
  2. 根据注解中的路径,通过 ResourceLoader 加载对应的配置文件;
  3. 将配置文件解析为 PropertySource
  4. PropertySource 添加到 EnvironmentMutablePropertySources 中;
  5. 后续可通过 @ValueEnvironment 获取配置属性。

源码关键逻辑

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 完成解析。

核心流程

  1. 容器启动时,AutowiredAnnotationBeanPostProcessor 扫描所有带有 @Value 的字段 / 方法;
  2. 解析 @Value 中的表达式(如 ${server.port});
  3. 通过 Environment.getProperty()PropertySource 中获取对应属性值;
  4. 将获取到的属性值,通过反射注入到 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.postProcessBeforeInitializationafterPropertiesSet → 自定义 init-methodBeanPostProcessor.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.propertiesmy.name=spring
  • application-dev.properties 中 `my.name=spring-d
相关推荐
葫芦和十三1 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp1 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑2 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯3 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan5 小时前
多Agent之间的区别
后端
青石路7 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充7 小时前
1.面向对象设计思想
后端
IT_陈寒7 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro8 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗8 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端