使用@ConfigurationProperties配置Spring Boot模块

19-03-23 banq
         

SpringBoot应用经常需要一些参数,这些参数可以定义要连接的数据库,要支持的区域设置或要应用的日志记录级别。这些参数应该外部化(放在代码之外),这意味着我们不应该将它们变成可部署的工件,而是在启动应用程序时将它们作为命令行参数或配置文件提供。

通过@ConfigurationProperties注释,Spring引导提供了一种从应用程序代码中访问此类参数的便捷方法。

本教程将详细介绍此注释,并说明如何使用它来配置Spring Boot应用程序模块。

想象一下,我们正在我们的应用程序中构建一个负责发送电子邮件的模块。在本地测试中,我们不希望模块实际发送电子邮件,因此我们需要一个参数来禁用此功能。此外,我们希望能够为这些邮件配置默认主题,以便我们可以快速识别从测试环境发送的收件箱中的电子邮件。

Spring Boot提供了许多不同的选项 来将这些参数传递到应用程序中。在本文中,我们选择application.properties使用我们需要的参数创建一个文件:

myapp.mail.enabled=true
myapp.mail.default-subject=This is a Test

在我们的应用程序中,我们现在可以通过询问Spring的Environmentbean或使用@Value注释等来访问这些属性的值。

但是,通过创建一个使用@ConfigurationProperties注释的类来访问这些属性有一种更方便,更安全的方法:

@ConfigurationProperties(prefix = "myapp.mail")
class MailModuleProperties {

  private Boolean enabled = Boolean.TRUE;
  private String defaultSubject;

  // getters / setters
  
}

@ConfigurationProperties基本用法非常简单:我们为要捕获的每个外部属性提供一个包含字段的类。请注意以下事项:

  • 在prefix其外部特性将被结合到类的字段定义。
  • 根据Spring Boot的宽松绑定 规则,类的属性名称必须与外部属性的名称匹配。
  • 我们可以通过简单地用值初始化字段来定义默认值。
  • 类本身可以是包私有的。
  • 类的字段必须有公共setter。

如果我们将MailModuleProperties类型的一个bean注入另一个bean中,那么这个bean现在可以以类型安全的方式访问这些外部配置参数的值。

但是,我们仍然必须让我们的@ConfigurationProperties类被Spring指导,这样才能被加载到Spring上下文中。

激活 @ConfigurationProperties

Spring Boot要创建MailModuleProperties类的一个bean, 我们需要以几种方式之一将它添加到应用程序上下文中。

首先,我们可以通过添加@Component 注释简单地让它成为组件扫描的一部分:

@Component
@ConfigurationProperties(prefix = "myapp.mail")
class MailModuleProperties {
  // ...  
}

这显然只有在包内扫描Spring的@ComponentScan构造型注释时才有效,默认情况下是主应用程序类(@SpringBoot注册的主要入口类)包结构下面的任何类。

我们可以使用Spring的Java配置功能获得相同的结果:

@Configuration
class MailModuleConfiguration {

  @Bean
  public MailModuleProperties mailModuleProperties(){
    return new MailModuleProperties();
  }

}

只要MailModuleConfigurationSpring引导应用程序扫描类,我们就可以访问MailModuleProperties应用程序上下文中的bean。

或者,我们可以使用@EnableConfigurationProperties注释使我们的类以被Spring Boot知道:

@Configuration
@EnableConfigurationProperties(MailModuleProperties.class)
class MailModuleConfiguration {

}

哪个是激活@ConfigurationProperties类的最佳方式?

所有上述方式同样有效。但是,我建议 模块化你的应用程序,并让每个模块只拥有@ConfigurationProperties标注的类,该类只提供模块所需的属性,就像我们在上面代码中对邮件模块所做的那样。这使得在一个模块中重构属性变得容易,而不会影响其他模块。

出于这个原因,我不建议@EnableConfigurationProperties 在主应用程序类本身上使用,如许多其他教程中所示,而是在特定于模块的@Configuration类上,该类也可以使用包私有可见性来隐藏其余部分的属性。

失败的不可转换的属性

如果在我们application.properties中定义一个无法正确解释的属性会发生什么?假设我们为需要布尔值enabled属性提供值'foo':

myapp.mail.enabled=foo

默认情况下,Spring Boot将拒绝启动应用程序,报错异常:

java.lang.IllegalArgumentException: Invalid boolean value 'foo'

如果出于任何原因,我们不希望Spring Boot在这种情况下失败,我们可以将ignoreInvalidFields参数设置为true(默认为false):

@ConfigurationProperties(prefix = "myapp.mail", ignoreInvalidFields = true)
class MailModuleProperties {
  
