外部化配置

什么是外部化配置

在软件开发中,外部化配置指的是将应用程序的配置信息(如环境参数、连接地址、业务开关等)与代码逻辑分离,存储在外部资源中(如配置文件、环境变量、数据库等)。Spring Boot 的外部化配置机制并非凭空产生,而是源于软件开发中一系列实际需求的驱动

Spring Boot 外部化配置 (Externalized Configuration) 提供了一套强大的机制,允许我们将应用的配置 从代码中解耦出来,并通过多种外部来源进行灵活管理,从而打造出 可移植、可扩展、易于维护 的 Spring Boot 应用。

SpringBoot支持多种外部化配置,以便于开发者能够在不同的环境下,使用同一套应用程序代码。外部化配置的方式有多种:properties文件,yaml文件,Environment变量已经命令行参数等等。

为什么需要外部化配置

  • 硬编码的痛点:若将配置直接写入代码(如数据库连接字符串、端口号),当环境变更时(如从测试环境部署到生产环境),必须修改代码并重新编译,这不仅增加了部署风险,还违背了 “开闭原则”。
  • 外部化配置的优势:将配置移至外部文件(如application.yml),代码只需通过变量引用配置,环境变更时仅需修改配置文件,无需修改代码。而且外部化配置使得应用的配置管理更加清晰、结构化,易于扩展和维护,适应应用规模的增长。
  • 环境多样性需求:开发、测试、预发、生产环境的基础设施(如服务器地址、缓存配置)往往不同,外部化配置可通过 Profile 机制(如application-dev.ymlapplication-prod.yml)为不同环境定制专属配置,通过命令行参数--spring.profiles.active=prod即可指定启动环境,无需修改代码或打包不同版本。
  • 安全性提升 (Security Enhancement): 可以将敏感信息存储在更安全的地方 (例如环境变量、外部配置中心),避免硬编码在代码中。

多种配置同时出现的生效顺序

Spring Boot 按以下顺序加载配置源,后加载的会覆盖先加载的配置

配置源优先级列表(由高到低)

  • DevTools 全局属性:devtool处于active状态时,$HOME/.config/spring-boot目录中的Devtool全局配置。

  • @TestPropertySource 注解:测试类中指定的属性源。

    1
    2
    @TestPropertySource("classpath:test.properties")
    class MyServiceTest { ... }
  • 测试属性@SpringBootTest 测试时通过 properties 参数指定。

    1
    2
    @SpringBootTest(properties = {"server.port=9090"})
    class MyAppTest { ... }
  • 命令行参数:通过 --key=value 传递,如 --server.port=8080

  • SPRING_APPLICATION_JSON:内置在环境变量或系统属性中的 JSON 配置。

    1
    export SPRING_APPLICATION_JSON='{"server.port": 8081, "app.name": "json-app"}'
  • ServletConfig 初始化参数:Servlet 的 <init-param>配置。

  • ServletContext 初始化参数:在 web.xml 中配置的 <context-param>

  • JNDI 属性:来自 java:comp/env 上下文,传统 Java EE 环境使用。

  • Java 系统属性:通过 System.getProperty() 获取,如 -Dapp.env=prod

    1
    java -jar app.jar -Dapp.env=prod
  • OS 环境变量:通过 System.getenv() 获取,如 DATABASE_URL

    1
    export DATABASE_URL=jdbc:mysql://prod.db:3306/app
  • 随机值配置random.* 配置(如 random.intrandom.uuid)。

    1
    2
    app.secret=${random.uuid}
    app.port=${random.int[10000,20000]}
  • 配置文件application.properties/.yml 及其 Profile 变体(如 application-dev.properties)。

    • 配置文件内的加载顺序如下
      • 首先,同路径下 .properties 优先于 .yml(如 application.properties 覆盖 application.yml)。
      • 环境专属配置application-{profile}.properties高于默认配置application.properties
      • 应用程序以外的application-{profile}.properties或者application.properties文件高于打包在应用程序内的application-{profile}.properties或者application.properties,也就是 jar 包外的高于 jar包内的,这就是外部化配置生效的重要原因
      • 类路径(内部)中的配置文件, 类下/config包 高于 类根路径
      • 当前路径(项⽬所在的位置),/config⽬录的直接⼦⽬录 高于 当前下 /config⼦⽬录 高于 当前路径
  • @PropertySource 注解:标注在 @Configuration 类上,加载指定配置文件。

    1
    2
    3
    @Configuration
    @PropertySource("classpath:custom.properties")
    public class AppConfig { ... }
  • 默认属性:通过 SpringApplication.setDefaultProperties(Map) 定义的默认值。

    1
    2
    SpringApplication app = new SpringApplication(MyApp.class);
    app.setDefaultProperties(Collections.singletonMap("app.name", "default-app"));

