Spring Boot 2.0 中的属性绑定

工程 | Phil Webb | 2018 年 3 月 28 日 | ...

自 Spring Boot 首次发布以来,就可以通过使用 @ConfigurationProperties 注解将属性绑定到类。也可以以不同的形式指定属性名称。例如,person.first-nameperson.firstNamePERSON_FIRSTNAME 都可以互换使用。我们将此特性称为“宽松绑定”(relaxed binding)。

不幸的是,在 Spring Boot 1.x 中,“宽松绑定”有点过于宽松了。很难精确定义绑定规则以及何时可以使用特定格式。我们也开始收到一些问题报告,这些问题在使用 1.x 实现时很难修复。例如,在 Spring Boot 1.x 中,无法将条目绑定到 java.util.Set

因此,在 Spring Boot 2.0 中,我们着手重塑了绑定方式。我们添加了一些新的抽象,并开发了全新的绑定 API。在这篇博文中,我们将介绍一些新的类和接口,并描述为什么添加它们、它们做什么以及如何在您自己的代码中使用它们。

属性源

如果您使用 Spring 已有一段时间,您可能熟悉 Environment 抽象。这个接口是一个 PropertyResolver,它允许您从底层的一些 PropertySource 实现解析属性。

Spring Framework 为一些常见的东西提供了 PropertySource 实现,例如系统属性、命令行标志和属性文件。Spring Boot 会自动配置这些实现,以一种对大多数应用都合理的方式(例如,加载 application.properties)。

配置属性源

Spring Boot 2.0 没有直接使用现有的 PropertySource 接口进行绑定,而是引入了一个新的 ConfigurationPropertySource 接口。我们引入了一个新接口,为我们提供了一个逻辑位置来实现以前属于绑定器一部分的宽松绑定规则。

这个接口的主要 API 非常简单

ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name);

还有一个 IterableConfigurationPropertySource 变体,它实现了 Iterable<ConfigurationPropertyName>,这样您就可以发现源包含的所有名称。

您可以使用以下代码将 Spring Environment 适配为 ConfigurationPropertySources

Iterable<ConfigurationPropertySource> sources =
	ConfigurationPropertySources.get(environment);

如果您需要,我们还提供了一个简单的 MapConfigurationPropertySource 实现。

配置属性名称

事实证明,如果将其限制在一个方向,宽松属性名称的思想更容易实现。您在代码中应始终使用规范形式访问属性,而无论它们在底层源中如何表示。

ConfigurationPropertyName 类强制执行这些规范命名规则,这些规则基本上可以归结为“使用小写烤肉串(kebab-case)命名”。

因此,例如,您在代码中应该将属性称为 person.first-name,即使底层源中使用了 person.firstNamePERSON_FIRSTNAME

源信息支持

正如您所料,从 ConfigurationPropertySource 返回的 ConfigurationProperty 对象封装了实际的属性值,但它也可以包含一个可选的 Origin 对象。

Origin 是 Spring Boot 2.0 中引入的一个新接口,它允许您精确查明值的加载位置。Origin 有许多实现,其中最有用的可能是 TextResourceOrigin。它提供了加载的 Resource 的详细信息,以及值的行号和列号。

对于 .properties.yaml 文件,我们编写了自定义加载器,用于在加载文件时跟踪源信息。一些现有的 Spring Boot 功能已经被改造以利用源信息。例如,绑定器验证异常现在会显示无法绑定的值以及源信息。以下是故障分析器如何显示错误:

*************************** 应用启动失败 ***************************

描述

绑定到目标 org.springframework.boot.context.properties.bind.BindException: 无法将 'person' 下的属性绑定到 scratch.PersonProperties 失败

Property: person.name
Value: Joe
Origin: class path resource \[application.properties\]:1:13
Reason: length must be between 4 and 2147483647

操作

更新您的应用配置

绑定器 API

