Spring Boot 配置教程

在上一篇 Spring Boot 测试教程 中,我们学习了如何为 Spring Boot 应用编写单元测试、片段测试和集成测试。

在本教程中,你将学习如何使用 propertiesYAML 文件配置 Spring Boot 应用程序,以便在不同环境中运行应用。

外部化 Spring Boot 的配置

大多数应用程序都会有一些配置项目,你希望这些项目是可以灵活配置的,而不是在程序代码中硬编码这些值。

Spring Boot 提供了许多 配置 application properties 的方法,其中我们最有可能使用的只有以下方法:

  • 默认 src/main/resources/application.{properties/yml} 中的属性值。
  • 特定 profile src/main/resources/application-{profile}.{properties/yml} 中的属性值。
  • 使用环境变量(Environment Variables)或系统属性(System Properties)覆盖默认配置。

在我们的应用程序中,通常会有两种配置属性:

  • Spring Boot 定义了用于配置 DataSource、Kafka 等服务的属性。例如:spring.datasource.urlspring.datasource.usernamespring.datasource.password 等。
  • 应用特定的配置属性。

配置默认属性

Spring Boot 默认从 src/main/resources/application.{properties/yml} 中加载属性。例如,我们可以使用 application.properties 文件配置应用属性,如下所示:

spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=secret123

ftp.host=ftpsrv001
ftp.port=21
ftp.username=appuser1
ftp.password=secret321

同样的配置值也可以使用 application.yml 文件进行配置,如下所示:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: secret123

ftp:
  host: ftpsrv001
  port: 21
  username: appuser1
  password: secret321

配置特定于 Profile 的属性

我们的应用可能会在不同的环境中运行,如本地、开发、QA、暂存和生产环境。我们可能希望为不同的环境配置不同的值。在这种情况下,我们可以使用 Spring 的 profile 概念来为不同环境配置不同的值。

假设我们想根据运行环境配置不同的数据库连接属性。我们可以在默认的 application.properties 文件中配置本地数据库参数。使用 qastaging properties 文件来配置 qastaging 环境的属性,如下所示:

application.properties:

# application.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=secret123

application-qa.properties:

# application-qa.properties
spring.datasource.url=jdbc:postgresql://qadb:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=secret123

application-staging.properties:

# application-staging.properties
spring.datasource.url=jdbc:postgresql://stagingdb:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=secret123

使用特定 profile 运行应用程序时,首先加载默认配置文件中的属性,然后会用特定 profile 中的属性覆盖它们。

与其为不同的 profile 创建单独的 properties 文件,还不如将所有配置文件属性放在 application.properties 文件中,并使用 "#---""!---" 分隔符将特定于 profile 的配置分开,如下所示:

application.properties:

spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=secret123

#---
spring.config.activate.on-profile=qa
spring.datasource.url=jdbc:postgresql://qadb:5432/postgres
spring.datasource.username=postgresqa
spring.datasource.password=secret1235

#---
spring.config.activate.on-profile=staging
spring.datasource.url=jdbc:postgresql://stagingdb:5432/postgres
spring.datasource.username=postgresstaging
spring.datasource.password=secret1236

如果使用 YAML 文件,则可以在同一个 application.yml 文件中使用 "---" 分隔符配置不同的 profile 属性,如下所示:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: secret123
---
spring:
  config:
    activate:
      on-profile: "qa"
  datasource:
    url: jdbc:postgresql://qadb:5432/postgres
    username: postgres
    password: secret1235

使用环境变量

一种常见的方法是在配置文件中定义默认属性和特定 profile 的属性,这些配置文件将打包到 jar 文件中,然后根据需要使用环境变量覆盖这些值。

Spring Boot 支持 宽松绑定(Relaxed Binding),允许我们使用环境变量,变量名以大写字母和下划线分隔,如下所示:

  • spring.datasource.url 属性值可由环境变量 SPRING_DATASOURCE_URL 覆盖。
  • app.jwt.expiryDuration 属性值可被环境变量 APP_JWT_EXPIRY_DURATION 覆盖。

如何在应用中使用配置的属性?

我们可以通过多种方式使用配置的属性值。在上述示例中,我们配置了 DataSource 属性,Spring Boot 将使用这些属性创建 DataSource Bean。

让我们看看如何使用已配置的 FTP server 详情。

使用 Environment

你可以使用 Spring 的 Environment 访问应用的所有属性。你可以注入 Environment Bean 并按如下方式访问属性:

import org.springframework.core.env.Environment;

@Service
class MyService {
    private final Environment environment;
    
    public MyService(Environment environment) {
        this.environment = environment;
    }
    
    void upload(File file) {
      String host = environment.getProperty("ftp.host", "localhost");
      Integer port = environment.getProperty("ftp.port", Integer.class, 21);
      String username = environment.getRequiredProperty("ftp.username");
      String password = environment.getRequiredProperty("ftp.password");
      ...
      ...
    }
}

如你所见,我们可以使用 environment.getProperty("...") 获取配置值。你还可以使用其他重载方法来指定默认值、属性的数据类型等。使用 environment.getRequiredProperty("...") 时,如果属性值为 null,则会抛出 IllegalStateException

Spring 的 Environment 会将代码与 Spring 框架耦合在一起,不如使用 @Value 注解直接注入属性值。

使用 @Value("${property.name}")

你可以使用 @Value 注解注入属性值,如下所示:

@Service
class MyService {
    private final String ftpHost;
    private final Integer ftpPort;
    private final String ftpUsername;
    private final String ftpPassword;
    