这里列表按组优先级排序,也就是说,任何在高优先级属性源里设置的属性都会覆盖低优先级的相同属性

所有参数均可由命令⾏传⼊,使⽤ --参数项 = 参数值 ,将会被添加到环境变量中,并优先于配置⽂件 。

结论:配置可以写到很多位置,常⻅的优先级顺序:

  • 命令⾏ > 配置⽂件 > spring application 配置

建议:⽤⼀种格式的配置⽂件。 .properties 和 结 .yml 同时存在 , 则 .properties 优先

包外 > 包内 ; 同级情况: profile 配置 > application 配置 properties配置 > yaml配置

命令⾏ > 包外config直接⼦⽬录 > 包外config⽬录 > 包外根⽬录 > 包内⽬录

配置文件的加载位置按优先级从高到低排列:

  1. 命令行参数(最高优先级,可覆盖一切)。
  2. 当前目录的 /config 子目录(如 ./config/application.yml)。
  3. 当前目录(如 ./application.yml)。
  4. 类路径的 /config(如 classpath:/config/application.yml)。
  5. 类路径根目录(如 classpath:/application.yml)。
image-20250620203750693

规律:最外层的最优先。

  • 命令⾏ > 所有
  • 包外 > 包内
  • config⽬录 > 根⽬录
  • profile > application

配置不同就都⽣效(互补),配置相同⾼优先级覆盖低优先级

各种外部化配置

导入配置

使⽤spring.config.import可以导⼊额外配置

让你能在 Spring Boot 主配置文件(比如 application.propertiesapplication.yml)里,引入外部的配置文件或配置片段 ,灵活扩展配置来源。

1
2
3
4
# 1. 导入外部配置文件
spring.config.import=my.properties
# 2. 主配置文件里的属性
my.property=value

spring.config.import=my.properties: 告诉 Spring Boot,加载当前配置文件时,额外导入 my.properties 文件的配置my.properties 需和主配置文件在类路径或可识别路径下 )

优先级说明: 无论 spring.config.importmy.property=value 谁写在前,my.properties 里的 my.property 值,会覆盖主配置文件里的同名属性 。 这是因为导入的配置,在加载顺序和优先级上更 “高”,用于覆盖主配置里的默认或基础配置

这样一来,就实现了配置拆分,把大而全的配置拆成多个小文件(比如 db.properties 放数据库配置、redis.properties 放 Redis 配置 ),用 spring.config.import 聚合,方便维护,而且不同环境(开发 / 测试 / 生产)的差异化配置,可单独写文件,通过 spring.config.import 按环境引入

属性占位符

配置⽂件中可以使⽤ ${name:default}形式取出之前配置过的值。在配置文件里,动态引用已定义的属性值 ,还能设置默认值,让配置更灵活、可复用。

在使用application.properties中的值的时候,他们会从 Environment 中获取值,那就意味着,可以引用之前定义过的值,比如引用系统属性。具体做法如下:

1
2
3
4
# 1. 定义基础属性
app.name=MyApp
# 2. 引用属性 + 默认值
app.description=${app.name} is a Spring Boot application written by ${username:Unknown}
  • ${app.name}: 引用已定义的 app.name 属性值(即 MyApp ),最终 app.description 里的 {app.name} 会被替换成 MyApp
  • ${username:Unknown}: 尝试引用 username 属性,如果 username 没定义,就用默认值 Unknown 兜底,避免配置缺失报错。

这个在如下情况下很有用

  • 配置拼接:比如拼接服务地址 service.url=${host}:${port} ,只需维护 hostport,动态组合成完整地址。让配置可拆分、可聚合,适配复杂项目的多环境、多模块配置管理,降低单个配置文件的复杂度。
  • 默认值兜底:对于一些非必填、或环境差异大的配置(比如开发者本地的用户名 ),用 :默认值 避免配置遗漏导致启动失败。实现配置的 “动态引用” 和 “默认兜底”,减少硬编码,提升配置的灵活性和可维护性

随机值配置

在配置文件里,生成随机的字符串、数字等 ,常用于测试环境的动态值(比如随机端口、随机密钥 ),避免固定值引发的冲突或安全问题。

配置文件中${random} 可以用来生成各种不同类型的随机值,从而简化了代码生成的麻烦

