8-[核心配置] Spring Boot 外部化配置详解

2. [核心配置] Spring Boot 外部化配置详解

摘要: 本章将深入探讨 Spring Boot 强大而灵活的配置管理机制。我们将对比 propertiesyml 两种主流格式,并重点掌握 @Value@ConfigurationProperties 两种读取配置的方式,最后学习如何通过 Profiles 实现多环境的配置隔离。

2.1. 外部化配置的核心思想与加载顺序

2.1.1. “痛点”场景:为什么配置需要与代码分离?

想象一下,在我们刚刚完成的 Hello, World! 项目中,我们需要连接一个数据库。一个初级的做法可能是将数据库连接信息硬编码在代码里:

1
2
3
4
5
6
7
8
public class DatabaseConnector {
public void connect() {
String url = "jdbc:mysql://localhost:3306/dev_db";
String username = "root";
String password = "dev_password";
// ... 连接逻辑
}
}

这段代码存在一个致命问题:配置与代码高度耦合。当我们需要将应用从开发环境部署到测试环境,再到生产环境时,数据库的地址、用户名和密码必然会改变。难道我们每次部署都要去修改 Java 源代码,然后重新编译、打包吗?这显然是低效且极易出错的。

解决方案:
将这些易变的配置信息从代码中抽离出来,存放到代码外部的文件中。应用程序在启动时去读取这些外部文件,获取配置。这就是外部化配置的核心思想。


2.1.2. Spring Boot 外部化配置的优势

Spring Boot 将外部化配置思想发挥到了极致,带来了诸多好处:

优势说明
灵活性同一份应用程序代码,无需重新打包,即可通过切换不同的配置文件,在不同环境中运行。
易于维护配置信息集中管理,非开发人员(如运维)也可以安全地修改配置,只需重启应用即可生效。
安全性可以将数据库密码、API密钥等敏感信息存储在生产服务器的特定文件中,而不会泄露在代码仓库里。
标准化Spring Boot 提供了一套标准化的配置机制,使得配置管理变得简单且有据可循。

2.1.3. 关键:Spring Boot 配置的加载优先级顺序

Spring Boot 可以在很多不同的地方查找配置,并且有一套严格的优先级顺序。高优先级的配置会覆盖低优先级的配置。了解这个顺序对于排查“配置为何没生效”的问题至关重要。

部署结构示例

假设我们的应用打包后,部署结构如下:

1
2
3
4
5
. 📂 /opt/app/
├── 📄 my-app-1.0.0.jar # 应用程序 JAR 包
├── 📄 application.yml # JAR 包外部的配置文件
└── 📂 config/ # JAR 包外部的 config 目录
└── 📄 application.yml # config 目录下的配置文件

在这个结构中,my-app-1.0.0.jar 内部的 resources/ 目录下也包含一个 application.yml 文件。

配置加载优先级

  1. jar 包内部的 application.propertiesapplication.yml (优先级最低)
  2. jar 包外部的 application.propertiesapplication.yml (与 jar 包同级目录下)
  3. jar 包外部的 config/ 目录下的 application.propertiesapplication.yml
  4. 操作系统环境变量
  5. Java 系统属性 (-Dkey=value)
  6. 通过命令行参数传入的配置 (--key=value) (优先级最高)

核心理念: 这个设计允许运维人员在不触碰任何代码包的情况下,通过命令行参数外部配置文件来覆盖应用打包时的默认配置,实现了完美的“运维友好”。


2.2. 主流配置方式:.properties vs .yml

Spring Boot 主要支持两种格式的配置文件:.properties.yml

2.2.1. application.properties 语法与用法

这是 Java 中传统的配置文件格式,以简单的键值对形式存在。

文件路径: src/main/resources/application.properties

1
2
3
4
5
6
# 服务器端口
server.port=8080

# 数据库连接信息
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root

2.2.2. application.yml 的层级结构与语法优势

YAML (.yml) 是一种对人类阅读更友好的数据序列化语言。它通过缩进来表示层级关系,结构非常清晰。

文件路径: src/main/resources/application.yml

1
2
3
4
5
6
7
8
9
# 服务器配置
server:
port: 8080

