Spring type-safe configuration properties

Using the @Value("${property}") annotation to inject configuration properties can sometimes be cumbersome, especially if you are dealing with multiple properties or your data is hierarchical. Spring Boot provides an alternative way of dealing with properties, letting strongly typed beans manage and validate your application's configuration.

See also the difference between @Value and typesafe configuration properties.

1. JavaBean property binding

As the following example shows, you can bind a bean that declares standard JavaBean properties.

Java

Kotlin

@ConfigurationProperties("my.service")
public class MyProperties {

    private boolean enabled;

    private InetAddress remoteAddress;

    private final Security security = new Security();

    // getters / setters...

    public static class Security {

        private String username;

        private String password;

        private List<String> roles = new ArrayList<>(Collections.singleton("USER"));

        // getters / setters...

    }

}

The preceding POJO defines the following properties.

  • my.service.enabled, default value is `false`.
  • my.service.remote-address, whose type can be provided by `String`.
  • my.service.security.username, there is a nested security object whose name is determined by the name of this property. In particular, there is absolutely no use of types there, which can be SecurityProperties.
  • my.service.security.password.
  • my.service.security.role, there is a collection of String, the default is USER.

Mapped to the properties of the @ConfigurationProperties class available in Spring Boot, configured through properties files, YAML files, environment variables, and other mechanisms. These properties are public APIs, but the getters/setters of the class itself do not mean that they can be used directly (a sentence In other words, Spring also sets the value through public methods such as getter/setter, so don't use it).

Such a design relies on a default no-argument constructor, and getters and setters are usually necessary because the binding is implemented through standard Java Beans property descriptors (Java introspection), just like in Spring MVC. The setter can be omitted in the following cases.

  • Maps, whenever they are initialized, need a getter, but not necessarily a setter, since they can be mutated by the binder.
  • Collections and arrays can be accessed by index (usually in YAML) or using a single comma-separated value (property). In the latter case, a setter is required. We recommend always adding a setter for such types. If you initialize a collection, make sure it is not immutable (as in the previous example).
  • If nested POJO properties are initialized (like the Security field in the previous example), no setter is needed. If you want the binder to create instances on the fly by using its default constructor, you need a setter.

Some people use Project Lombok to add getters and setters automatically. Please make sure that Lombok does not generate any specific constructor for such a type, as it is automatically used by the container to instantiate objects.

Finally, only standard Java Bean properties are considered, binding to static properties is not supported.

2. Constructor binding

The example from the previous section can be rewritten in an immutable way, as shown in the following example.

Java

@ConfigurationProperties("my.service")
public class MyProperties {

    // fields...

    public MyProperties(boolean enabled, InetAddress remoteAddress, Security security) {
        this.enabled = enabled;
        this.remoteAddress = remoteAddress;
        this.security = security;
    }

    // getters...

    public static class Security {

        // fields...

        public Security(String username, String password, @DefaultValue("USER") List<String> roles) {
            this.username = username;
            this.password = password;
            this.roles = roles;
        }

        // getters...

    }

}

In this setup, the presence of a unique "parameterized constructor" means that that constructor should be used for binding. This means that the binder will find a constructor with the parameters you wish to bind. If your class has multiple constructors, you can use the @ConstructorBinding annotation to specify which constructor to use for constructor binding. If you want to choose not to bind a constructor for a class with only one "parameterized constructor", the constructor must be annotated with @Autowired. Constructor binding can be used with Record. There is no need to use @ConstructorBinding unless your record has multiple constructors.

Nested members of constructor-bound classes (such as Security in the example above) will also be bound via their constructors.

Default values ​​can be specified using @DefaultValue on constructor parameters and Record components. The conversion service will be applied to coerce the annotation's String value to the target type for the missing attribute.

Referring to the previous example, if no properties are bound to the Security, the MyProperties instance will contain a null value of type security. To make it contain a non-null Security instance, even if no properties are bound to it (when using Kotlin, this would require Security's username and password parameters to be declared nullable, since they have no default value), use an empty @ DefaultValue annotation.

Java

Kotlin

public MyProperties(boolean enabled, InetAddress remoteAddress, @DefaultValue Security security) {
    this.enabled = enabled;
    this.remoteAddress = remoteAddress;
    this.security = security;
}

To use constructor binding, the class must be enabled using @EnableConfigurationProperties or configuration property scanning. You cannot use constructor binding on beans created through normal Spring mechanisms (such as @Component beans, beans created by using @Bean methods or beans loaded by using @Import ).

To use constructor binding in a native image, the class must be compiled with the -parameters argument. This is automatically configured if you use Spring Boot's Gradle plugin or use Maven with spring-boot-starter-parent.

It is not recommended to use java.util.Optional with @ConfigurationProperties as it is primarily used as a return type. Therefore, it is not suitable for configuration property injection. For consistency with other types of properties, if you do declare an Optional property, but it has no value, null instead of an empty Optional will be bound.

3. Enable the @ConfigurationProperties class

Spring Boot provides the infrastructure to bind @ConfigurationProperties types and register them as beans. You can enable configuration properties on a class-by-class basis, or enable configuration property scanning, which works similarly to component scanning.

Sometimes classes annotated with @ConfigurationProperties may not be suitable for scanning, for example if you are developing your own auto-configurations or you want to enable them conditionally. In these cases, use the @EnableConfigurationProperties annotation to specify the list of types to handle, which can be annotated on any @Configuration class, as shown in the following example.

Java

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(SomeProperties.class)
public class MyConfiguration {

}

Java

@ConfigurationProperties("some.properties")
public class SomeProperties {

}

To use configuration property scanning, add the @ConfigurationPropertiesScan annotation to your application. Typically, it is added to the main class annotated with @SpringBootApplication, but it can also be added to any @Configuration class. By default, the scan will start from the package where the annotation is located. If you want to customize the scan of other packages, you can refer to the following.

Java

@SpringBootApplication
@ConfigurationPropertiesScan({ "com.example.app", "com.example.another" })
public class MyApplication {

}

When a @ConfigurationProperties bean is scanned using configuration properties or registered via @EnableConfigurationProperties, the bean has a general name: <prefix>-<fqn>, where <prefix> is the environment key prefix specified in the @ConfigurationProperties annotation and <fqn> is the bean The fully qualified name of the . If the annotation does not provide any prefix, only the fully qualified name of the bean is used.

Assuming it's in the com.example.app package, the bean name for the SomeProperties example above is some.properties-com.example.app.SomeProperties.

We recommend that @ConfigurationProperties only deal with the environment, especially not to inject other beans from the context. For corner cases (special cases), you can use setter injection or any *Aware interface provided by the framework (such as EnvironmentAware, if you need to access Environment). If you still want to inject other beans using the constructor, the configuration property bean must be annotated with @Component and use JavaBean-based property binding.

Fourth, use the @ConfigurationProperties class

This configuration works particularly well with the SpringApplication external YAML configuration, as shown in the following example.

my:
  service:
    remote-address: 192.168.1.1
    security:
      username: "admin"
      roles:
      - "USER"
      - "ADMIN"

To use @ConfigurationProperties beans, you inject them in the same way as other beans, as shown in the following example.

Java

@Service
public class MyService {

    private final MyProperties properties;

    public MyService(MyProperties properties) {
        this.properties = properties;
    }

    public void openConnection() {
        Server server = new Server(this.properties.getRemoteAddress());
        server.start();
        // ...
    }

    // ...

}

Using @ConfigurationProperties also allows you to generate metadata files that can be used by IDEs for configuration property "autocompletion" functionality. See the appendix for details.

5. Third-party configuration

In addition to annotating a class with @ConfigurationProperties, you can also use it on public @Bean methods. This is especially useful when you want to bind properties to third-party components outside of your control.

To configure a bean from Environment properties, add @ConfigurationProperties to its bean registration, as shown in the following example.

Java

@Configuration(proxyBeanMethods = false)
public class ThirdPartyConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "another")
    public AnotherComponent anotherComponent() {
        return new AnotherComponent();
    }

}

