Skip to content

数据绑定 (Data Binding)

数据绑定是将用户输入绑定到目标对象的功能,其中用户输入是一个以属性路径为键的 Map,遵循 JavaBeans 规范DataBinder 是支持此功能的主类,它提供了两种绑定用户输入的方式:

  • 构造函数绑定:将用户输入绑定到公共数据构造函数,在用户输入中查找构造函数参数值。
  • 属性绑定:将用户输入绑定到 setter 方法,将用户输入中的键与目标对象结构的属性进行匹配。

你可以同时应用构造函数绑定和属性绑定,也可以只应用其中一种。

构造函数绑定 (Constructor Binding)

使用构造函数绑定的步骤:

  1. 创建一个以 null 作为目标对象的 DataBinder
  2. targetType 设置为目标类。
  3. 调用 construct 方法。

目标类应具有单个公共构造函数或带有参数的单个非公共构造函数。如果存在多个构造函数,则使用默认构造函数(如果存在)。

默认情况下,参数值通过构造函数参数名称进行查找。Spring MVC 和 WebFlux 支持通过构造函数参数或字段上的 @BindParam 注解(如果存在)进行自定义名称映射。如果有必要,你还可以在 DataBinder 上配置 NameResolver 来自定义要使用的参数名称。

类型转换会根据需要应用于转换用户输入。如果构造函数参数是一个对象,它将以相同的方式通过嵌套属性路径递归构造。这意味着构造函数绑定会同时创建目标对象及其包含的任何对象。

构造函数绑定支持 ListMap 和数组参数,这些参数可以从单个字符串转换(例如逗号分隔的列表),也可以基于索引键(如 accounts[2].nameaccount[KEY].name)。

绑定和转换错误会反映在 DataBinderBindingResult 中。如果目标对象创建成功,则在调用 construct 后,target 将被设置为创建的实例。

使用 BeanWrapper 进行属性绑定

org.springframework.beans 包遵循 JavaBeans 标准。JavaBean 是具有默认无参数构造函数且遵循命名约定的类,例如名为 bingoMadness 的属性将具有 setter 方法 setBingoMadness(..) 和 getter 方法 getBingoMadness()。有关 JavaBeans 及其规范的更多信息,请参阅 javabeans

beans 包中一个非常重要的类是 BeanWrapper 接口及其对应的实现(BeanWrapperImpl)。正如 Javadoc 中所述,BeanWrapper 提供了设置和获取属性值(可以是单个或批量)、获取属性描述符以及查询属性以确定它们是否可读或可写的功能。此外,BeanWrapper 支持嵌套属性,能够以无限深度设置子属性上的属性。BeanWrapper 还支持添加标准的 JavaBeans PropertyChangeListenersVetoableChangeListeners,而无需在目标类中提供支持代码。最后但同样重要的一点是,BeanWrapper 支持设置索引属性。BeanWrapper 通常不直接由应用程序代码使用,而是由 DataBinderBeanFactory 使用。

BeanWrapper 的工作方式部分由其名称指示:它包装了一个 Bean,以对该 Bean 执行操作,例如设置和检索属性。

设置和获取基本及嵌套属性

属性的设置和获取是通过 BeanWrappersetPropertyValuegetPropertyValue 重载方法变体完成的。详情请参阅其 Javadoc。下表显示了这些规范的一些示例:

表达式说明
name指示与 getName()isName() 以及 setName(..) 方法对应的属性 name
account.name指示属性 account 的嵌套属性 name,对应于(例如)getAccount().setName()getAccount().getName() 方法。
accounts[2]指示索引属性 account第三个 元素。索引属性可以是数组、列表或其他自然有序的集合。
accounts[KEY]指示由 KEY 键索引的 Map 条目的值。

(如果你不打算直接使用 BeanWrapper,那么下一节对你来说并不至关重要。如果你仅使用 DataBinderBeanFactory 及其默认实现,你应该直接跳到 关于 PropertyEditors 的部分。)