# 数据库连接信息
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root

YAML 语法关键:

  1. 使用空格进行缩进,严禁使用 Tab 键
  2. : 冒号后面必须至少有一个空格

2.2.3. 两种格式的优先级与选择建议

特性.properties 文件.yml 文件
格式扁平的键值对 (spring.datasource.url=...)层级的树状结构,更清晰
可读性配置多时可读性差非常适合描述复杂的、有层级的配置数据
优先级高于 .yml低于 .properties
建议推荐使用 .yml,因其结构化能力远超 .properties仅在需要覆盖 .yml 中某个特定值时少量使用

resources 目录下同时存在 application.propertiesapplication.yml 时,Spring Boot 会两个都加载。如果两个文件中有相同的配置项,.properties 文件中的值会覆盖 .yml 文件中的值。


2.3. 读取配置:@Value vs @ConfigurationProperties

“痛点”场景:

好了,我们已经在 application.yml 中定义了配置,但我们的 Java 代码如何才能获取到这些值呢?

Spring Boot 提供了两种主流的注解来解决这个问题:@Value@ConfigurationProperties

2.3.1. [实践] 使用 @Value 读取单个配置 (含默认值)

@Value 注解非常适合用于注入单个简单的配置值。

文件路径: src/main/resources/application.yml

1
2
3
4
app:
name: "My Awesome App"
owner: "Prorise"
# 我们故意不设置 port