Binder 类(位于 org.springframework.boot.context.properties.bind)允许您使用一个或多个 ConfigurationPropertySource 并从中绑定某些内容。更精确地说,一个 Binder 接受一个 Bindable 并返回一个 BindResult

可绑定对象

一个 Bindable 可以是现有的 Java bean、一个类类型,或者一个复杂的 ResolvableType(例如 List<Person>)。以下是一些例子:

Bindable.ofInstance(existingBean);
Bindable.of(Integer.class);
Bindable.listOf(Person.class);
Bindable.of(resovableType);

Bindable 也用于携带注解信息,但您通常不必担心这一点。

绑定结果

绑定器不会直接返回绑定的对象,而是返回一个名为 BindResult 的东西。与 Java 8 Streams 返回 Optional 的方式类似,BinderResult 表示可能已绑定或未绑定的内容。

如果您尝试获取未绑定对象的实际结果,将抛出异常。我们还提供了方法,允许您在未绑定任何内容时提供备用值,或者 map 到不同的类型。

var bound = binder.bind("person.date-of-birth",
	Bindable.of(LocalDate.class));

// Return LocalDate or throws if not bound
bound.get();

// Return a formatted date or "No DOB"
bound.map(dateFormatter::format).orElse("No DOB");

// Return LocalDate or throws a custom exception
bound.orElseThrow(NoDateOfBirthException::new);

格式化和转换

大多数 ConfigurationPropertySource 实现将其底层值暴露为字符串。当 Binder 需要将源值转换为不同的类型时,它会委托给 Spring 的 ConversionService API。如果您需要调整值的转换方式,可以自由使用格式化注解,例如 @NumberFormat@DateFormat

Spring Boot 2.0 还引入了一些对绑定特别有用的新注解和转换器。例如,您现在可以将 4s 等值转换为 Duration。详细信息请参阅 org.springframework.boot.convert 包。

绑定处理器

有时,您可能需要在绑定时实现额外的逻辑,BindHandler 接口提供了一种很好的方式来完成此操作。每个 BindHandler 都有 onStartonSuccessonFailureonFinish 方法,可以实现这些方法来覆盖行为。

Spring Boot 提供了许多处理器,主要用于支持现有的 @ConfigurationProperties 绑定。例如,ValidationBindHandler 可用于对绑定的对象应用 Validator 验证。

@ConfigurationProperties

正如这篇博文开头所提到的,@ConfigurationProperties 自 Spring Boot 发布之初就是其特性之一。@ConfigurationProperties 很可能仍然是大多数人进行绑定的方式。

尽管我们重写了整个绑定过程,但大多数人在升级 Spring Boot 1.5 应用时似乎没有遇到太多问题。只要您遵循迁移指南中的建议,您应该会发现一切仍然工作正常。如果您在升级应用时确实发现问题,请在GitHub issue tracker 上报告,并附上一个能够重现问题的小示例。

未来工作

我们计划在 Spring Boot 2.1 中继续开发 Binder,我们想要支持的第一个特性是不可变的配置属性。如果当前需要 getter 和 setter 的配置属性可以使用基于构造函数的绑定方式,那将非常好。

public class Person {

	private final String firstName;
	private final String lastName;
	private final LocalDateTime dateOfBirth;

	public Person(String firstName, String lastName,
			LocalDateTime dateOfBirth) {
		this.firstName = firstName;
		this.lastName = lastName;
		this.dateOfBirth = dateOfBirth;
	}

	// getters

}

我们认为构造函数绑定与Kotlin 数据类也能很好地配合。如果您有兴趣跟踪此功能的进展,请订阅 issue #8762

总结

我们希望您觉得 Spring Boot 2.0 中的新绑定特性很有用,并考虑升级您现有的 Spring Boot 应用。

如果您想就绑定进行一般性讨论,或者有具体的增强建议或问题,请加入我们的 Gitter

获取 Spring 时事通讯

订阅 Spring 时事通讯,保持联系

订阅

抢先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获取支持

Tanzu Spring 通过一个简单的订阅提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看全部