以下两个示例类使用 BeanWrapper 来获取和设置属性:

java
public class Company {

	private String name;
	private Employee managingDirector;

	public String getName() {
		return this.name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Employee getManagingDirector() {
		return this.managingDirector;
	}

	public void setManagingDirector(Employee managingDirector) {
		this.managingDirector = managingDirector;
	}
}
kotlin
class Company {
	var name: String? = null
	var managingDirector: Employee? = null
}
java
public class Employee {

	private String name;
	private float salary;

	public String getName() {
		return this.name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public float getSalary() {
		return salary;
	}

	public void setSalary(float salary) {
		this.salary = salary;
	}
}
kotlin
class Employee {
	var name: String? = null
	var salary: Float? = null
}

以下代码片段展示了如何检索和操作已实例化的 CompanyEmployee 的一些属性:

java
BeanWrapper company = new BeanWrapperImpl(new Company());
// 设置公司名称..
company.setPropertyValue("name", "Some Company Inc.");
// ... 也可以这样写:
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);

// 好了,让我们创建主管并将其与公司关联:
BeanWrapper jim = new BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());

// 通过公司检索主管的薪水
Float salary = (Float) company.getPropertyValue("managingDirector.salary");
kotlin
val company = BeanWrapperImpl(Company())
// 设置公司名称..
company.setPropertyValue("name", "Some Company Inc.")
// ... 也可以这样写:
val value = PropertyValue("name", "Some Company Inc.")
company.setPropertyValue(value)

// 好了,让我们创建主管并将其与公司关联:
val jim = BeanWrapperImpl(Employee())
jim.setPropertyValue("name", "Jim Stravinsky")
company.setPropertyValue("managingDirector", jim.wrappedInstance)

// 通过公司检索主管的薪水
val salary = company.getPropertyValue("managingDirector.salary") as Float?

PropertyEditors (属性编辑器)

Spring 使用 PropertyEditor 的概念来实现 ObjectString 之间的转换。有时用不同于对象本身的方式来表示属性是很方便的。例如,Date 可以用人类可读的方式表示(如 String: '2007-14-09'),同时我们仍然可以将人类可读的形式转换回原始日期(甚至更好的是,将输入的任何人类可读格式转换回 Date 对象)。这种行为可以通过注册类型为 java.beans.PropertyEditor 的自定义编辑器来实现。在 BeanWrapper 或特定的 IoC 容器中注册自定义编辑器,可以使其了解如何将属性转换为所需类型。有关 PropertyEditor 的更多信息,请参阅 Oracle 的 java.beans 包 Javadoc

在 Spring 中使用属性编辑器的几个例子:

  • 在 Bean 上设置属性:通过使用 PropertyEditor 实现来完成。当你在 XML 文件中声明的某个 Bean 的属性中使用 String 作为值时,Spring(如果相应属性的 setter 具有 Class 参数)会使用 ClassEditor 尝试将该参数解析为 Class 对象。
  • 解析 HTTP 请求参数:在 Spring MVC 框架中,解析 HTTP 参数是通过使用各种 PropertyEditor 实现完成的,你可以在 CommandController 的子类中手动绑定它们。

Spring 具有许多内置的 PropertyEditor 实现,让开发变得轻松。它们都位于 org.springframework.beans.propertyeditors 包中。大多数(并非全部,如下表所示)默认由 BeanWrapperImpl 注册。在属性编辑器可以以某种方式配置的情况下,你仍然可以注册你自己的变体来覆盖默认编辑器。下表描述了 Spring 提供的各种 PropertyEditor 实现:

说明
ByteArrayPropertyEditor字节数组的编辑器。将字符串转换为其对应的字节表示。默认由 BeanWrapperImpl 注册。
ClassEditor解析代表类的字符串为实际类,反之亦然。当找不到类时,抛出 IllegalArgumentException。默认由 BeanWrapperImpl 注册。
CustomBooleanEditorBoolean 属性的可定制属性编辑器。默认由 BeanWrapperImpl 注册,但可以通过注册自定义实例来覆盖。
CustomCollectionEditor集合的属性编辑器,将任何源 Collection 转换为给定的目标 Collection 类型。
CustomDateEditorjava.util.Date 的可定制属性编辑器,支持自定义 DateFormat。默认注册,必须由用户根据需要使用适当的格式进行注册。
CustomNumberEditor任何 Number 子类(如 IntegerLongFloatDouble)的可定制属性编辑器。默认由 BeanWrapperImpl 注册,但可以被覆盖。
FileEditor将字符串解析为 java.io.File 对象。默认由 BeanWrapperImpl 注册。
InputStreamEditor单向属性编辑器,可以将字符串生成为一个 InputStream,这样 InputStream 属性就可以直接设置为字符串。注意默认用法不会为你关闭流。默认由 BeanWrapperImpl 注册。
LocaleEditor可以将字符串解析为 Locale 对象,反之亦然(字符串格式为 [language]_[country]_[variant],与 Locale.toString() 相同)。也接受空格作为分隔符。默认由 BeanWrapperImpl 注册。
PatternEditor可以将字符串解析为 java.util.regex.Pattern 对象,反之亦然。
PropertiesEditor可以将格式化字符串转换为 Properties 对象。默认由 BeanWrapperImpl 注册。
StringTrimmerEditor去除字符串前后空格。可选地允许将空字符串转换为 null 值。默认注册,必须由用户注册。
URLEditor可以将 URL 的字符串表示形式解析为实际的 URL 对象。默认由 BeanWrapperImpl 注册。

Spring 使用 java.beans.PropertyEditorManager 来设置所需的属性编辑器搜索路径。搜索路径还包括 sun.bean.editors,它包含 FontColor 和大多数基本类型的 PropertyEditor 实现。还要注意,标准的 JavaBeans 基础设施会自动发现 PropertyEditor 类,只要它们与所处理的类在同一个包中,且类名相同并在末尾追加 Editor。例如,以下结构足以让 SomethingEditor 被识别并作为 Something 类型属性的 PropertyEditor

text
com
  chank
    pop
      Something
      SomethingEditor // Something 类的属性编辑器

注意,你也可以在这里使用标准的 BeanInfo 获取机制(详见 Oracle 教程)。下面的示例使用 BeanInfo 机制显式为关联类的属性注册一个或多个 PropertyEditor 实例:

text
com
  chank
    pop
      Something
      SomethingBeanInfo // Something 类的 BeanInfo

以下 SomethingBeanInfo 类的代码将 CustomNumberEditorSomething 类的 age 属性关联起来:

java
public class SomethingBeanInfo extends SimpleBeanInfo {

	public PropertyDescriptor[] getPropertyDescriptors() {
		try {
			final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
			PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) {
				@Override
				public PropertyEditor createPropertyEditor(Object bean) {
					return numberPE;
				}
			};
			return new PropertyDescriptor[] { ageDescriptor };
		}
		catch (IntrospectionException ex) {
			throw new Error(ex.toString());
		}
	}
}
kotlin
class SomethingBeanInfo : SimpleBeanInfo() {

	override fun getPropertyDescriptors(): Array<PropertyDescriptor> {
		try {
			val numberPE = CustomNumberEditor(Int::class.java, true)
			val ageDescriptor = object : PropertyDescriptor("age", Something::class.java) {
				override fun createPropertyEditor(bean: Any): PropertyEditor {
					return numberPE
				}
			}
			return arrayOf(ageDescriptor)
		} catch (ex: IntrospectionException) {
			throw Error(ex.toString())
		}
	}
}

自定义 PropertyEditors