  private Boolean enabled = Boolean.TRUE;
  
  // getters / setters
}

在这种情况下,Spring Boot会将enabled字段设置为我们在Java代码中定义的默认值。如果我们不在Java代码中初始化字段,那就是null。

未知属性失败

如果我们在我们的application.properties 文件中提供了我们MailModuleProperties类不知道的某些属性,会发生什么?

myapp.mail.enabled=true
myapp.mail.default-subject=This is a Test
myapp.mail.unknown-property=foo

默认情况下,Spring Boot将忽略无法绑定到@ConfigurationProperties类中字段的那些属性。

但是,如果配置文件中的某个属性实际上未绑定到@ConfigurationProperties类,我们则可能希望启动失败,就无法启动。也许我们以前使用过这个配置属性但是之后它已经被删除了,当它从application.properties文件中被删除时需要触发通知我们。

如果我们希望这样,也就是通过启动失败通知我们,让SpringBoot启动时碰到未知属性上就无法启动,我们可以简单地将ignoreUnknownFields 参数设置为false(默认为true):

@ConfigurationProperties(prefix = "myapp.mail", ignoreUnknownFields = false)
class MailModuleProperties {
  
  private Boolean enabled = Boolean.TRUE;
  private String defaultSubject;
  
  // getters / setters
}

我们现在将在应用程序启动时获得一个异常,它告诉我们某个属性无法绑定到我们MailModuleProperties类中的某个字段,因为没有匹配的字段:

org.springframework.boot.context.properties.bind.UnboundConfigurationPropertiesException:
  The elements [myapp.mail.unknown-property] were left unbound.

弃用警告

该参数ignoreUnknownFields将 在未来的Spring Boot版本中弃用。原因是我们可以将两个@ConfigurationProperties 类绑定到同一个名称空间。虽然我们有两个完全有效的配置,但是其中一个类可能已知某个属性,另一个属性未知,导致启动失败。

@ConfigurationProperties在启动时验证

如果我们想确保传递给应用程序的配置参数的参数是有效的,我们可以将bean验证 注释添加到字段中,并将@Validated注释添加到类本身:

@ConfigurationProperties(prefix = "myapp.mail")
@Validated
class MailModuleProperties {

  @NotNull private Boolean enabled = Boolean.TRUE;
  @NotEmpty private String defaultSubject;

  // getters / setters
}

如果我们现在忘记enabled在我们的application.properties文件中设置属性并保留defaultSubject空,我们将在启动时获得一个BindValidationException:

myapp.mail.default-subject=

org.springframework.boot.context.properties.bind.validation.BindValidationException: 
   Binding validation errors on myapp.mail
   - Field error in object 'myapp.mail' on field 'enabled': rejected value [null]; ...
   - Field error in object 'myapp.mail' on field 'defaultSubject': rejected value []; ...

如果我们需要验证默认bean注释不支持的验证,我们可以创建自定义bean验证注释

如果我们的验证逻辑对于bean验证来说太特殊了,我们可以在注释的方法中实现它,@PostConstruct如果验证失败则抛出异常。

复杂的属性类型

我们想要传递给应用程序的大多数参数都是原始字符串或数字。但是,在某些情况下,我们有一个参数,我们想要绑定到@ConfigurationProperty类中具有复杂数据类型(如List)的字段 。

1. 列表List和集合

想象一下,我们需要为邮件模块提供SMTP服务器列表。我们可以简单地List向我们的MailModuleProperties类添加一个字段:

@ConfigurationProperties(prefix = "myapp.mail")
class MailModuleProperties {

  private List<String> smtpServers;
  
  // getters / setters
  
}

如果我们在application.properties文件中使用数组表示法,Spring Boot会自动填充此列表List:

myapp.mail.smtpServers[0]=server1
myapp.mail.smtpServers[1]=server2

YAML内置了对列表类型的支持,所以如果我们使用的application.yml是配置文件,我们人类可以更好地阅读:

myapp:
  mail:
    smtp-servers:
      - server1
      - server2

我们可以以相同的方式将参数绑定到Set字段。

2.持续时间Duration

Spring Boot内置支持 从配置参数解析持续时间Duration :

@ConfigurationProperties(prefix = "myapp.mail")
class MailModuleProperties {

  private Duration pauseBetweenMails;
  
  // getters / setters
  
}

该Duration 持续时间可以被设置为很长,以指示毫秒或以文本包括所述单位的,人类可读的方式(ns,us,ms,s,m,h,d):

myapp.mail.pause-between-mails=5s

3. 文件大小