Any JavaBean properties defined with the another prefix will be mapped to the AnotherComponent Bean in a manner similar to the SomeProperties example above.

6. Loose binding

Spring Boot uses some loose rules when binding Environment properties to @ConfigurationProperties beans, so there is no need for an exact match between Environment property names and bean property names. This is useful, common examples include dash-separated property names (for example, context-path binds to contextPath ), and uppercase property names (for example, PORT binds to port ).

To demonstrate an example, consider the following @ConfigurationProperties class.

Java

@ConfigurationProperties(prefix = "my.main-project.person")
public class MyPersonProperties {

    private String firstName;

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

}

For the above code, the following property names can be used.

Table 3. relaxed binding

Property

Note

my.main-project.person.first-name

Kebab style (separated by dashes), recommended to be used in .properties and YAML files.

my.main-project.person.firstName

Standard camelCase syntax.

my.main-project.person.first_name

Underscore, which is an alternate format for .properties and YAML files.

MY_MAINPROJECT_PERSON_FIRSTNAME

Uppercase format, it is recommended to use uppercase format when using system environment variables.

The prefix value of the annotation must be in kebab style (lowercase and separated by -, such as my.main-project.person).

Table 4. Relaxed binding rules for each property source

attribute source

simple

the list

Properties file

hump, kebab, underscore

