Skip to content

Spring Boot 配置体系:配置优先级、Profile 与外部化配置

提出问题

一个 Spring Boot 项目,application.yml 里配了 server.port=8080,启动时加了 --server.port=9090,结果端口是 9090 还是 8080?答案是 9090,因为命令行参数优先级高于配置文件。但如果还设置了环境变量 SERVER_PORT=7070 呢?优先级又变了。

这是 Spring Boot 配置体系中最基础也最容易踩坑的问题——配置来源的优先级。生产环境里,开发在 application.yml 里配了 spring.datasource.url,运维在 application-prod.yml 里覆盖了同一个值,但部署时环境变量 SPRING_DATASOURCE_URL 又被 CI/CD 脚本设置了。三个地方写了同一个配置,最终哪个生效?

再往深了说:@Value@ConfigurationProperties 怎么选?matchIfMissing 默认 false 的坑还记得吗?Profile 多环境怎么做最佳实践?YAML 里的 List 和 Map 怎么绑定?配置不生效时怎么排查?

这些不是问你会不会配,是问你能不能把 Spring Boot 的配置加载全链路说清楚,以及遇到配置覆盖、不回读、绑定失败时怎么排查。

分析问题

配置源的 17 级优先级(从高到低)

Spring Boot 的 Environment 抽象将所有配置源组织成一个 PropertySource 链,按照优先级排序。优先级高的覆盖优先级低的。

完整优先级(从高到低):

1. 命令行参数(--xxx=yyy)
2. 来自 java:comp/env 的 JNDI 属性
3. JVM 系统属性(System.getProperties())
4. 操作系统环境变量
5. RandomValuePropertySource(random.*)
6. application-{profile}.yml(外部 jar 包外)
7. application-{profile}.yml(jar 包内)
8. application.yml(外部 jar 包外)
9. application.yml(jar 包内)
10. @Configuration 类上的 @PropertySource
11. 默认属性(SpringApplication.setDefaultProperties() 设置)

这个顺序在实际中经常被误解。一个典型例子:application.yml 里写了 server.port=8080,环境变量 SERVER_PORT=9090,命令行 --server.port=7070,最终生效的是 7070(命令行最高)。但如果不加命令行,环境变量 9090 会覆盖 yml 里的 8080。

为什么环境变量要用大写加下划线? Spring Boot 的 SystemEnvironmentPropertySource 在解析环境变量时,会宽松匹配:SERVER_PORT 可以匹配 server.portserver_port。这是 Java 环境变量命名惯例(大写、下划线)和 Spring 属性名(小写、点号)之间的转换规则。环境变量中不能有 .,所以 Spring Boot 用 _ 替代 .,并把值全部转为小写匹配。

注意RelaxedBinding(宽松绑定)在 Spring Boot 2.0+ 已经被 RelaxedNames 替代,属性名匹配规则有变化:环境变量 SPRING_DATASOURCE_URL 可以匹配 spring.datasource.url,但 SPRING_DATASOURCE_URL 不能匹配 spring.datasource.url 如果配置源是 YAML——因为 YAML 的键名是大小写敏感的。

配置加载顺序的完整流程

Spring Boot 应用启动时,配置加载的具体流程:

