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 小时前
一手实测 Claude Fable 5,手搓了个 Obsidian 的 Codex 插件
后端
zfoo-framework1 小时前
[修改代码使用]codex官方app中使用中转(不需要cc-switch) 1.config.toml 2.sk方式登录
java
逍遥德1 小时前
MQTT教程详解-05.SpringBoot集成mqtt client 性能分析
java·spring boot·spring·mt
云烟成雨TD1 小时前
Spring AI 1.x 系列【54】Retry 机制分析
java·人工智能·spring
weixin_523185321 小时前
Collections.unmodifiableMap详解:真的不可修改吗?
java·linux·前端
点燃大海1 小时前
SpringAI构建智能体
java·spring boot·spring·springai智能体
xier_ran1 小时前
【infra之路】02_RadixAttention与KV_Cache管理
java·spring boot·spring
swipe1 小时前
做多轮对话 Agent,为什么我建议把短期记忆放到 Redis
后端·面试·llm
黑马师兄2 小时前
RAG混合检索深度解析:让AI真正找到你要的内容
java·人工智能·ai·agent·rag·ai-native
码客日记2 小时前
Spring Boot 配置文件敏感信息加密(Jasypt 企业级完整方案)
java·spring boot·git