以非常类似的方式,我们可以提供定义文件大小的配置参数:

@ConfigurationProperties(prefix = "myapp.mail")
class MailModuleProperties {

  private DataSize maxAttachmentSize;
  
  // getters / setters
  
}

DataSize类型由Spring Framework本身提供。现在,我们可以提供一种文件大小配置参数作为长来表示的字节或用单位(B,KB,MB,GB,TB):

myapp.mail.max-attachment-size=1MB

4. 自定义类型

在极少数情况下,我们可能希望将配置参数解析为自定义值对象。想象一下,我们想要为电子邮件提供(假设的)最大附件重量:

myapp.mail.max-attachment-weight=5kg

我们想将此属性绑定到我们的自定义类型的字段Weight:

@ConfigurationProperties(prefix = "myapp.mail")
class MailModuleProperties {

  private Weight maxAttachmentWeight;
  
  // getters / setters
}

有两个轻量级选项可使Spring Boot自动将String('5kg')解析为类型为Weight的对象:

  • Weight类提供一个构造函数,单个字符串('5kg')作为其参数,或
  • Weight类提供了一个静态valueOf方法,采用单个字符串作为其参数并返回一个方法Weight的对象。

如果我们无法提供构造函数或valueOf方法,我们就会遇到创建自定义转换器的更具侵入性的选项:

class WeightConverter implements Converter<String, Weight> {

  @Override
  public Weight convert(String source) {
    // create and return a Weight object from the String
  }

}

一旦我们创建了转换器,我们必须让Spring Boot知道:

@Configuration
class MailModuleConfiguration {

  @Bean
  @ConfigurationPropertiesBinding
  public WeightConverter weightConverter() {
    return new WeightConverter();
  }

}

添加@ConfigurationPropertiesBinding注释以使Spring Boot知道在绑定配置属性期间需要此转换器非常重要。

使用Spring Boot配置处理器进行自动完成

想要任何Spring Boot的内置配置参数自动完成吗?或者您自己的配置属性?

Spring Boot提供了一个配置处理器从类路径中找到的所有@ConfigurationProperties注释的类中收集数据,然后创建带有一些元数据的JSON文件。IDE可以使用此JSON文件来提供自动完成等功能。

我们所要做的就是将配置处理器的依赖项添加到我们的项目中(gradle表示法):

dependencies {
  ...
  annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
}

当我们构建项目时,配置处理器现在创建一个类似于以下内容的JSON文件:

{
 "groups": [
   {
     "name": "myapp.mail",
     "type": "io.reflectoring.configuration.mail.MailModuleProperties",
     "sourceType": "io.reflectoring.configuration.mail.MailModuleProperties"
   }
 ],
 "properties": [
   {
     "name": "myapp.mail.enabled",
     "type": "java.lang.Boolean",
     "sourceType": "io.reflectoring.configuration.mail.MailModuleProperties",
     "defaultValue": true
   },
   {
     "name": "myapp.mail.default-subject",
     "type": "java.lang.String",
     "sourceType": "io.reflectoring.configuration.mail.MailModuleProperties"
   }
 ],
 "hints": []
}

IntelliJ:

要在IntelliJ中获得自动完成功能,我们只需安装Spring Assistant 插件即可。如果我们现在在一个application.properties或application.yml 文件中点击CMD + Space ,我们会得到一个自动完成弹出窗口。

Eclipse:

木有

将配置属性标记为已弃用

配置处理器的一个很好的功能是它允许我们将属性标记为已弃用:

@ConfigurationProperties(prefix = "myapp.mail")
class MailModuleProperties {
  
  private String defaultSubject;
  
  @DeprecatedConfigurationProperty(
      reason = "not needed anymore", 
      replacement = "none")
  public String getDefaultSubject(){
    return this.defaultSubject;
  }
  
  // setter
  
}

我们可以简单地将@DeprecatedConfigurationProperty注释添加到我们的@ConfigurationProperties类的字段中,配置处理器将在元数据中包含弃用信息:

...
{
  "name": "myapp.mail.default-subject",
  "type": "java.lang.String",
  "sourceType": "io.reflectoring.configuration.mail.MailModuleProperties",
  "deprecated": true,
  "deprecation": {
    "reason": "not needed anymore",
    "replacement": "none"
  }
}
...

结论

Spring Boot的@ConfigurationProperties注释是将配置参数绑定到Java bean中的类型安全字段的强大工具。

我们可以利用此功能为每个模块创建单独的配置bean,而不是简单地为我们的应用程序创建一个配置bean ,这使我们可以灵活地不仅在代码中而且在配置中单独地发展每个模块。

​​​​​​​