Spring Boot 内置了 RandomValuePropertySourceRandomValuePropertySource类重写了getProperty方法,判断以random.为前缀之后,进行了适当的处理,支持以下语法:

1
2
3
4
5
6
7
8
9
10
11
# 1. 随机整数(范围可选,如 1000-9999)
server.port=${random.int:1000-9999}

# 2. 随机长整数
my.random.long=${random.long}

# 3. 随机 UUID(唯一识别码)
my.random.uuid=${random.uuid}

# 4. 随机字符串(自定义长度或直接生成)
my.random.str=${random.value}

这样配置的效果:

  • 启动应用时,server.port 会随机生成 1000-9999 之间的整数,避免多个服务本地启动时端口冲突。
  • my.random.uuid 每次启动都会生成一个唯一的 UUID,可用于动态生成临时 Token、测试用的唯一标识等。

一般在生成随机的加密密钥(如 jwt.secret=${random.value} ),测试环境不用写死密钥,减少泄露风险。

命令行参数配置

默认情况下,SpringApplication将所有的命令行选项参数【以--开头的参数,如--server.port=9000】转换为属性,并将它们加入SpringEnvironment中,命令行属性的配置始终优先于其他的属性配置。

所以我们可以运行时通过命令行传入配置参数 ,覆盖配置文件里的默认值,灵活适配不同环境(尤其适合生产环境快速调整配置 )。

使用方式也比较简单,启动 Spring Boot 应用时,通过 --参数=值 的形式传参,示例:

1
2
3
4
5
# 方式 1:jar 包启动(经典方式)
java -jar myapp.jar --server.port=8081 --app.env=prod

# 方式 2:IDE 里启动(添加 VM Options 或 Program arguments)
--server.port=8082 --db.username=test

命令行参数的优先级非常高 ,会覆盖配置文件(application.properties/application.yml )、环境变量里的同名配置。

如果你不希望将命令行属性添加到Environment中,可以使用SpringApplication.setAddCommandLineProperties(false)禁用它。

加密属性

配置文件里常包含 敏感信息(如数据库密码、API 密钥 ),直接明文存储有泄露风险。

Spring Boot不提供对加密属性值的任何内置支持,但是,它提供了修改Spring环境中的值所必需的挂钩点。所以Spring Boot 可通过扩展实现 配置加密,让敏感配置以密文存储,运行时解密使用。

jasypt-spring-boot扩展为例子,先导入如下依赖

1
2
3
4
5
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version> <!-- 版本按需选 -->
</dependency>

在配置文件(或环境变量)里设置 加密密钥(生产环境建议用环境变量 / 启动参数,避免硬编码 ):

1
2
# application.properties
jasypt.encryptor.password=MySuperSecretKey!2025

加密配置值,用 Jasypt 工具生成密文。比如命令行加密(或写代码加密 ):

1
2
3
4
5
# 用 jasypt 命令行工具加密(假设密钥是上面的 MySuperSecretKey!2025 )
java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI \
input="db.password=MyDBP@ssw0rd" \
password=MySuperSecretKey!2025 \
algorithm=PBEWithMD5AndDES

配置文件用密文,把加密后的字符串放到配置里,用 ENC(...) 包裹:

1
spring.datasource.password=ENC(abcdef123456...)  

Spring Boot 启动时,jasypt-spring-boot 会自动识别 ENC(...) 格式的配置,用密钥解密,注入到对应的 Bean 中。

其中

  • 编译期 / 运行期解密:通过自定义 PropertySourceBeanPostProcessor,拦截配置加载,对密文做解密处理。
  • 密钥安全:生产环境密钥绝不能硬编码到配置文件!建议通过 环境变量export JASYPT_ENCRYPTOR_PASSWORD=xxx )、启动参数java -jar app.jar --jasypt.encryptor.password=xxx )传入。

类型安全的属性配置

配置文件里的 key=value松散的字符串配置,直接用 @Value("${key}") 注入时:

  • 零散的 @Value 注入在配置多的时候,代码冗余且易出错。
  • 无法做 类型校验(比如配置是 int 类型,但填了字符串 )。

例如:

1
2
3
4
5
6
7
redis.host=localhost
redis.port=6379
redis.password=123456
redis.timeout=3000
redis.pool.max-active=100
redis.pool.max-idle=20
redis.pool.min-idle=5

