Ohhnews

分类导航

$ cd ..
Jetbrains Blog原文

Spring Boot配置管理最佳实践

#spring boot#配置管理#最佳实践#环境变量#微服务

Spring Boot 提供了全面的外部化应用配置支持。它允许同一个应用制品(artifact)通过从各种来源提供值,从而在不同的环境中运行,例如:

  • 属性文件
  • 环境变量
  • 系统属性
  • 命令行参数

在本文中,我们将探讨管理 Spring Boot 应用配置的最佳实践。

一个设计良好的配置策略应确保:

  • 配置与应用代码保持分离。
  • 当必需的配置缺失或无效时,应用无法启动。
  • 默认值可以被每个部署环境覆盖。
  • 敏感值由专用的机密管理系统提供。

配置属性分类

通常,Spring Boot 应用配置分为三类:

  • 应用默认值: 安全、非机密的值,例如第三方服务 URL、超时时间和重试次数限制。这些值随应用一起存储。
  • 部署配置: 标识环境的值,例如数据库主机、队列名称和外部服务 URL。这些值通过部署平台提供。
  • 机密信息: 密码、API 密钥、证书和私钥。这些值存储在专用的机密系统中。

例如,application.properties 可以提供应用默认配置属性:

$ properties
app.promotion-service.base-url=http://localhost:8181
app.promotion-service.timeout=3s
app.promotion-service.retries=3
logging.level.com.jetbrains=DEBUG
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.open-in-view=false

默认值对于其可能被使用的每个环境都应该是安全的。数据库 URL 和凭据等属性绝不应硬编码在应用代码中。如果某个必需值没有安全的默认值,请在启动时验证其是否存在。

使用 @ConfigurationProperties 绑定应用属性

Spring 应用可以通过 Environment@Value@ConfigurationProperties 访问配置值。

当属性名称必须动态解析,或基础设施代码需要直接访问属性源时,请使用 Environment

对于孤立的值,请使用 @Value

$ java
PromotionService(
  @Value("${app.promotion-service.base-url}") String baseUrl,
  @Value("${app.promotion-service.timeout}") Duration timeout,
  @Value("${app.promotion-service.retries}") int retries) {
    this.baseUrl = baseUrl;
    this.timeout = timeout;
    this.retries = retries;
}

分散的 @Value 表达式使得属性名称难以发现、验证和重构。使用 @ConfigurationProperties 的专用配置类型则支持所有这些特性。

对于相关的配置属性,优先使用 @ConfigurationProperties。它提供了:

  • 类型安全的绑定和转换
  • 属性名称与 Java 成员之间的宽松绑定
  • 分组级校验
  • 通过生成的元数据进行 IDE 补全和导航

例如,如果我们要与第三方 REST API 集成,可能需要配置服务的基 URL、超时时间和重试次数。

$ properties
app.promotion-service.base-url=${PROMOTION_SERVICE_URL}
app.promotion-service.timeout=${PROMOTION_SERVICE_TIMEOUT:3s}
app.promotion-service.retries=3

在上面的配置中,我们从环境变量 PROMOTION_SERVICE_URL 设置 base-url 的值,并从环境变量 PROMOTION_SERVICE_TIMEOUT 设置 timeout 的值,其默认值为 3 秒。

Spring Boot 支持基于 setter 的绑定。你可以将属性绑定到使用 setter 的类,如下所示:

$ java
@ConfigurationProperties(prefix = "app.promotion-service")
public class PromotionSvcProperties {

    private String baseUrl;
    private Duration timeout;
    private int retries;

    // Setters and getters
}

使用 @ConfigurationPropertiesScan 注册配置类型:

$ java
@SpringBootApplication
@ConfigurationPropertiesScan
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@ConfigurationPropertiesScan 注解会扫描带有 @ConfigurationProperties 注解的组件,并将它们注册为 Spring Bean。

现在我们可以将 PromotionSvcProperties 注入其他 Spring Bean,并访问属性值。

优先使用 Record 进行 @ConfigurationProperties 绑定

通常,配置在启动期间建立,并在应用的整个生命周期内保持不变。

对于大多数应用配置,Java Record 是首选方案。它开箱即用地提供了不可变性,因此即使误操作也不会修改其值;相比之下,基于类的绑定可能会意外调用 setter:

$ java
@ConfigurationProperties(prefix = "app.promotion-service")
public record PromotionSvcProperties(
        String baseUrl,
        Duration timeout,
        int retries) {
}

Spring Boot 的宽松绑定会将规范的 kebab-case 名称(如 base-url)映射到 baseUrl 字段。

有时我们可能希望将属性绑定到第三方库提供的 Bean,但我们无法修改其源代码来添加 @ConfigurationProperties 注解。

要将配置属性直接绑定到第三方类,请将其声明为 @Bean,并在 Bean 方法上使用 @ConfigurationProperties 注解:

$ java
@Configuration
public class ClientConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "third-party.client")
    public ThirdPartyClientProperties clientProperties() {
        return new ThirdPartyClientProperties();
    }
}