在将 Bean 属性设置为字符串值时,Spring IoC 容器最终使用标准的 JavaBeans PropertyEditor 实现将这些字符串转换为属性的复杂类型。Spring 预注册了许多自定义编辑器(例如将类名字符串转换为 Class 对象)。此外,Java 标准的 PropertyEditor 查找机制允许编辑器根据命名规范被自动找到。

如果需要注册其他的自定义 PropertyEditor,有几种机制可用。最原始的方法(通常不方便或不推荐)是使用 ConfigurableBeanFactory 接口的 registerCustomEditor() 方法。另一种稍微方便的方法是使用名为 CustomEditorConfigurer 的专用 Bean 工厂后处理器。虽然可以在 BeanFactory 实现中使用它,但 CustomEditorConfigurer 具有嵌套属性设置,因此强烈建议你在 ApplicationContext 中使用它,这样它可以被自动检测并应用。

注意,所有 Bean 工厂和应用上下文都会通过使用 BeanWrapper 处理属性转换来自动使用一系列内置属性编辑器。此外,ApplicationContext 还会重写或添加额外的编辑器,以适合特定应用上下文类型的方式处理资源查找。

你可以使用 CustomEditorConfigurer 方便地为 ApplicationContext 添加额外的 PropertyEditor 实例。

考虑以下示例,它定义了一个名为 ExoticType 的用户类,以及另一个依赖于它的 DependsOnExoticType 类:

java
package example;

public class ExoticType {

	private String name;

	public ExoticType(String name) {
		this.name = name;
	}
}

public class DependsOnExoticType {

	private ExoticType type;

	public void setType(ExoticType type) {
		this.type = type;
	}
}
kotlin
package example

class ExoticType(val name: String)

class DependsOnExoticType {

	var type: ExoticType? = null
}

我们希望能够将 type 属性分配为字符串,并让 PropertyEditor 将其转换为实际的 ExoticType 实例:

xml
<bean id="sample" class="example.DependsOnExoticType">
	<property name="type" value="aNameForExoticType"/>
</bean>

PropertyEditor 的实现可能如下:

java
package example;

import java.beans.PropertyEditorSupport;

// 将字符串表示转换为 ExoticType 对象
public class ExoticTypeEditor extends PropertyEditorSupport {

	public void setAsText(String text) {
		setValue(new ExoticType(text.toUpperCase()));
	}
}
kotlin
package example

import java.beans.PropertyEditorSupport

// 将字符串表示转换为 ExoticType 对象
class ExoticTypeEditor : PropertyEditorSupport() {

	override fun setAsText(text: String) {
		value = ExoticType(text.toUpperCase())
	}
}

最后,使用 CustomEditorConfigurerApplicationContext 注册它:

xml
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
	<property name="customEditors">
		<map>
			<entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
		</map>
	</property>
</bean>

PropertyEditorRegistrar

向 Spring 容器注册属性编辑器的另一种机制是使用 PropertyEditorRegistrar。当你在多种不同情况下需要使用同一组属性编辑器时,这个接口特别有用。你可以编写一个对应的注册器并在每种情况下重用它。PropertyEditorRegistrarPropertyEditorRegistry 接口配合使用,Spring 的 BeanWrapper(和 DataBinder)实现了该接口。

PropertyEditorRegistrarCustomEditorConfigurersetPropertyEditorRegistrars(..) 属性配合使用非常方便。以这种方式添加的注册器可以轻松地与 DataBinder 和 Spring MVC 控制器共享。此外,它避免了对自定义编辑器的同步需求:PropertyEditorRegistrar 期望为每次 Bean 创建尝试创建新的 PropertyEditor 实例。

以下示例显示了如何创建自己的 PropertyEditorRegistrar 实现:

java
package com.foo.editors.spring;

public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {

	public void registerCustomEditors(PropertyEditorRegistry registry) {

		// 预期创建新的 PropertyEditor 实例
		registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());

		// 你可以在此处根据需要注册任意数量的自定义编辑器...
	}
}
kotlin
package com.foo.editors.spring

