如果这篇文章对你有帮助,欢迎关注我的CSDN账号「来福猿」, 有问题可以在评论区留言,我会一一回复。1. 引言
SpringBoot 的「约定优于配置」让开发者可以快速启动一个应用,但真正理解配置加载机制,才能在多环境部署、配置覆盖和排障时做到心中有数。在 SpringBoot 应用启动时,配置可能来自application.yml、命令行参数、环境变量、系统属性、随机值等多个来源。
本文以 SpringBoot 2.x/3.x 的配置体系为背景,从源码角度梳理配置加载流程,重点分析 application.yml、命令行参数与环境变量三者的加载顺序、优先级以及背后的核心类。文中示例保持与 SpringBoot 原生技术栈一致,便于读者直接对照源码调试。
2. 配置来源与优先级
SpringBoot 通过PropertySource来统一表示不同的配置来源,最终把它们聚合到Environment中。常见的配置来源包括:
- 开发者工具中的全局配置
- 测试环境中的
@TestPropertySource - 命令行参数,例如
--server.port=8081 - SPRING_APPLICATION_JSON中的 JSON 属性
- ServletConfig初始化参数
- ServletContext初始化参数
- JNDI 属性
- Java 系统属性
- 操作系统环境变量
- 随机值
- 应用外部配置,例如
application-dev.yml - 应用内部配置,例如
application.yml - @PropertySource 注解加载的配置
- 默认属性,例如
SpringApplication.setDefaultProperties
从优先级角度看,命令行参数优先级最高,其次是系统属性、环境变量,而应用内部的application.yml通常位于靠后的位置。也就是当多个来源同时配置同一个 key 时,靠前的来源会覆盖靠后的来源。例如:
java -jar app.jar --server.port=9090即使application.yml中写了server.port: 8080,最终生效的端口仍然是9090,因为命令行参数的优先级高于配置文件。
3. 源码入口:SpringApplication 与环境准备
SpringBoot 应用的启动入口是SpringApplication.run方法。在run方法内部,会先执行prepareEnvironment,完成ConfigurableEnvironment的创建和配置源装配。
public ConfigurableApplicationContext run(String... args) { // ... ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); // ... }prepareEnvironment的核心逻辑如下:
private ConfigurableEnvironment prepareEnvironment( SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) { ConfigurableEnvironment environment = getOrCreateEnvironment(); configureEnvironment(environment, applicationArguments.getSourceArgs()); ConfigurationPropertySources.attach(environment); listeners.environmentPrepared(environment); // ... return environment; }其中configureEnvironment会进一步进行属性源配置,并把命令行参数转成PropertySource加入环境。
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) { if (this.addConversionService) { environment.setConversionService(new ApplicationConversionService()); } configurePropertySources(environment, args); configureProfiles(environment, args); }在这一阶段,Environment只完成了基础属性源装载,而application.yml等配置文件尚未被解析加载。
4. 命令行参数解析:ApplicationArguments 与 SimpleCommandLineArgsParser
命令行参数是优先级最高的运营期配置来源,适合通过部署脚本动态覆盖端口、日志级别等参数。SpringBoot 使用ApplicationArguments来统一表示命令行输入。
默认实现是DefaultApplicationArguments,它内部调用SimpleCommandLineArgsParser完成解析。解析结果分为两类:
- 选项参数:以
--开头,例如--server.port=8081。 - 非选项参数:不以
--开头的普通参数。
选项参数在后续会被包装为CommandLinePropertySource加入Environment。例如下面的启动命令:
java -jar app.jar --server.port=9090 --logging.level.root=debug会被解析成两组属性:
server.port=9090 logging.level.root=debug从源码看,SpringApplication.configurePropertySources会执行如下逻辑:
protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) { MutablePropertySources sources = environment.getPropertySources(); if (!CollectionUtils.isEmpty(this.defaultProperties)) { DefaultPropertiesPropertySource.addOrMerge(this.defaultProperties, sources); } if (this.addCommandLineProperties && args.length > 0) { String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME; if (sources.contains(name)) { PropertySource<?> source = sources.get(name); CompositePropertySource composite = new CompositePropertySource(name); composite.addPropertySource( new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args)); composite.addPropertySource(source); sources.replace(name, composite); } else { sources.addFirst(new SimpleCommandLinePropertySource(args)); } } }可以看到,命令行生成的SimpleCommandLinePropertySource通过addFirst被放到属性源列表的最前面,这决定了它拥有最高优先级。
5. 操作系统环境变量:SystemEnvironmentPropertySource
环境变量是容器化部署和云平台中最常用的配置注入方式,例如 Kubernetes 的env、Docker 的-e参数。SpringBoot 在创建StandardServletEnvironment或StandardEnvironment时,会把操作系统的环境变量包装成PropertySource。
StandardEnvironment的构造过程如下:
public class StandardEnvironment extends AbstractEnvironment { public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment"; public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties"; @Override protected void customizePropertySources(MutablePropertySources propertySources) { propertySources.addLast( new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties())); propertySources.addLast( new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment())); } }在 Servlet 环境中,StandardServletEnvironment还会在更靠前的位置加入servletConfigInitParams和servletContextInitParams。
SystemEnvironmentPropertySource有一个重要特性:它支持宽松绑定,可以把环境变量名中的下划线自动映射成点号语法。例如环境变量SERVER_PORT可以直接匹配属性server.port。这一能力来自SystemEnvironmentPropertySource.resolvePropertyName方法。
@Override protected final String resolvePropertyName(String name) { Assert.notNull(name, "Property name must not be null"); String resolvedName = checkPropertyName(name); if (resolvedName != null) { return resolvedName; } String uppercasedName = name.toUpperCase(); if (!name.equals(uppercasedName)) { String result = checkPropertyName(uppercasedName); if (result != null) { return result; } } return name; }也就是说,即便我们不直接写server.port,而是设置环境变量SERVER_PORT,SpringBoot 依然能够识别并使用该值。
6. application.yml 的加载流程
虽然命令行参数和环境变量在prepareEnvironment阶段就进入Environment,但application.yml要等到ConfigDataEnvironmentPostProcessor执行时才被读取。这个处理器在 SpringBoot 2.4 之后取代了旧版的ConfigFileApplicationListener,承担配置文件定位、解析、激活 profile 等职责。
6.1 ConfigDataEnvironmentPostProcessor
核心入口是postProcessEnvironment方法:
@Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { postProcessEnvironment(environment, application.getResourceLoader(), application.getBootstrapRegistry()); } private void postProcessEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader, BootstrapRegistry bootstrapRegistry) { bootstrapRegistry.registerIfAbsent(ConfigDataLocationResolvers.class, this::createConfigDataLocationResolvers); ConfigDataEnvironment configDataEnvironment = getConfigDataEnvironment(environment, resourceLoader, application.getConfigDataLocationResolverRegistry()); environment.getPropertySources().addLast( new ConfigDataEnvironmentPostProcessorPropertySource(configDataEnvironment)); }最终,配置数据会被包装成ConfigDataEnvironmentPostProcessorPropertySource并追加到属性源列表末尾。因为它是addLast,所以在默认情况下,其优先级低于命令行参数、系统属性和环境变量。
6.2 ConfigDataEnvironment 的创建与处理
ConfigDataEnvironment在构造时会读取spring.config.import、spring.config.location等参数,并确定默认的配置搜索路径。默认情况下,SpringBoot 会按以下顺序搜索:
- 当前目录下的
/config子目录 - 当前目录
- classpath 下的
/config包 - classpath 根目录
配置文件默认名称是application,默认扩展名包含.properties、.xml、.yml和.yaml。
创建完成后,processAndApply会驱动整个配置加载过程,包括加载默认配置、无 profile 配置以及spring.profiles.active指定的 profile 配置。
6.3 YAML 解析:YamlPropertySourceLoader
application.yml的解析由YamlPropertySourceLoader完成。它的load方法会读取资源内容,并使用 SnakeYAML 解析成PropertySource集合。
public class YamlPropertySourceLoader implements PropertySourceLoader { @Override public String[] getFileExtensions() { return new String[] { "yml", "yaml" }; } @Override public List<PropertySource<?>> load(String name, Resource resource) throws IOException { List<Map<String, Object>> loaded = new OriginTrackedYamlLoader(resource).load(); if (loaded.isEmpty()) { return Collections.emptyList(); } List<PropertySource<?>> propertySources = new ArrayList<>(loaded.size()); for (int i = 0; i < loaded.size(); i++) { String documentNumber = (loaded.size() != 1) ? " (document #" + i + ")" : ""; propertySources.add(new OriginTrackedMapPropertySource( name + documentNumber, Collections.unmodifiableMap(loaded.get(i)), true)); } return propertySources; } }因此,即使只有一份application.yml,其多层嵌套结构也会被展平成形如spring.datasource.url的属性名。多文档语法则通过---分隔成不同的文档,并分别生成属性源。
6.4 Profile 配置的叠加
当仅指定application.yml时,SpringBoot 会先加载不带 profile 的配置,再加载 profile 对应的配置,例如application-dev.yml。后加载的配置可以覆盖先加载的配置中的同名属性。
spring: profiles: active: dev它对应的加载顺序可以简化为:
application.ymlapplication-dev.yml
如果配置分散在多个位置,那么更靠近应用目录外部的配置具有更高优先级,具体顺序与第 6.2 节中的搜索路径有关。
7. 优先级总览与验证示例
我们把三种本文重点关注的配置来源放到同一个应用里验证。配置文件如下:
server: port: 8080 app: name: config-demo启动时添加命令行参数和环境变量:
SERVER_PORT=8081 APP_NAME=env-value \ java -jar app.jar --server.port=9090那么最终读取到的值会是:
| 属性 | application.yml | 环境变量 | 命令行参数 | 最终值 |
|---|---|---|---|---|
| server.port | 8080 | 8081 | 9090 | 9090 |
| app.name | config-demo | env-value | 未设置 | env-value |
可以通过一个简单的CommandLineRunner输出验证:
@SpringBootApplication public class ConfigDemoApplication { public static void main(String[] args) { SpringApplication.run(ConfigDemoApplication.class, args); } @Bean CommandLineRunner runner(Environment environment) { return args -> { System.out.println("server.port=" + environment.getProperty("server.port")); System.out.println("app.name=" + environment.getProperty("app.name")); }; } }运行结果印证了优先级顺序:命令行参数 > 环境变量 > application.yml。
8. 关键类速查
为了便于后续阅读源码,下面整理本文涉及的关键类与其职责:
| 类名 | 职责 |
|---|---|
SpringApplication | 应用启动入口,负责环境和上下文准备 |
SimpleCommandLineArgsParser | 解析命令行参数 |
SimpleCommandLinePropertySource | 命令行参数属性源 |
SystemEnvironmentPropertySource | 环境变量属性源,支持下划线宽松绑定 |
ConfigDataEnvironmentPostProcessor | 驱动 application 配置文件的加载 |
ConfigDataEnvironment | 定位并处理默认配置文件 |
YamlPropertySourceLoader | 解析 yml/yaml 资源 |
OriginTrackedMapPropertySource | 携带来源追踪信息的属性源 |
9. 排障建议与实际应用
在实际项目中使用多种配置来源时,建议遵循以下原则:
- 默认配置入文件:将稳定的默认值写入
application.yml,让仓库内配置保持语义清晰。 - 环境差异用 profile:使用
application-dev.yml、application-prod.yml管理不同环境。 - 敏感信息用环境变量:数据库密码、第三方密钥等通过环境变量或密钥管理系统注入,避免明文进入代码仓库。
- 应急覆盖用命令行参数:临时调整端口、开关或日志级别时使用
--参数,因其优先级最高且无需改动文件。
当配置没有按预期生效时,可以按以下顺序排查:
- 确认属性名是否与读取时使用的 key 完全一致,注意 yml 中的层级展开。
- 检查是否存在 profile 配置覆盖了默认配置。
- 确认环境变量是否因下划线映射规则意外匹配了目标属性。
- 确认命令行参数是否格式正确,例如 key 和 value 之间应使用
=。 - 在启动时通过 Actuator 的
/actuator/env端点查看最终属性值与来源。
10. 总结
SpringBoot 的配置加载体系以PropertySource和Environment为核心,通过「先聚合、后覆盖」的方式统一管理多种配置来源。命令行参数在prepareEnvironment阶段即被加装为最高优先级属性源;环境变量由SystemEnvironmentPropertySource表示,并支持下划线与点号的宽松绑定;而application.yml的解析则由ConfigDataEnvironmentPostProcessor和YamlPropertySourceLoader在后续阶段完成。
理解这三者的加载顺序与源码实现,可以帮助我们在微服务、容器化部署和多环境切换场景中更加准确地控制配置行为,也能在出现配置异常时快速定位问题来源。