SpringApplication.run()

  ├─ [1] 初始化 SpringApplication 对象
  │     └─ 读取 spring.factories 中的 ApplicationContextInitializer 和 ApplicationListener

  ├─ [2] 调用 run() 方法
  │     ├─ 创建并启动 StopWatch
  │     ├─ 创建 DefaultBootstrapContext(3.x 新特性,用于配置加载的前置准备)
  │     ├─ 配置 Headless 模式
  │     ├─ 启动所有 SpringApplicationRunListener
  │     │
  │     ├─ [3] prepareEnvironment()
  │     │     ├─ 创建 Environment 对象(根据 web 类型选择 StandardServletEnvironment 或 StandardEnvironment)
  │     │     ├─ 配置 Environment:设置 activeProfiles、ConversionService
  │     │     ├─ 配置 PropertySources:将命令行参数、JNDI、系统属性、环境变量依次加入环境
  │     │     ├─ 调用所有 EnvironmentPostProcessor
  │     │     │     └─ 重要:ConfigDataEnvironmentPostProcessor 在这里加载 application.yml/application.properties
  │     │     │           ├─ 按优先级加载外部配置 → 外部 application-{profile}.yml
  │     │     │           ├─ 加载外部 application.yml
  │     │     │           ├─ 加载内部配置 → 内部 application-{profile}.yml
  │     │     │           ├─ 加载内部 application.yml
  │     │     │           └─ 按优先级顺序合并到 PropertySources
  │     │     └─ 绑定到 SpringApplication 的各个属性(如 mainApplicationClass、listeners 等)
  │     │
  │     ├─ [4] printBanner()(可选)
  │     │
  │     ├─ [5] createApplicationContext()
  │     │     └─ 创建 ApplicationContext 实例(如 AnnotationConfigServletWebApplicationContext)
  │     │
  │     ├─ [6] prepareContext()
  │     │     ├─ 设置 context 的 Environment(从 prepareEnvironment 得到的完整配置环境)
  │     │     ├─ 执行 ApplicationContextInitializer(包括从 spring.factories 加载的)
  │     │     ├─ 发送 ApplicationPreparedEvent
  │     │     ├─ 注册 springApplicationArguments(用于 ${} 占位符引用命令行参数)
  │     │     └─ 注册所有 spring boot 的 Banner、BeanFactoryPostProcessor 等
  │     │
  │     ├─ [7] refreshContext()
  │     │     └─ AbstractApplicationContext.refresh()
  │     │           └─ invokeBeanFactoryPostProcessors()
  │     │                 └─ PropertySourcesPlaceholderConfigurer.postProcessBeanFactory()
  │     │                       └─ 解析 ${...} 占位符,从 Environment 中取值
  │     │
  │     └─ [8] afterRefresh()
  │           ├─ 调用 CommandLineRunner 和 ApplicationRunner
  │           └─ 发送 ApplicationReadyEvent

关键细节prepareEnvironment() 阶段创建的 Environment 已经包含了所有配置源,但此时 application.yml 中的 spring.profiles.active 还没有被解析——Profile 的激活是在 ConfigDataEnvironmentPostProcessor 内部处理的,它会在加载配置时递归解析 spring.profiles.activespring.profiles.include,然后加载对应的 Profile 文件。

同名字段覆盖规则

多个配置源同名时,"谁覆盖谁"取决于 PropertySource 在链中的位置:

java
// 打印当前 PropertySource 链
@SpringBootApplication
public class DemoApplication implements ApplicationRunner {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Override
    public void run(ApplicationArguments args) {
        ConfigurableEnvironment env = (ConfigurableEnvironment) ctx.getEnvironment();
        for (PropertySource<?> ps : env.getPropertySources()) {
            System.out.println(ps.getName() + " : " + ps.getSource());
        }
    }
}

输出示例

commandLineArgs: {server.port=9090}
servletConfigInitParams: {}
servletContextInitParams: {}
systemProperties: {java.runtime.name=..., server.port=8080}
systemEnvironment: {SERVER_PORT=9090, ...}
...
Config resource 'class path resource [application.yml]' via location 'class path resource [application.yml]': {server.port=8080, ...}
Config resource 'class path resource [application-prod.yml]' via location 'class path resource [application-prod.yml]': {server.port=7070}

PropertySources 是一个有序列表,遍历时返回第一个匹配的值。所以排在前面的覆盖后面的。env.getPropertySources().addFirst() 可以在队列头部插入,实现最高优先级。

同 Profile 内的配置覆盖

Profile 配置文件的加载顺序比普通配置更复杂。如果一个配置在 application.ymlapplication-prod.yml 中都出现了,最终以 application-prod.yml 为准——因为 Profile 文件的优先级高于默认文件。