如果你使用 @Value注入

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
30
31
32
33
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class RedisService {

@Value("${redis.host}")
private String host;

@Value("${redis.port}")
private int port;

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

@Value("${redis.timeout}")
private int timeout;

@Value("${redis.pool.max-active}")
private int maxActive;

@Value("${redis.pool.max-idle}")
private int maxIdle;

@Value("${redis.pool.min-idle}")
private int minIdle;

// 使用这些配置的方法...
public void connect() {
System.out.println("Connecting to Redis: " + host + ":" + port);
// ...
}
}
  • 每个配置项都要写一个 @Value 注解,重复模板代码多。
  • 如果有多个类需要这些配置,每个类都要重复写一遍 @Value

@ConfigurationProperties 让配置 结构化、类型安全,把配置映射到 Java 实体类,支持校验、嵌套。

使用步骤如下:

  • 定义配置实体类,用 @ConfigurationProperties 绑定配置前缀

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    import java.util.List;

    @Component
    @ConfigurationProperties(prefix = "app")
    public class AppConfig {
    private String name; // 对应 app.name
    private int port; // 对应 app.port
    private List<String> features; // 对应 app.features[0], app.features[1]...

    // Getter + Setter 必须有(Lombok 可简化)
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    // ...其他 Getter/Setter
    }
  • 配置文件编写,在 application.properties 里按前缀写配置:

    1
    2
    3
    4
    app.name=MyApp
    app.port=8080
    app.features[0]=security
    app.features[1]=logging
  • 注入并且使用,在任意 Bean 中注入 AppConfig,直接用结构化属性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import org.springframework.stereotype.Service;

    @Service
    public class MyService {
    private final AppConfig appConfig;

    public MyService(AppConfig appConfig) {
    this.appConfig = appConfig;
    }

    public void printConfig() {
    System.out.println("Name: " + appConfig.getName()); // 直接用实体类属性
    System.out.println("Port: " + appConfig.getPort());
    }
    }
  • 而且可以加 @Validated 和 JSR303 注解,实现配置校验:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import jakarta.validation.constraints.Min;
    import jakarta.validation.constraints.NotEmpty;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    import org.springframework.validation.annotation.Validated;
    import java.util.List;

    @Component
    @ConfigurationProperties(prefix = "app")
    @Validated // 开启校验
    public class AppConfig {
    @NotEmpty(message = "app.name 不能为空")
    private String name;

    @Min(value = 1024, message = "app.port 不能小于 1024")
    private int port;

    private List<String> features;

    // Getter + Setter
    }

    如果配置不符合规则(如 app.port=80 ),启动时会直接报错,提前拦截配置错误。

通过@ContructorBinding注解使用构造器绑定的方式:

简单说,它是 让 @ConfigurationProperties 类通过 构造器 完成属性绑定 的注解,核心解决 “配置类字段不可变(final)” 场景的绑定问题,也能让对象初始化更可控。

如果使用默认 @ConfigurationProperties ,它是用 Setter 方法来绑定属性,这会带来一些问题

  • 字段必须可变:类里的字段得有 Setter(即 private String name; public void setName(String name) {...} ),无法用 final 修饰。
  • 初始化时机模糊:属性通过 Setter 零散赋值,若依赖多字段初始化,容易出现 “部分字段已赋值、部分未赋值” 的中间状态。

@ConfigurationProperties 类上,加上 @ConstructorBinding,明确告诉 Spring Boot:用构造器来绑定配置属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;

@ConstructorBinding // 关键:启用构造器绑定
@ConfigurationProperties(prefix = "app")
public class AppConfig {
private final String name;
private final int port;

// 构造器注入所有配置属性
public AppConfig(String name, int port) {
this.name = name;
this.port = port;
}

// Getter(无需 Setter)
public String getName() { return name; }
public int getPort() { return port; }
}

和常规 @ConfigurationProperties 一样,按前缀写配置:

1
2
app.name=MyApp
app.port=8080

在需要的地方,通过 构造器注入@Autowired 注入 AppConfig 即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.springframework.stereotype.Service;

@Service
public class MyService {
private final AppConfig appConfig;

// 构造器注入(推荐)
public MyService(AppConfig appConfig) {
this.appConfig = appConfig;
}

public void printConfig() {
System.out.println("Name: " + appConfig.getName());
System.out.println("Port: " + appConfig.getPort());
}
}

注意:

  • 必须配合 @ConfigurationProperties@ConstructorBinding 不能单独用,必须和 @ConfigurationProperties 一起标注在类上。

  • 依赖注入方式: 若用构造器绑定,配置类必须能被 Spring 扫描到(或通过 @EnableConfigurationProperties 显式启用 )。 比如非 @Component 标注的配置类,需要在启动类加:

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableConfigurationProperties(AppConfig.class) // 显式启用
    public class Application {
    public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    }
    }