Spring 的 Environment 和 PropertySource 是什么?@Value 是怎么拿到配置值的?
简化版
Environment 是 Spring 对「运行环境」的抽象,管两件事:① Profiles(环境标识,如 dev/test/prod);② Properties(所有配置属性)。而所有的配置属性来自一个个 PropertySource(属性源)——每个 PropertySource 代表「一处配置来源」:命令行参数、系统环境变量、JVM 系统属性、application.properties/yml、@PropertySource 引入的文件……它们被 Environment 按优先级排成一个有序列表。@Value("${key}") 怎么拿到值:Spring 解析 ${key} 占位符时,去 Environment 里查这个 key —— Environment 按优先级依次问每个 PropertySource「你有这个 key 吗」,第一个有的就用它的值(所以命令行参数能覆盖配置文件,这就是「外部化配置」和「配置覆盖」的原理)。核心:Environment 是配置的「统一入口」,PropertySource 是「一处来源」,多个来源按优先级叠加,高优先级覆盖低优先级。
详细版
PropertySource 的优先级(Spring Boot,从高到低,高的覆盖低的):
优先级高(先查,能覆盖下面的)
① 命令行参数(--server.port=8081)
② SPRING_APPLICATION_JSON
③ Servlet 参数
④ JVM 系统属性(-Dkey=value)
⑤ 操作系统环境变量
⑥ application-{profile}.yml(激活的 profile 专属配置)
⑦ application.yml(通用配置)
⑧ @PropertySource 引入的
⑨ 默认属性(SpringApplication.setDefaultProperties)
优先级低(后查,容易被上面覆盖)
@Value 取值的链路:
@Value("${jdbc.url}")
↓
PropertySourcesPlaceholderConfigurer / 内嵌解析器 解析 ${jdbc.url}
↓
environment.getProperty("jdbc.url")
↓
遍历 PropertySources(按优先级):
命令行有 jdbc.url 吗?没有 →
系统属性有吗?没有 →
application.yml 有吗?有!→ 返回它的值
↓
把值注入到字段
// 三种拿配置的方式
@Value("${server.port:8080}") // ① @Value + 占位符(:8080 是默认值)
private int port;
@Autowired
private Environment env; // ② 注入 Environment 编程式取
// env.getProperty("server.port")
@ConfigurationProperties("app") // ③ 批量绑定(Spring Boot 推荐,见对应题)
⚠️ 理解「PropertySource 优先级 + 逐个查找、第一个命中就返回」是理解「配置覆盖」的关键。很多线上问题源于此:明明改了
application.yml,却不生效——因为有个更高优先级的来源(命令行参数、环境变量、-D系统属性)也定义了同一个 key,把配置文件的值覆盖了。排查配置问题的思路:先搞清这个 key 在哪几个 PropertySource 里都有值、谁的优先级最高。这套「多来源、按优先级叠加覆盖」的设计,正是 Spring Boot「外部化配置」(同一个 jar 在不同环境用不同配置、无需改代码)的底层机制。
完整版教学
一、Environment:运行环境的抽象
Environment 是 Spring 对「应用运行环境」的统一抽象,管两件事:
Environment 管两块:
① Profiles(环境标识):
- 当前激活哪些 profile(dev / test / prod)
- 决定哪些 Bean、哪些配置生效
- environment.getActiveProfiles()
② Properties(配置属性):
- 应用的所有配置(端口、数据库连接、各种开关)
- 来自多个 PropertySource
- environment.getProperty("key")
为什么要抽象出 Environment:
应用需要"感知运行环境"——现在是开发还是生产?配置值是什么?
Environment 把这些统一起来,代码通过它拿环境信息
→ 一个入口,屏蔽"配置到底存在哪"的细节
Environment 是「运行环境的统一抽象」——管 Profiles(当前激活哪些环境标识)和 Properties(所有配置属性)。它是应用「感知环境」的统一入口:代码不用关心配置到底存在命令行、环境变量还是配置文件,问 Environment 就行。理解「Environment 抽象运行环境、管 Profiles 和 Properties、是配置的统一入口屏蔽来源细节」,就理解了它的定位。
二、PropertySource:一处配置来源
配置属性从哪来?答案是一个个 PropertySource:
PropertySource = 一处配置来源的抽象
每个 PropertySource 代表一个"配置从哪来":
- CommandLinePropertySource:命令行参数
- SystemEnvironmentPropertySource:OS 环境变量
- PropertiesPropertySource:JVM 系统属性(-D)
- ResourcePropertySource:application.yml/properties 文件
- @PropertySource 引入的文件
- MapPropertySource:程序里塞的 Map
本质:每个 PropertySource 就是一个"名字 + 一批 key-value"
getProperty(key) → 从自己这批 key-value 里找
Environment 内部持有一个 MutablePropertySources
= 一个"有序的 PropertySource 列表"
PropertySource 是「一处配置来源的抽象」——命令行、环境变量、系统属性、配置文件各是一个 PropertySource,本质是「一批 key-value + 一个名字」。Environment 内部持有一个有序的 PropertySource 列表(MutablePropertySources)。这个设计把「不同来源的配置」统一成同一种接口,让 Environment 能统一处理。理解「PropertySource 是一处配置来源(命令行/环境变量/配置文件各一个)、本质是 key-value 集合、Environment 持有它们的有序列表」,就理解了配置来源的抽象。
三、优先级:多来源如何叠加覆盖
多个 PropertySource 存在时,靠「优先级」决定谁说了算:
Environment 里的 PropertySource 列表是"有序"的:
查 key 时,从头到尾遍历,第一个有这个 key 的就返回它的值
→ 排在前面的 = 优先级高 = 能覆盖后面的
Spring Boot 的典型优先级(高→低):
命令行参数 > 系统属性(-D) > 环境变量 > application-{profile}.yml
> application.yml > @PropertySource > 默认属性
覆盖的意义(外部化配置):
同一个 jar,代码不变:
开发:application.yml 里 port=8080
生产:启动加 --server.port=80,命令行覆盖配置文件
→ 一份构建产物,不同环境用不同配置,无需改代码重新打包
优先级是「多来源叠加覆盖」的核心——PropertySource 列表有序,查 key 时从高优先级到低优先级遍历,第一个命中就返回。所以命令行参数能覆盖配置文件。这正是「外部化配置」的底层机制:同一个 jar 不改代码,靠外部(命令行/环境变量)覆盖配置,就能在不同环境用不同配置。理解「PropertySource 有序、查 key 从高到低第一个命中返回、高优先级覆盖低优先级、是外部化配置的机制」,就理解了配置覆盖的原理。
四、@Value 取值的完整链路
把 @Value("${key}") 拿到值的完整过程串起来:
@Value("${jdbc.url}") 的处理链路:
1. Bean 创建时,AutowiredAnnotationBeanPostProcessor 处理 @Value
2. 拿到 "${jdbc.url}" 这个字符串
3. 用占位符解析器(PropertySourcesPlaceholderConfigurer 注册的)
解析 ${jdbc.url}:
→ 调 environment.getProperty("jdbc.url")
4. Environment 遍历 PropertySources(按优先级):
命令行有 jdbc.url?没有 → 系统属性有?没有 →
环境变量有?没有 → application.yml 有?有!返回值
5. 拿到值,做类型转换(String → 目标类型,见"类型转换"题)
6. 注入到字段
带默认值:@Value("${jdbc.url:jdbc:mysql://localhost}")
→ 冒号后是默认值,所有 PropertySource 都没有时用它
@Value("${key}") 的链路:BeanPostProcessor 处理 @Value → 占位符解析器解析 ${key} → 调 environment.getProperty(key) → Environment 按优先级遍历 PropertySource 找到值 → 类型转换 → 注入。冒号语法 ${key:default} 提供默认值(所有来源都没有时用)。所以 @Value 本质是「解析占位符 → 问 Environment → 注入」。理解「@Value 链路:BPP 处理→占位符解析器→environment.getProperty→按优先级找→类型转换→注入、${key:default} 提供默认值」,就理解了 @Value 取值的完整机制。
五、Profiles:环境标识与条件生效
Environment 的另一半职责是 Profiles(环境标识):
Profile 是"环境标签",用来让"不同环境用不同的 Bean/配置":
@Profile("dev") 这个 Bean 只在 dev 环境生效
@Profile("prod") 那个 Bean 只在 prod 环境生效
激活 profile:
- application.yml: spring.profiles.active=dev
- 命令行: --spring.profiles.active=prod
- 环境变量: SPRING_PROFILES_ACTIVE=prod
激活后:
① 对应的 application-{profile}.yml 生效(且优先级高于通用 application.yml)
② 标了 @Profile("该profile") 的 Bean 才被注册
典型用法:
dev 用 H2 内存库、prod 用 MySQL;dev 打开详细日志、prod 关掉
→ 靠 profile 切换,同一份代码适配多环境
Profiles 是「环境标识」——让不同环境(dev/test/prod)用不同的 Bean 和配置。激活某 profile 后,对应的 application-{profile}.yml 生效(优先级高于通用配置)、标了 @Profile("该profile") 的 Bean 才注册。这让「同一份代码适配多环境」(dev 用内存库、prod 用 MySQL)。理解「Profiles 是环境标识、@Profile 控制 Bean 按环境生效、application-{profile}.yml 按 profile 加载、实现多环境适配」,就理解了 Environment 的另一半职责。
六、编程式访问与配置绑定方式
除了 @Value,还有其他访问配置的方式,各有适用场景:
① @Value("${key}"):注入单个值
适合:零散的、单个配置项
缺点:配置多了要写一堆 @Value;类型转换、校验弱
② 注入 Environment 编程式取:
@Autowired Environment env;
env.getProperty("key"); env.getProperty("key", Integer.class);
适合:需要在代码逻辑里动态取配置、判断 profile
③ @ConfigurationProperties("app"):批量绑定到对象(Spring Boot 推荐)
把 app.xxx 一批配置绑定到一个 POJO 的字段
优点:类型安全、支持嵌套/List/Map、可校验(@Validated)、IDE 提示
适合:一组相关配置(如数据源配置、业务配置)
选择:单个值 @Value;一组配置 @ConfigurationProperties;
动态/条件判断 Environment
访问配置有三种方式:① @Value(注入单个值,适合零散配置);② 注入 Environment(编程式取,适合逻辑里动态取值、判断 profile);③ @ConfigurationProperties(批量绑定到 POJO,Spring Boot 推荐,类型安全、支持嵌套/校验)。它们底层都走 Environment → PropertySource。选择上:单值用 @Value、一组配置用 @ConfigurationProperties、动态判断用 Environment。理解「三种配置访问方式(@Value 单值/Environment 编程式/@ConfigurationProperties 批量绑定)及适用场景、底层都走 Environment」,就掌握了配置访问的全貌。
记忆钩子:「Environment = 运行环境抽象,管 Profiles(环境标识 dev/prod) + Properties(所有配置);配置来自多个 PropertySource(命令行/环境变量/系统属性/application.yml/@PropertySource 各是一个),Environment 持有它们的有序列表;@Value(${key}) 取值:占位符解析器→environment.getProperty→按优先级从高到低遍历 PropertySource 第一个命中就返回→类型转换→注入;优先级:命令行>系统属性>环境变量>profile配置>通用配置(高覆盖低=外部化配置/配置覆盖原理);${key:default} 给默认值;访问方式 @Value(单值)/Environment(编程式)/@ConfigurationProperties(批量绑定)」。
七、常见误区与追问
- 误区:改了 application.yml 配置就一定生效。 不一定——如果有更高优先级的 PropertySource(命令行参数、-D 系统属性、环境变量)也定义了同一个 key,会覆盖配置文件的值;排查配置不生效要先看这个 key 在哪些来源都有、谁优先级最高。
- 误区:@Value 和 Environment 是两套配置系统。 同一套——@Value 的 ${key} 底层就是调 environment.getProperty(key);Environment 是配置的统一入口,@Value 只是它的一个便捷注入方式。
- 误区:${} 里的值一定来自 application.yml。 ${} 会从所有 PropertySource 按优先级找——可能来自命令行、环境变量、系统属性、@PropertySource 文件,不只是 application.yml;配置文件只是众多来源之一。
- 误区:@Value 找不到 key 会用 null。 默认会抛异常(IllegalArgumentException: Could not resolve placeholder);要提供默认值得写
${key:defaultValue}(冒号后是默认值),或让所有来源至少有一个提供该 key。 - 追问:为什么命令行参数能覆盖配置文件? 因为 Environment 里的 PropertySource 是有序的、命令行参数的 PropertySource 排在最前(优先级最高);查 key 时从高到低遍历、第一个命中就返回,所以命令行的值先被找到,覆盖了后面配置文件的值——这是外部化配置的基础。
- 追问:@Value 和 @ConfigurationProperties 怎么选? 单个、零散的配置项用 @Value;一组相关配置(如数据源、业务参数)用 @ConfigurationProperties 批量绑定到 POJO——后者类型安全、支持嵌套/List/Map、可加 @Validated 校验、有 IDE 提示,是 Spring Boot 推荐方式。
- 追问:application-dev.yml 和 application.yml 都有同一个 key,用哪个? 激活 dev profile 时用 application-dev.yml 的(profile 专属配置优先级高于通用 application.yml);这样通用配置放 application.yml、环境差异放 application-{profile}.yml,profile 配置覆盖通用配置。
八、加强记忆
Environment 是 Spring 对「运行环境」的抽象,管两件事:① Profiles(环境标识 dev/test/prod,@Profile 控制 Bean 按环境生效);② Properties(所有配置属性)。配置属性来自一个个 PropertySource(属性源)——命令行参数、系统属性(-D)、环境变量、application.yml、@PropertySource 文件等各是一个 PropertySource(本质是 key-value 集合),Environment 持有它们的有序列表。@Value("${key}") 取值链路:BeanPostProcessor 处理 → 占位符解析器解析 → 调 environment.getProperty(key) → Environment 按优先级从高到低遍历 PropertySource、第一个命中就返回 → 类型转换 → 注入;${key:default} 提供默认值(否则找不到会抛异常)。优先级(高→低):命令行 > 系统属性 > 环境变量 > application-{profile}.yml > application.yml > @PropertySource;高优先级覆盖低优先级——这是「外部化配置/配置覆盖」的原理(同一 jar 不改代码,靠命令行/环境变量覆盖配置适配多环境;也是「改了配置文件不生效」的常见原因)。访问方式:@Value(单值)、注入 Environment(编程式/判断 profile)、@ConfigurationProperties(批量绑定 POJO、类型安全,Spring Boot 推荐)。一句话「Environment 是配置统一入口(管 Profiles+Properties),配置来自多个 PropertySource(命令行/环境变量/配置文件各一个)按优先级排序;@Value 的 ${key} 底层是 environment.getProperty,从高到低遍历第一个命中返回,高覆盖低=外部化配置」。