你可以如下配置 third-party.client 属性:

$ properties
third-party.client.base-url=https://api.example.com
third-party.client.connect-timeout=5s
third-party.client.read-timeout=30s

如果第三方类是不可变的或不支持 setter 绑定,请创建你自己的属性类,并使用它来构造第三方对象:

$ java
@ConfigurationProperties(prefix = "third-party.client")
public record ClientProperties(
    URI baseUrl,
    Duration connectTimeout,
    Duration readTimeout
) {}


@Configuration
@EnableConfigurationProperties(ClientProperties.class)
class ClientConfiguration {

    @Bean
    ThirdPartyClient thirdPartyClient(ClientProperties properties) {
        return new ThirdPartyClient(
                properties.baseUrl(),
                properties.connectTimeout(),
                properties.readTimeout()
        );
    }
}

包装这种方式通常更可取,因为它可以避免将应用配置直接耦合到第三方库的类结构。

快速失败、尽早失败:在启动时验证配置

配置错误应在应用启动时被发现,并在配置缺失或无效时快速失败。在 @ConfigurationProperties Bean 上添加 @Validated,并对其属性应用 Jakarta Bean Validation 约束。

$ java
@Validated
@ConfigurationProperties(prefix = "app.promotion-service")
public record PromotionSvcProperties(
 @NotBlank String baseUrl,
        @NotNull Duration timeout,
        @Min(1) @Max(5) int retries,
        @NotNull @Valid SyncProperties sync) {

    public record SyncProperties(@NotEmpty String cron) {
    }
}

spring-boot-starter-validation 在 classpath 中时,绑定或校验失败会阻止应用启动。请验证必需值、数值范围、嵌套组以及其他应用级约束。

当必须区分缺失值与 Java 默认值时,请使用包装类型。例如,使用 @NotNull 注解的 Integer 可以标识缺失值,而 int 默认值为 0。

理解属性优先级

Spring Boot 会合并多个属性源。当同一属性出现在多个来源中时,优先级较高的来源会提供最终生效的值。

以下简化顺序展示了应用部署中最常用的属性源,按从低到高的优先级排列:

application.properties/yaml(低优先级)
特定于 profile 的配置文件
操作系统环境变量
Java 系统属性
命令行参数(高优先级)

当排查某个值与预期配置值不一致的问题时,Spring Boot 配置加载的优先级就很重要。

环境变量受到操作系统、容器运行时和云平台的广泛支持。Spring Boot 会根据规范化属性名称推导环境变量名称:将点替换为下划线、移除短横线,并将结果转换为大写:

app.payment-timeout     -> APP_PAYMENT_TIMEOUT
spring.datasource.url   -> SPRING_DATASOURCE_URL

当一个属性在多个配置源中定义时,确定其有效值可能很困难。IntelliJ IDEA 可以将已解析的配置值以内联提示(inlay hints)的形式显示在编辑器中。选择某个提示可以识别提供该值的属性源,并显示该值是否被其他源(如环境变量或系统属性)覆盖。

IntelliJ IDEA 还提供属性声明、@ConfigurationProperties 成员和属性使用之间的导航。对于自定义配置属性,spring-boot-configuration-processor 生成的元数据增强了这一支持。

将机密信息存储在专用系统中

不要将密码、API 密钥、证书或私钥存储在源代码控制中。请使用诸如 HashiCorp VaultAWS Secrets ManagerGoogle Cloud Secret ManagerAzure Key Vault 等系统,或等效的平台服务。

确保机密信息不会出现在日志、错误消息、配置元数据和可公开访问的管理端点中。

注意: 在非生产环境中,Actuatorenv 端点可以帮助识别某个有效属性的来源。该端点不应公开暴露,因为配置可能包含敏感信息。

推荐的配置管理方式

没有一种适用于所有应用的单一配置管理方法。请根据应用的架构、部署环境和复杂性选择策略。

单体应用

对于单体应用,将共享默认值保留在应用中,仅在必要时使用特定于 profile 的配置文件,并通过环境变量提供特定于部署的覆盖值。将敏感值存储在专用的机密管理器中。

容器化工作负载

对于运行在 Kubernetes 等容器平台上的工作负载,请在应用中保留合理的默认值,并通过 ConfigMaps 提供特定于部署的配置。将机密信息单独存储在专用的机密管理系统中。

微服务

对于微服务架构,可以考虑使用 Spring Cloud Config Server 来集中管理配置、治理和版本控制。继续通过专用的机密管理系统管理机密信息。

总结

有效的应用配置始于合理的默认值、类型安全的 @ConfigurationProperties 和启动时校验。将特定于环境的值保留在应用之外,理解属性源的优先级,并将机密信息存储在专用的机密管理系统中。

正确的配置策略应反映应用的架构和部署环境。

在本地开发和远程调试期间,IntelliJ IDEA 通过显示已解析的属性值及其来源、突出显示覆盖,并提供配置文件与绑定的 Java 属性之间的导航,帮助你了解生效的配置。