Spring Boot 2.0 中的属性绑定

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

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

不幸的是,在 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

操作

更新您的应用程序配置

Binder API

Binder 类(位于 org.springframework.boot.context.properties.bind)允许您使用一个或多个 ConfigurationPropertySource 并从它们中进行绑定。更准确地说,Binder 接收一个 Bindable 并返回一个 BindResult

Bindable

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

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

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

BindResult

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

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

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 接口提供了一种很好的方法来做到这一点。每个 BindHandler 都有 onStartonSuccessonFailureonFinish 方法,可以实现这些方法来覆盖行为。

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

@ConfigurationProperties

正如本博文开头提到的,@ConfigurationProperties 自始至终都是 Spring Boot 的一项功能。大多数人很可能仍将 @ConfigurationProperties 作为执行绑定的方式。

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

未来工作

我们计划在 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 社区所有即将举行的活动。

查看所有