    public MyService(@Value("${ftp.host:localhost}") String ftpHost,
                     @Value("${ftp.port:21}") Integer ftpPort,
                     @Value("${ftp.username}") String ftpUsername,
                     @Value("${ftp.password}") String ftpPassword
                     ) {
        this.ftpHost = ftpHost;
        this.ftpPort = ftpPort;
        this.ftpUsername = ftpUsername;
        this.ftpPassword = ftpPassword;
    }
    
    void upload(File file) {
      ...
      ...
    }
}

我们使用了 Spring 属性引用语法 @Value("${property.name}") 来注入配置值。另外,我们还可以使用 @Value("${property.name:defaultValue}") 指定默认值,以注入配置值。

你也可以使用 “字段注入” 来注入属性值,但这是非常不推荐的。

@Service
class MyService {
    @Value("${ftp.host:localhost}")
    private String ftpHost;
    
    @Value("${ftp.port:21}")
    private Integer ftpPort;
    
    @Value("${ftp.username}")
    private String ftpUsername;
    
    @Value("${ftp.password}")
    private String ftpPassword;
    
}

使用 @Value("${property.name}") 方法虽然有效,但却非常啰嗦。还有另一种方法,即使用类型安全的配置属性绑定来使用配置的属性。

使用类型安全的配置属性绑定

与其使用 @Value 分别绑定每个属性值,我们可以创建一个包含属性的类,然后让 Spring 框架通过使用 @ConfigurationProperties 将配置值绑定到该配置属性类的实例中,如下所示:

@ConfigurationProperties(prefix = "ftp")
public class FtpProperties {
    private String host;
    private Integer port;
    private String username;
    private String password;
}

定义配置属性类后,我们需要使用 @EnableConfigurationProperties(FtpProperties.class) 启用绑定。这通常在应用程序 main 类中完成,如下所示:

@SpringBootApplication
@EnableConfigurationProperties(FtpProperties.class)
public class MyApplication {

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

我们可以拥有任意多的配置属性类,并使用 @EnableConfigurationProperties({FtpProperties.class, SomeProperties.class, OtherProperties.class}) 进行注册。也可以使用 @ConfigurationPropertiesScan 来扫描所有注解了 @ConfigurationProperties 的类并自动注册它们,而不是显式地指定每个配置属性类。

@SpringBootApplication
@ConfigurationPropertiesScan
public class MyApplication {

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

不过,一种常见的方法是使用一个配置属性类,并为不同的属性组使用嵌套类,如下所示:

@ConfigurationProperties(prefix = "app")
public class ApplicationProperties {
    private final FtpProperties ftp = new FtpProperties();
    private final Jwt jwt = new Jwt();
    
    public static class FtpProperties {
      private String host;
      private Integer port;
      private String username;
      private String password;
      //setters & getters
    }

  public static class Jwt {
    private String secret;
    private Long expiryDuration;
    //setters & getters
  }
  
}

现在,你可以使用 app.{prefix} 对 application properties 进行如下配置:

app.ftp.host=ftpsrv001
app.ftp.port=21
app.ftp.username=appuser1
app.ftp.password=secret321

app.jwt.secret=supersecret
app.jwt.expiryDuration=3600000

使用 Java Record 进行配置属性绑定

通常,一旦应用启动且 Spring 初始化了配置属性类,我们就不会更改该对象中的属性值。在这种情况下,Java Record 比普通类更适合。

让我们看看如何使用 Java Record 作为配置属性类。

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;

@ConfigurationProperties(prefix = "app")
public record ApplicationProperties(
    FtpProperties ftp,
    Jwt jwt){

    public record FtpProperties(
        @DefaultValue("localhost")
        String host,
        @DefaultValue("21")
        Integer port,
        String username,
        String password){
    }
    
    public record Jwt(
        String secret, 
        Long expiryDuration){
    }
}

使用 record 绑定配置属性将有助于防止意外修改值,因为你无法更改记录的值。

校验绑定的配置属性

我们可以使用 Java Bean Validation API 来验证配置属性值,以便在应用启动过程中出现配置无效的情况时能快速失效。

为了使用 Java Bean Validation API,我们需要添加以下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

现在,我们可以使用 Bean 验证注解,如 @NotNull@NotEmpty@Min@Max,如下所示:

import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@ConfigurationProperties(prefix = "app")
@Validated
public class ApplicationProperties {
    @Valid
    private final FtpProperties ftp = new FtpProperties();
    
    @Valid
    private final Jwt jwt = new Jwt();
    
    public static class FtpProperties {
      @NotEmpty  
      private String host;
      private Integer port;
      @NotEmpty
      private String username;
      @NotEmpty
      private String password;
      //setters & getters
    }

  public static class Jwt {
    @NotEmpty
    private String secret;
    @NotNull
    @Min(30000)
    private Long expiryDuration;
    //setters & getters
  }
  
}

请注意,为了触发验证,我们需要在顶层类上添加 @Validated 注解,而为了验证嵌套类属性的属性,我们需要在属性声明中添加 @Valid 注解。

高级配置选项

除上述方法外,Spring Boot 还支持更多高级选项来配置应用属性。

这些配置选项将在今后的教程中单独讲解。

总结

我们学习了如何使用默认属性、特定 profile 属性和环境变量将应用配置外部化。我们还学习了如何从应用程序中使用这些配置属性。最后,我们还学习了如何使用 Java Bean Validation API 注解验证应用配置属性值。


参考:https://www.sivalabs.in/spring-boot-application-configuration-tutorial/