文件路径: src/main/java/com/example/springbootdemo/config/AppInfo.java (新增文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.example.springbootdemo.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class AppInfo {

// 使用 ${...} 占位符来引用配置文件中的 key
@Value("${app.name}")
private String name;

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

// 使用冒号 ":" 来提供一个默认值
// 如果配置文件中找不到 app.port,则会使用 8080
@Value("${app.port:8080}")
private Integer port;

@Override
public String toString() {
return "AppInfo{" +
"name='" + name + '\'' +
", owner='" + owner + '\'' +
", port=" + port +
'}';
}
}

测试代码:

1
2
3
4
5
6
7
8
9
10
@SpringBootTest
class ConfigTest {
@Autowired
private AppInfo appInfo;

@Test
void testValueAnnotation() {
System.out.println(appInfo);
}
}
1
AppInfo{name='My Awesome App', owner='Prorise', port=8080}

2.3.2. [推荐] 使用 @ConfigurationProperties 进行类型安全的属性绑定

“痛点”场景:

@Value 注解虽然简单,但如果要注入的配置项非常多(比如一个数据源有十几个配置),在类中写十几个 @Value 注解会显得非常繁琐和重复。而且,它对复杂的嵌套结构或集合配置支持不佳。

解决方案:
@ConfigurationProperties 注解为此而生。它允许我们将配置文件中一个前缀下的所有属性,整体地、类型安全地绑定到一个 Java 对象(POJO)上。

文件路径: src/main/resources/application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
datasource:
mysql:
url: jdbc:mysql://localhost:3306/prod
username: prod_user
password: prod_password
# 集合配置
connection-properties:
- characterEncoding=utf8
- useSSL=false
# 嵌套对象配置
pool-options:
max-active: 20
min-idle: 5

文件路径: src/main/java/com/example/springbootdemo/config/MySQLProperties.java (新增文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example.springbootdemo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;

@Data // Lombok 注解,自动生成 Getter/Setter
@Component
// 关键:将此类与配置文件中 "datasource.mysql" 为前缀的属性进行绑定
@ConfigurationProperties(prefix = "datasource.mysql")
public class MySQLProperties {

private String url;
private String username;
private String password;
private List<String> connectionProperties; // 对应 YAML 中的列表
private PoolOptions poolOptions; // 对应 YAML 中的嵌套对象

@Data
public static class PoolOptions { // 内部静态类用于映射嵌套对象
private int maxActive;
private int minIdle;
}
}

测试代码:

注意: 记得将上一节的ApiInfo删除,因为我们已经修改了yml文件,不删除会找不到bean的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.springbootdemo;
import com.example.springbootdemo.config.MySQLProperties;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringBootDemoApplicationTests {


@Autowired
private MySQLProperties mySQLProperties;

@Test
void testConfigProperties() {
System.out.println(mySQLProperties);
}

}

1
2
3
MySQLProperties(url=jdbc:mysql://localhost:3306/prod, username=prod_user, 
password=prod_password, connectionProperties=[characterEncoding=utf8, useSSL=false],
poolOptions=MySQLProperties.PoolOptions(maxActive=20, minIdle=5))

2.3.3. 启用属性类:@Component vs @EnableConfigurationProperties

“痛点”场景:

我们已经创建了一个 MySQLProperties 类,并用 @ConfigurationProperties 标注了它。但是,Spring Boot 默认并不会知道这个普通 Java 类的存在。我们必须通过某种方式告诉 Spring:“嘿,这是一个需要你来管理的 Bean,请在容器启动时创建它,并把配置文件里的值绑定进去!”。那么,我们有几种方式来“激活”或“启用”这个属性类呢?

Spring Boot 提供了两种主流的激活方式。

方案一:使用 @Component (简单、直接)

这是最直接的方式。我们只需在属性类上添加 @Component 注解,它就会被 Spring Boot 的组件扫描 (@ComponentScan) 机制自动发现,并注册为一个 Bean。

文件路径: src/main/java/com/example/springbootdemo/config/MySQLProperties.java

1
2
3
4
5
6
7
8
9
10
11
package com.example.springbootdemo.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; // 1. 引入 Component 注解

@Data
@Component // 2. 将此类声明为一个通用的 Spring 组件
@ConfigurationProperties(prefix = "datasource.mysql")
public class MySQLProperties {
// ... 属性不变
}
  • 优点: 简单明了,所有配置都集中在一个类上。
  • 缺点: 属性类 (MySQLProperties) 与 Spring 框架的 @Component 注解产生了耦合。对于我们自己项目内部的配置类,这通常是可以接受的。

方案二:使用 @EnableConfigurationProperties (集中、解耦)

“进阶痛点”:

如果 MySQLProperties 这个类来源于一个我们无法修改的第三方 jar 包呢?或者,在一个大型项目中,我们希望将所有的配置属性类在一个地方进行集中式的、统一的管理,而不是让它们散落在各个角落,我们该怎么做?

解决方案:
这就是 @EnableConfigurationProperties 的用武之地。它允许我们在一个集中的配置类(通常是我们自己创建的,被 @Configuration 标注的类)中,明确地列出所有需要被激活的属性类。

第一步:移除属性类上的 @Component
MySQLProperties 变回一个纯粹的、不依赖 Spring 框架的 POJO。
文件路径: src/main/java/com/example/springbootdemo/config/MySQLProperties.java (修改)

1
2
3
4
5
6
7
8
9
10
package com.example.springbootdemo.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix = "datasource.mysql")
// 移除了 @Component 注解
public class MySQLProperties {
// ... 属性不变
}

第二步:创建集中的配置管理类
文件路径: src/main/java/com/example/springbootdemo/config/AppConfig.java (新增文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.springbootdemo.config;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

// 1. @Configuration 声明这是一个配置管理类
@Configuration
// 2. @EnableConfigurationProperties 明确告诉 Spring Boot
// 去激活并注册 MySQLProperties.class 这个属性类
@EnableConfigurationProperties(MySQLProperties.class)
public class AppConfig {
// 这个类可以是空的,它的主要作用就是通过注解来集中管理配置。
// 当然,我们后续还可以在这里定义 @Bean 等。
}

对比总结

特性方式一: @Component方式二: @EnableConfigurationProperties
使用方式直接在属性类上添加 @Component在一个 @Configuration 配置类上添加 @EnableConfigurationProperties
优点简单、直接、自包含集中管理与框架解耦(属性类本身可以是纯 POJO)
适用场景项目内部自己定义的属性类1. 启用第三方的属性类<br>2. 在项目中集中管理所有属性配置

2.4. 多环境配置管理 (Profiles)

“痛点”场景:

我们的应用开发完成,即将上线。但在部署时遇到了一个典型问题:开发环境用的是本地数据库,测试环境用的是测试库,而生产环境则需要连接生产主库。难道我们每次部署到不同环境,都要手动去修改 application.yml 里的数据库连接信息,然后再重新打包吗?这不仅效率低下,而且极易引发“配错环境”的生产事故。

解决方案:
Spring Profiles 是专门为解决这一问题而设计的。它允许我们为不同的环境创建各自独立的配置文件,在应用启动时,只需通过一个简单的开关,就能激活指定环境的配置,实现“一次打包,多环境灵活部署”。

2.4.1. [实践] application-{profile}.yml 的约定与使用

Spring Boot 约定,特定环境的配置文件命名格式为 application-{profile}.yml (或 .properties),其中 {profile} 就是你自定义的环境名(如 dev, test, prod)。

现在,我们在 src/main/resources 目录下进行如下改造:

  1. application.yml: 作为主配置文件,存放所有环境共享的配置,并指定默认激活的环境。
  2. application-dev.yml: 存放开发环境特有的配置。
  3. application-prod.yml: 存放生产环境特有的配置。

文件路径: src/main/resources/application.yml

1
2
3
4
5
6
7
8
9
10
11
# --------------------
# 主配置文件 (共享配置)
# --------------------
spring:
application:
name: my-app # 比如应用名称,是所有环境共享的

# 关键:指定默认激活的环境为 'dev'
# 如果不通过其他更高优先级的方式指定,应用将默认加载 application-dev.yml
profiles:
active: dev

文件路径: src/main/resources/application-dev.yml

1
2
3
4
5
6
7
8
# --------------------
# 开发环境 (dev)
# --------------------
server:
port: 8080 # 开发时使用 8080 端口
app:
owner: Prorise # 作者名
env: Development Environment

文件路径: src/main/resources/application-prod.yml

1
2
3
4
5
6
7
8
# --------------------
# 生产环境 (prod)
# --------------------
server:
port: 80 # 生产环境通常使用 80 端口
app:
owner: Prorise
env: Production Environment

工作原理: Spring Boot 启动时,会总是先加载主配置文件 application.yml,然后再根据激活的 profile (比如 dev),去加载对应的 application-dev.yml。后加载的配置会覆盖先加载的同名配置。


2.4.2. [实践] 激活特定 Profile 的多种方式

我们有多种方式来激活一个 Profile,它们的优先级各不相同。

方式一:在主配置文件中指定 (开发默认)

这是我们在 application.yml 中已经做过的方式,通过 spring.profiles.active 属性来设置。它通常用于指定本地开发时的默认环境。

1
2
3
spring:
profiles:
active: dev

方式二:通过命令行参数 (生产部署首选)

这是优先级更高生产环境部署最常用的方式。它可以在不修改任何打包文件的情况下,在启动时动态指定环境。

1
2
# 激活生产环境配置来启动应用
java -jar my-app.jar --spring.profiles.active=prod

实践验证

为了验证 Profile 是否生效,我们创建一个简单的 Bean 来读取配置。

文件路径: src/main/java/com/example/springbootdemo/config/AppInfo.java (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.springbootdemo.config;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Data
@Component
public class AppInfo {
@Value("${app.owner}")
private String owner;

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

文件路径: src/test/java/com/example/springbootdemo/SpringBootDemoApplicationTests.java (添加新测试)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SpringBootDemoApplicationTests.java

import com.example.springbootdemo.config.AppInfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringBootDemoApplicationTests {

@Autowired
private AppInfo appInfo;

@Test
void testProfile() {
// 打印注入的配置,验证当前激活的是哪个环境
System.out.println(appInfo);
}
}

1. 默认环境测试
直接在 IDEA 中运行 testProfile() 测试。

1
AppInfo(owner=Prorise, env=Development Environment)

2. 切换环境测试
在 IDEA 中,我们可以模拟命令行参数来切换 Profile:

  • 点击 Edit Configurations...
  • Program arguments 中输入 --spring.profiles.active=prod
    再次运行 testProfile() 测试。
1
AppInfo(owner=Prorise, env=Production Environment)

结论: 实验证明,通过激活不同的 Profile,我们成功地让应用加载了不同的配置,而无需修改任何代码。