Standard list syntax using [ ] or comma separated values

YAML file

hump, kebab, underscore

Standard YAML list syntax or comma-separated values

environment variable

Uppercase, with underscores as separators (see Binding from Environment Variables).

Numeric values ​​surrounded by underscores (see Binding from Environment Variables)

System properties

hump, kebab, underscore

Standard list syntax using [ ] or comma separated values

我们建议,在可能的情况下,属性应以小写的kebab格式存储,例如 my.person.first-name=Rod 。 

绑定Map

当绑定到 Map 属性时,你可能需要使用一个特殊的括号符号,以便保留原始的 key 值。 如果key没有被 [ ] 包裹,任何非字母数字、- 或 . 的字符将被删除。

例如,考虑将以下属性绑定到一个 Map<String,String>。

Properties

Yaml

my.map.[/key1]=value1
my.map.[/key2]=value2
my.map./key3=value3

对于YAML文件,括号需要用引号包裹,以使key被正确解析。

上面的属性将绑定到一个 Map ,/key1,/key2 和 key3 作为map的key。 斜线已经从 key3 中删除,因为它没有被方括号包裹。

当绑定到标量值时,带有 . 的键不需要用 [] 包裹。 标量值包括枚举和所有 java.lang 包中的类型,除了 Object 。 将 a.b=c 绑定到 Map<String, String> 将保留键中的 . ,并返回一个带有 {"a.b"="c"} Entry的Map。 对于任何其他类型,如果你的 key 包含 . ,你需要使用括号符号。 例如,将 a.b=c 绑定到 Map<String, Object> 将返回一个带有 {"a"={"b"="c"} entry的Map,而 [a.b]=c 将返回一个带有 {"a.b"="c"} entry 的Map。

从环境变量绑定

例如,Linux shell变量只能包含字母(a 到 z 或 A 到 Z )、数字( 0 到 9 )或下划线字符( _ )。 按照惯例,Unix shell变量的名称也将采用大写字母。

Spring Boot宽松的绑定规则被设计为尽可能地与这些命名限制兼容。

要将规范形式的属性名称转换为环境变量名称,你可以遵循这些规则。

  • 用下划线(_)替换点(.)。
  • 删除任何破折号(-)。
  • 转换为大写字母。

例如,配置属性 spring.main.log-startup-info 将是一个名为 SPRING_MAIN_LOGSTARTUPINFO 的环境变量。

环境变量也可以在绑定到对象列表(List)时使用。 要绑定到一个 List,在变量名称中,元素编号(索引)应该用下划线包裹。

例如,配置属性 my.service[0].other 将使用一个名为 MY_SERVICE_0_OTHER 的环境变量。

七、 合并复杂的类型

当List被配置在多个地方时,覆盖的作用是替换整个list。

例如,假设一个 MyPojo 对象的 name 和 description 属性默认为 null。 下面的例子从 MyProperties 中暴露了一个 MyPojo 对象的列表。

Java

@ConfigurationProperties("my")
public class MyProperties {

    private final List<MyPojo> list = new ArrayList<>();

    public List<MyPojo> getList() {
        return this.list;
    }

}

考虑以下配置。

Properties

Yaml

my.list[0].name=my name
my.list[0].description=my description
#---
spring.config.activate.on-profile=dev
my.list[0].name=my another name

如果 dev 配置文件未被激活,MyProperties.list 包含一个 MyPojo 条目,如之前定义的那样。 然而,如果 dev 配置文件被激活,list 仍然只包含一个条目(name 为 my another name,description为 null)。 这种配置不会在列表中添加第二个 MyPojo 实例,也不会合并项目。

当一个 List 在多个配置文件中被指定时,将使用具有最高优先级的那个(并且只有那个)。 考虑下面的例子。

Properties

Yaml

my.list[0].name=my name
my.list[0].description=my description
my.list[1].name=another name
my.list[1].description=another description
#---
spring.config.activate.on-profile=dev
my.list[0].name=my another name

在前面的例子中,如果 dev 配置文件是激活的,MyProperties.list 包含 一个 MyPojo 条目(name 是 my another name,description是 null)。 对于YAML,逗号分隔的列表和YAML列表都可以用来完全覆盖列表的内容。

对于 Map 属性,你可以用从多个来源获取的属性值进行绑定。 然而,对于多个来源中的同一属性,使用具有最高优先级的那个。 下面的例子从 MyProperties 暴露了一个 Map<String, MyPojo>。

Java

Kotlin

@ConfigurationProperties("my")
public class MyProperties {

    private final Map<String, MyPojo> map = new LinkedHashMap<>();

    public Map<String, MyPojo> getMap() {
        return this.map;
    }

}

考虑以下配置。

Properties

Yaml

my.map.key1.name=my name 1
my.map.key1.description=my description 1
#---
spring.config.activate.on-profile=dev
my.map.key1.name=dev name 1
my.map.key2.name=dev name 2
my.map.key2.description=dev description 2

如果 dev 配置文件没有激活,MyProperties.map 包含一个key为 key1 的条目(name为 my name 1 ,description为 my description 1 )。 然而,如果 dev 配置文件被激活,map 包含两个条目,key为 key1 (name为 dev name 1,description为 my description 1 )和 key2(name为 dev name 2,description为 dev description 2)。

前面的合并规则适用于所有属性源的属性,而不仅仅是文件。

八、属性(Properties)转换

当Spring Boot与 @ConfigurationProperties Bean绑定时,它试图将外部application properties强制改为正确的类型。 如果你需要自定义类型转换,你可以提供一个 ConversionService bean(Bean的名称为 conversionService )或自定义属性编辑器(通过 CustomEditorConfigurer bean)或自定义 Converters Bean(使用 @ConfigurationPropertiesBinding 注解)。

由于这个Bean是在应用程序生命周期的早期被请求的,请确保限制你的 ConversionService 所使用的依赖关系。 通常情况下,你所需要的任何依赖关系在创建时可能没有完全初始化。 如果你的自定义 ConversionService 不需要配置keys coercion,你可能想重命名它,并且只依赖用 @ConfigurationPropertiesBinding 限定的自定义转换器。

转换为 Duration

Spring Boot对表达持续时间有专门的支持。 如果你公开了一个 java.time.Duration 属性,application properties中的以下格式就可用。

  • 普通的 long (使用毫秒作为默认单位,除非指定了 @DurationUnit )。
  • 标准的ISO-8601格式 由 java.time.Duration 使用。
  • 一个更易读的格式,其中值和单位是耦合的(10s 表示10秒)。

请考虑以下例子。

Java

@ConfigurationProperties("my")
public class MyProperties {

    @DurationUnit(ChronoUnit.SECONDS)
    private Duration sessionTimeout = Duration.ofSeconds(30);

    private Duration readTimeout = Duration.ofMillis(1000);

    // getters / setters...

}

要指定一个30秒的会话超时, 30 、 PT30S 和 30s 都是等价的。 读取超时为500ms,可以用以下任何一种形式指定。 500, PT0.5S 和 500ms.

你也可以使用如下支持的时间单位。

  • ns 纳秒
  • us 微秒
  • ms 毫秒
  • s 秒
  • m 分
  • h 小时
  • d 天

默认单位是毫秒,可以使用 @DurationUnit 来重写,如上面的例子所示。

如果你喜欢使用构造函数绑定,同样的属性可以被暴露出来,如下面的例子所示。

Java

@ConfigurationProperties("my")
public class MyProperties {

    // fields...

    public MyProperties(@DurationUnit(ChronoUnit.SECONDS) @DefaultValue("30s") Duration sessionTimeout,
            @DefaultValue("1000ms") Duration readTimeout) {
        this.sessionTimeout = sessionTimeout;
        this.readTimeout = readTimeout;
    }

    // getters...

}

 如果你要升级一个 Long 的属性,如果它不是毫秒,请确保定义单位(使用 @DurationUnit )。 这样做提供了一个透明的升级路径,同时支持更丰富的格式

转换为期间(Period)

除了duration,Spring Boot还可以使用 java.time.Period 类型。 以下格式可以在application properties中使用。

  • 一个常规的 int 表示法(使用天作为默认单位,除非指定了 @PeriodUnit )。
  • 标准的ISO-8601格式 由 java.time.Period 使用。
  • 一个更简单的格式,其中值和单位对是耦合的( 1y3d 表示1年3天)。

支持下列简单的单位格式。

  • y 年
  • m 月
  • w 周
  • d 日

java.time.Period 类型实际上从未存储过周数,它是一个快捷方式,意味着 “7天”。

转换为数据大小(Data Sizes)

Spring Framework有一个 DataSize 值类型,以字节为单位表达大小。 如果你公开了一个 DataSize 属性,application properties中的以下格式就可用。

  • 一个常规的 long 表示(使用字节作为默认单位,除非指定了 @DataSizeUnit)。
  • 一个更易读的格式,其中值和单位是耦合的(10MB 意味着10兆字节)。

考虑以下例子。

Java

Kotlin

@ConfigurationProperties("my")
public class MyProperties {

    @DataSizeUnit(DataUnit.MEGABYTES)
    private DataSize bufferSize = DataSize.ofMegabytes(2);

    private DataSize sizeThreshold = DataSize.ofBytes(512);

    // getters/setters...

}

要指定一个10兆字节(Mb)的缓冲区大小, 10 和 10MB 是等价的。 256字节的大小阈值可以指定为 256 或 256B。

你也可以使用如下这些支持的单位。

  • B 字节
  • KB KB
  • MB MB
  • GB GB
  • TB TB

默认单位是字节,可以使用 @DataSizeUnit 来重写,如上面的例子所示。

如果你喜欢使用构造函数绑定,同样的属性可以被暴露出来,如下面的例子所示。

Java

@ConfigurationProperties("my")
public class MyProperties {

    // fields...

    public MyProperties(@DataSizeUnit(DataUnit.MEGABYTES) @DefaultValue("2MB") DataSize bufferSize,
            @DefaultValue("512B") DataSize sizeThreshold) {
        this.bufferSize = bufferSize;
        this.sizeThreshold = sizeThreshold;
    }

    // getters...

}

如果你正在升级一个 Long 属性,确保定义单位(使用 @DataSizeUnit),如果它不是字节。 这样做提供了一个透明的升级路径,同时支持更丰富的格式。 

九、@ConfigurationProperties 校验

只要使用Spring的 @Validated 注解,Spring Boot就会尝试验证 @ConfigurationProperties 类。 你可以直接在你的配置类上使用JSR-303的 jakarta.validation 约束注解。 要做到这一点,请确保你的classpath上有一个兼容的JSR-303实现,然后将约束注解添加到你的字段中,如下面的例子所示。

Java

@ConfigurationProperties("my.service")
@Validated
public class MyProperties {

    @NotNull
    private InetAddress remoteAddress;

    // getters/setters...

}

你也可以通过在 configuration properties 的 @Bean 方法上注解 @Validated 来触发验证。 

为了确保总是为嵌套的属性触发验证,即使没有找到属性,相关的字段必须用 @Valid 来注释。 下面的例子建立在前面的 MyProperties 的基础上。

Java

@ConfigurationProperties("my.service")
@Validated
public class MyProperties {

    @NotNull
    private InetAddress remoteAddress;

    @Valid
    private final Security security = new Security();

    // getters/setters...

    public static class Security {

        @NotEmpty
        private String username;

        // getters/setters...

    }

}

你也可以通过创建一个名为 configurationPropertiesValidator 的bean定义来添加一个自定义的Spring Validator。 @Bean 方法应该被声明为 static。 配置属性验证器是在应用程序生命周期的早期创建的,将 @Bean 方法声明为静态,可以让Bean的创建不需要实例化 @Configuration 类。 这样做可以避免过早实例化可能引起的任何问题。

spring-boot-actuator 模块包括一个暴露所有 @ConfigurationProperties Bean 的端点。 你可以通过浏览器访问 /actuator/configprops 或使用相应的JMX端点。 详情见"生产就绪"部分。

十、@ConfigurationProperties vs. @Value

@Value 注解是一个核心的容器功能,它不提供与类型安全的配置属性相同的功能。 下表总结了 @ConfigurationProperties 和 @Value 所支持的功能。

功能

@ConfigurationProperties

@Value

宽松绑定

Yes

有限制 (见 下文注释)

支持 Meta-data

Yes

No

SpEL 表达式

No

Yes

如果你确实想使用 @Value,我们建议你使用属性名称的规范形式(仅使用小写字母的kebab-case)来引用属性名称。 这将允许Spring Boot使用与 宽松绑定 @ConfigurationProperties 时相同的逻辑。

例如,@Value("${demo.item-price}") 将从 application.properties 文件中获取 demo.item-price 和 demo.itemPrice 形式,以及从系统环境中获取 DEMO_ITEMPRICE。 如果你用 @Value("${demo.itemPrice}") 代替,demo.item-price 和 DEMO_ITEMPRICE 将不会被考虑。

如果你为你自己的组件定义了一组配置键,我们建议你将它们分组在一个用 @ConfigurationProperties 注解的POJO中。 这样做将为你提供结构化的、类型安全的对象,你可以将其注入到你自己的bean中。

来自应用application property 文件的 SpEL 表达式在解析这些文件和填充environment时不会被处理。 然而,可以在 @Value 中写一个 SpEL 表达式。 如果来自应用程序属性文件的属性值是一个 SpEL 表达式,它将在被 @Value 消费时被解析。

Guess you like

Origin blog.csdn.net/leesinbad/article/details/131970671