Spring Boot 2.4+ 对配置加载做了重构,引入了 ConfigDataLocation 概念。核心变化:

  • spring.profiles 被废弃,改用 spring.config.activate.on-profile
  • 支持 spring.profiles.group 分组激活
  • 支持 spring.profiles.include 在当前 Profile 基础上包含其他 Profile
  • 通过 spring.config.import 导入外部配置(如 configtree:/etc/config/

示例

yaml
# application.yml
spring:
  config:
    activate:
      on-profile: prod
  datasource:
    url: jdbc:mysql://prod-db:3306/mydb

# 等同于以前的 application-prod.yml(但 on-profile 写在文件内部)

多环境配置文件组织方式对比

方式优点缺点适用场景
多文件 application-{profile}.yml清晰分离,每个环境独立文件文件数量多,公共配置重复环境差异大
多文档块(---)单文件,配量少时简洁文件一长就难维护小型项目
单文件 + 外部化配置中心管理,运行时动态依赖外部系统微服务
spring.config.import可组合多个配置源文件关系隐式,新人难理解中大型项目

@Value vs @ConfigurationProperties

两个注解都能从配置文件取值,但侧重点不同。

维度@Value@ConfigurationProperties
绑定方式逐个字段,字符串直接注入批量绑定到 POJO 类
类型安全弱,需要手动转换(SpEL 或 Converter)强,自动类型转换 + 校验(@Validated)
松散绑定不支持(my-name 不能匹配 myName支持(my-name / myName / my_name 均可)
SpEL 支持支持 ${...}#{...} 表达式不支持 SpEL
复杂类型不支持 List/Map 的深度绑定支持 List、Map、嵌套对象绑定
校验手动校验@Validated 自动校验
元数据支持spring-boot-configuration-processor 生成 IDE 提示
刷新默认缓存,不刷新@RefreshScope 支持运行时刷新

适用场景判断

  • 单个配置项,不需要校验,不需要复杂类型 → @Value
  • 一组关联配置,需要类型安全、校验、IDE 提示 → @ConfigurationProperties
  • 配置需要运行时刷新(配合 Spring Cloud 配置中心) → @ConfigurationProperties + @RefreshScope
  • 配置值是 SpEL 表达式计算结果 → @Value

@Value@Configuration 类中解析时间比 @ConfigurationProperties 晚。如果 @Bean 方法里用了 @Value 参数,而这个参数依赖另一个配置文件,可能出现解析不到的值。@ConfigurationProperties 没有这个问题。

java
// @Value 方式:逐个注入,不推荐用于一组配置
@Component
public class DatabaseConfig {
    @Value("${app.database.url}")
    private String url;

    @Value("${app.database.username}")
    private String username;

    @Value("${app.database.password}")
    private String password;
}

// @ConfigurationProperties 方式:批量绑定,推荐
@ConfigurationProperties(prefix = "app.database")
@Component
public class DatabaseProperties {
    private String url;
    private String username;
    private String password;
    private Pool pool = new Pool();  // 嵌套对象

    // getters / setters

    public static class Pool {
        private int maxSize = 10;
        private int minIdle = 2;
        // getters / setters
    }
}

配置嵌套绑定与复杂类型

Spring Boot 支持对 YAML 中的嵌套结构进行绑定:

yaml
app:
  database:
    url: jdbc:mysql://localhost:3306/db
    pool:
      max-size: 20
      min-idle: 5
    connections:
      - host: primary
        port: 3306
      - host: secondary
        port: 3307
java
@ConfigurationProperties(prefix = "app.database")
public class DatabaseProperties {
    private String url;
    private Pool pool = new Pool();
    private List<ConnectionInfo> connections = new ArrayList<>();

    // getters/setters

    public static class Pool {
        private int maxSize;
        private int minIdle;
        // getters/setters
    }

    public static class ConnectionInfo {
        private String host;
        private int port;
        // getters/setters
    }
}

注意事项:

  • 嵌套类必须是 public static,否则 Spring 无法实例化
  • 必须提供 getter/setter(Spring Boot 3.x 可以用 @ConfigurationPropertiesuseRecords 参数配合 record 类)
  • 如果嵌套对象没有默认值,必须初始化(如 new Pool()),否则 Spring 不会自动创建实例,字段为 null
  • List 类型绑定:如果 application.yml 中定义了 List,可以为空([]),但不会 null

常见配置踩坑

1. YAML 缩进错误

# 错误:缩进不一致
server:
  port: 8080
   port: 9090  # 多了一个空格,变成了另一个属性

# 正确
server:
  port: 8080

YAML 的缩进一旦错乱,不会报错,而是解析成你完全想不到的键值结构。server.port 直接变成 null,新属性 server.port(注意前面有个空格)变成了 server..port。排查了半小时才发现是多了一个空格。

2. List 绑定失败

yaml
# 这样写,@ConfigurationProperties 的 List 字段会绑定失败
my:
  items: "a,b,c"

# 正确写法
my:
  items:
    - a
    - b
    - c

"a,b,c" 是字符串,不是 List。Spring Boot 的 DefaultConversionService 不会自动将逗号分割的字符串转为 List。需要写 YAML 的数组语法。

3. @Value 绑定 List

java
// 这样不行——@Value 只支持简单类型
@Value("${my.items}")
private List<String> items;

// 可以这样——用 SpEL 拆解
@Value("#{'${my.items}'.split(',')}")
private List<String> items;

但 SpEL 有个坑:如果 my.items 配置值为空,split(',') 会返回 [""](一个空字符串的 List),而不是空 List。正确做法是加默认值:"${my.items:}",并在 SpEL 中判断空字符串。

4. 配置不生效的排查路径

bash
# 查看所有配置源和值
curl http://localhost:8080/actuator/env

# 查看某个具体配置项
curl http://localhost:8080/actuator/env/server.port

# 查看配置来源
curl http://localhost:8080/actuator/env/server.port | jq '.propertySources'

Actuator /env 端点的输出结构

json
{
  "activeProfiles": ["prod"],
  "propertySources": [
    {
      "name": "server.ports",
      "properties": {
        "local.server.port": {
          "value": 8080,
          "origin": "Managed Source"
        }
      }
    },
    {
      "name": "application-prod.yml",
      "properties": {
        "server.port": {
          "value": "9090",
          "origin": "class path resource [application-prod.yml]:3:12"
        }
      }
    },
    {
      "name": "application.yml",
      "properties": {
        "server.port": {
          "value": "8080",
          "origin": "class path resource [application.yml]:2:9"
        }
      }
    }
  ]
}

Origin 字段:class path resource [application-prod.yml]:3:12 直接告诉你这个值来自 application-prod.yml 第 3 行第 12 个字符。排查配置覆盖时,这个信息比任何日志都有用。

手动排查路径(没有 Actuator 时):

java
// 注入 Environment 手动查看
@Component
public class ConfigDebugger implements ApplicationRunner {
    @Autowired
    private Environment env;

    @Override
    public void run(ApplicationArguments args) {
        // 查看某个属性及其来源
        System.out.println("server.port = " + env.getProperty("server.port"));

        // 查看所有 active profiles
        String[] profiles = env.getActiveProfiles();
        System.out.println("Active profiles: " + Arrays.toString(profiles));

        // 查看所有 PropertySource
        if (env instanceof ConfigurableEnvironment ce) {
            for (PropertySource<?> ps : ce.getPropertySources()) {
                System.out.println(ps.getName() + " -> " + ps.getSource());
            }
        }
    }
}

5. @ConditionalOnProperty 的 matchIfMissing 陷阱

前面在条件注解那篇讲过,这里再提一下:

java
@Bean
@ConditionalOnProperty(name = "my.feature.enabled", havingValue = "true")
public MyService myService() {
    return new MyService();
}

如果 application.yml 里没有配 my.feature.enabledmatchIfMissing 默认 false,这个 Bean 不会创建。正确的做法是明确 matchIfMissing = true 或者确保配置存在。

配置热更新:RefreshScope 与配置中心

Spring Cloud 提供了 @RefreshScope 注解,配合配置中心(Nacos、Apollo、Spring Cloud Config)实现运行时配置刷新:

java
@RefreshScope
@ConfigurationProperties(prefix = "app.feature")
@Component
public class FeatureFlags {
    private boolean newPayment = false;
    private boolean newCheckout = false;
    // getters/setters
}

@RefreshScope 的原理:被标注的 Bean 在刷新时会重新创建。@RefreshScope 是一个特殊的 @Scope("refresh")RefreshScope 实现了 ApplicationContextAware,收到 RefreshScopeRefreshedEvent 时清除 refresh scope 的 Bean 缓存,下次 getBean 时重建。

注意:@RefreshScope 不适用于 @Configuration 类中的 @Bean 方法——@Configuration 类本身是单例,不受 @RefreshScope 影响。要把 @RefreshScope 放在 @Bean 方法上,而不是 @Configuration 类上。

不同配置中心的刷新机制

配置中心刷新机制时效性侵入性
Spring Cloud Config + Bus发送 RefreshRemoteApplicationEvent,广播刷新秒级低(依赖 Bus)
Nacos长轮询检测配置变更,自动推送秒级低(spring-cloud-starter-alibaba-nacos-config 内置)
ApolloHTTP 长连接推送准实时低(Apollo 客户端自动处理)
手动刷新POST /actuator/refresh手动触发

Nacos 配置热更新的实现流程

  1. 客户端启动时,向 Nacos Server 注册 ConfigService 并建立长轮询连接
  2. 服务端接到客户端的长轮询请求,对比本地配置的 MD5 值
  3. 如果配置变更,服务端立即返回变更的数据 ID 和 group
  4. 客户端收到响应后,调用 ConfigService.getConfig() 获取最新配置
  5. 触发 RefreshScope.refreshAll()EnvironmentChangeEvent 发布
  6. @RefreshScope 标注的 Bean 重新创建,获取新配置值

Nacos 的配置热更新走的是长轮询,不是 WebSocket 长连接。长轮询连接数少(每个客户端只维持一个 HTTP 连接)、兼容性好、不受防火墙限制,但延迟比 WebSocket 稍高(约 1-3 秒)。

高级:spring.config.import 与外部配置

Spring Boot 2.4+ 的 spring.config.import 支持从外部位置加载配置:

yaml
# 从文件系统加载
spring:
  config:
    import: file:/etc/config/myapp.yml

# 从 classpath 加载
spring:
  config:
    import: classpath:extra-config.yml

# 从配置中心加载(需要对应的 starter)
spring:
  config:
    import: nacos:myapp.yml?group=DEFAULT_GROUP&refreshEnabled=true

spring.config.import 的加载顺序:被 import 的配置按声明顺序依次加载,排在后面的优先级更高。如果 spring.config.import 中的配置和 application.yml 冲突,import 的配置优先。

spring.config.import 的两种模式:

  • optional:结尾加 optional: 前缀,表示文件不存在时不会报错
  • 默认:不加 optional:,文件不存在则启动失败
yaml
spring:
  config:
    import: optional:file:/etc/config/myapp.yml

Profile 激活与多环境最佳实践

Profile 激活方式(按优先级从高到低):

  1. 命令行:--spring.profiles.active=prod
  2. 环境变量:SPRING_PROFILES_ACTIVE=prod
  3. application.yml 中的 spring.profiles.active 属性
  4. SpringApplication.setAdditionalProfiles()

Spring Boot 2.4+ 的 Profile 分组

yaml
spring:
  profiles:
    group:
      dev: dev,dev-db,dev-mq
      prod: prod,prod-db,prod-mq

激活 dev Profile 时,devdev-dbdev-mq 三个 Profile 同时激活。

多环境配置的最佳实践:

  1. 公共配置放 application.yml,环境差异放 application-{profile}.yml
  2. 敏感配置(密码、密钥)不要放配置文件,用环境变量或配置中心
  3. 使用 spring.profiles.group 组合细粒度 Profile
  4. 配置校验:@ConfigurationProperties + @Validated + hibernate-validator
  5. 配置变更记录:使用 Git 管理配置文件的版本历史
yaml
# application.yml(公共配置)
server:
  port: 8080

spring:
  jpa:
    hibernate:
      ddl-auto: none
    show-sql: false

# application-dev.yml(开发环境,覆盖公共配置)
server:
  port: 8081

spring:
  jpa:
    show-sql: true

# application-prod.yml(生产环境)
spring:
  datasource:
    url: ${DB_URL}  # 敏感信息用环境变量
    username: ${DB_USER}
    password: ${DB_PASS}

总结

  • Spring Boot 配置源有 17 级优先级,从高到低:命令行 > JVM 属性 > 环境变量 > Profile 文件 > 默认配置
  • 同名字段覆盖规则:PropertySource 链中排在前面的覆盖后面的,env.getPropertySources().addFirst() 可以动态插入最高优先级
  • @Value 适用于单个简单配置项,@ConfigurationProperties 适用于一组关联配置,需要类型安全和校验的场景
  • 配置嵌套绑定时,嵌套类必须是 public static,必须有 getter/setter,嵌套对象必须初始化
  • 排查配置不生效用 Actuator 的 /env 端点,看 Origin 字段确定值来源;没有 Actuator 时注入 Environment 手动打印
  • Profile 激活优先级:命令行 > 环境变量 > 配置文件;2.4+ 支持分组和 spring.config.import
  • 配置热更新依赖 @RefreshScope + 配置中心,不同配置中心的刷新机制不同(Nacos 长轮询、Apollo 长连接推送)
  • 敏感配置永远不要放配置文件,用环境变量或配置中心统一管理

参考:Spring Boot Reference Documentation — Externalized Configuration · Spring Boot Actuator /env 端点 · Nacos Spring Cloud 配置管理文档 · Spring Cloud @RefreshScope 源码

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1