import org.springframework.beans.PropertyEditorRegistrar
import org.springframework.beans.PropertyEditorRegistry

class CustomPropertyEditorRegistrar : PropertyEditorRegistrar {

	override fun registerCustomEditors(registry: PropertyEditorRegistry) {

		// 预期创建新的 PropertyEditor 实例
		registry.registerCustomEditor(ExoticType::class.java, ExoticTypeEditor())

		// 你可以在此处根据需要注册任意数量的自定义编辑器...
	}
}

另请参阅 org.springframework.beans.support.ResourceEditorRegistrar 以获取示例。注意它在 registerCustomEditors(..) 方法中如何为每个编辑器创建新实例。

下一例显示如何配置 CustomEditorConfigurer 并注入我们的注册器:

xml
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
	<property name="propertyEditorRegistrars">
		<list>
			<ref bean="customPropertyEditorRegistrar"/>
		</list>
	</property>
</bean>

<bean id="customPropertyEditorRegistrar"
	class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>

最后,对于使用 Spring MVC 框架的开发者,在数据绑定 Web 控制器中使用 PropertyEditorRegistrar 非常方便。以下示例在 @InitBinder 方法中使用它:

java
@Controller
public class RegisterUserController {

	private final PropertyEditorRegistrar customPropertyEditorRegistrar;

	RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
		this.customPropertyEditorRegistrar = propertyEditorRegistrar;
	}

	@InitBinder
	void initBinder(WebDataBinder binder) {
		this.customPropertyEditorRegistrar.registerCustomEditors(binder);
	}

	// 注册用户相关的其他方法
}
kotlin
@Controller
class RegisterUserController(
	private val customPropertyEditorRegistrar: PropertyEditorRegistrar) {

	@InitBinder
	fun initBinder(binder: WebDataBinder) {
		this.customPropertyEditorRegistrar.registerCustomEditors(binder)
	}

	// 注册用户相关的其他方法
}

这种风格的注册可以使代码更简洁,并将通用的编辑器注册逻辑封装在一个类中,供多个控制器共享。


补充教学

1. 现代趋势:构造函数绑定优于 setter 绑定

在 Java 16+ 引入 record 之后,构造函数绑定 变得越发重要。

  • Setter 绑定:要求对象是可变的(Mutable),且必须有无参构造函数。
  • 构造函数绑定:允许对象是不可变的(Immutable),例如 Spring Boot 的 @ConfigurationProperties 现在大量使用构造函数绑定。这提高了代码的安全性和线程安全性。

2. BeanWrapper 的幕后操作

当你调用 BeanWrapper.setPropertyValue("address.city", "Shanghai") 时,BeanWrapper 实际上执行了以下操作:

  1. 解析路径,发现需要先调用 getAddress()
  2. 如果 address 为 null,且开启了自动实例化,它会尝试创建 Address 对象。
  3. 找到 setCity(String) 方法。
  4. 如果有必要(例如输入是字符串而方法参数是枚举),调用注册的 PropertyEditor 进行转换。

3. PropertyEditor 的局限性与 ConversionService

虽然 PropertyEditor 依然在工作,但它有两个明显的缺点:

  1. 非线程安全PropertyEditor 通常维护状态(通过 value 属性),因此同一实例不能由多个线程并发使用。
  2. 局限于 String/Object 转换:它最初设计就是为了在 GUI 编辑器中处理字符串输入。

在 Spring 3.0 以后,引入了 ConversionService 系统,它是线程安全的,且支持任意两个类型之间的转换(例如 Long -> Date)。在高性能 Web 应用中,推荐优先使用 ConverterFormatter

4. @InitBinder 的作用域

在 Spring MVC 中,@InitBinder 默认只作用于当前的 Controller。如果你想要全局配置,应该创建一个带有 @ControllerAdvice 注解的类,并在其中定义 @InitBinder 方法。配合 PropertyEditorRegistrar,这能极大地减少冗余代码。

Based on Spring Framework.