数据绑定

数据绑定对于将用户输入绑定到目标对象很有用,其中用户输入是一个以属性路径作为键的映射,遵循JavaBeans 约定DataBinder 是支持此功能的主要类,它提供两种方式来绑定用户输入

  • 构造函数绑定 - 将用户输入绑定到公共数据构造函数,在用户输入中查找构造函数参数值。

  • 属性绑定 - 将用户输入绑定到 setter 方法,将用户输入中的键与目标对象结构的属性匹配。

您可以同时应用构造函数和属性绑定,或只应用其中一个。

构造函数绑定

要使用构造函数绑定

  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。下表显示了这些约定的某些示例

表 1. 属性示例
表达式 说明

name

指示对应于getName()isName()setName(..)方法的属性name

account.name

指示对应于(例如)getAccount().setName()getAccount().getName()方法的属性account的嵌套属性name

accounts[2]

指示索引属性account第三个元素。索引属性可以是arraylist或其他自然排序的集合类型。

accounts[KEY]

指示由KEY值索引的映射条目的值。

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

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

  • Java

  • Kotlin

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;
	}
}
class Company {
	var name: String? = null
	var managingDirector: Employee? = null
}
  • Java

  • Kotlin

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;
	}
}
class Employee {
	var name: String? = null
	var salary: Float? = null
}

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

  • Java

  • Kotlin

BeanWrapper company = new BeanWrapperImpl(new Company());
// setting the company name..
company.setPropertyValue("name", "Some Company Inc.");
// ... can also be done like this:
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);

// ok, let's create the director and tie it to the company:
BeanWrapper jim = new BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());

// retrieving the salary of the managingDirector through the company
Float salary = (Float) company.getPropertyValue("managingDirector.salary");
val company = BeanWrapperImpl(Company())
// setting the company name..
company.setPropertyValue("name", "Some Company Inc.")
// ... can also be done like this:
val value = PropertyValue("name", "Some Company Inc.")
company.setPropertyValue(value)

// ok, let's create the director and tie it to the company:
val jim = BeanWrapperImpl(Employee())
jim.setPropertyValue("name", "Jim Stravinsky")
company.setPropertyValue("managingDirector", jim.wrappedInstance)

// retrieving the salary of the managingDirector through the company
val salary = company.getPropertyValue("managingDirector.salary") as Float?

PropertyEditor

Spring 使用PropertyEditor的概念来实现ObjectString之间的转换。以不同于对象本身的方式表示属性可能很方便。例如,Date 可以以人类可读的方式表示(作为字符串:'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 对象。

  • 在 Spring 的 MVC 框架中解析 HTTP 请求参数是通过使用各种 PropertyEditor 实现来完成的,您可以手动将这些实现绑定到 CommandController 的所有子类中。

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

表 2. 内置 PropertyEditor 实现
说明

ByteArrayPropertyEditor

用于字节数组的编辑器。将字符串转换为其对应的字节表示形式。默认情况下由 BeanWrapperImpl 注册。

ClassEditor

解析表示类的字符串为实际类,反之亦然。如果找不到类,则会抛出 IllegalArgumentException。默认情况下,由 BeanWrapperImpl 注册。

CustomBooleanEditor

Boolean 属性的可定制属性编辑器。默认情况下,由 BeanWrapperImpl 注册,但可以通过注册其自定义实例作为自定义编辑器来覆盖。

CustomCollectionEditor

用于集合的属性编辑器,将任何源 Collection 转换为给定的目标 Collection 类型。

CustomDateEditor

java.util.Date 的可定制属性编辑器,支持自定义 DateFormat。默认情况下未注册。必须根据需要由用户注册并使用相应的格式。

CustomNumberEditor

任何 Number 子类的可定制属性编辑器,例如 IntegerLongFloatDouble。默认情况下,由 BeanWrapperImpl 注册,但可以通过注册其自定义实例作为自定义编辑器来覆盖。

FileEditor

将字符串解析为 java.io.File 对象。默认情况下,由 BeanWrapperImpl 注册。

InputStreamEditor

单向属性编辑器,可以接收字符串并生成(通过中间的 ResourceEditorResource)一个 InputStream,以便可以直接将 InputStream 属性设置为字符串。请注意,默认用法不会为您关闭 InputStream。默认情况下,由 BeanWrapperImpl 注册。

LocaleEditor

可以将字符串解析为 Locale 对象,反之亦然(字符串格式为 [language]_[country]_[variant],与 LocaletoString() 方法相同)。还可以接受空格作为分隔符,作为下划线的替代方案。默认情况下,由 BeanWrapperImpl 注册。

PatternEditor

可以将字符串解析为 java.util.regex.Pattern 对象,反之亦然。

PropertiesEditor

可以将字符串(使用 java.util.Properties 类 javadoc 中定义的格式)转换为 Properties 对象。默认情况下,由 BeanWrapperImpl 注册。

StringTrimmerEditor

修剪字符串的属性编辑器。可以选择将空字符串转换为 null 值。默认情况下未注册——必须由用户注册。

URLEditor

可以将 URL 的字符串表示形式解析为实际的 URL 对象。默认情况下,由 BeanWrapperImpl 注册。

Spring 使用 java.beans.PropertyEditorManager 设置可能需要的属性编辑器的搜索路径。搜索路径还包括 sun.bean.editors,其中包含 FontColor 和大多数基本类型的 PropertyEditor 实现。另请注意,如果标准 JavaBeans 基础结构中的 PropertyEditor 类与它们处理的类的包相同,并且与该类具有相同的名称(附加 Editor),则会自动发现这些类(无需您显式注册)。例如,可以有以下类和包结构,这对于 SomethingEditor 类被识别并用作 Something 类型属性的 PropertyEditor 就足够了。

com
  chank
    pop
      Something
      SomethingEditor // the PropertyEditor for the Something class

请注意,您也可以在此处使用标准的 BeanInfo JavaBeans 机制(在此处进行了某种程度的描述)。以下示例使用 BeanInfo 机制显式地将一个或多个 PropertyEditor 实例与关联类的属性注册。

com
  chank
    pop
      Something
      SomethingBeanInfo // the BeanInfo for the Something class

引用的 SomethingBeanInfo 类的以下 Java 源代码将 CustomNumberEditorSomething 类的 age 属性关联。

  • Java

  • Kotlin

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());
		}
	}
}
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())
		}

	}
}

自定义 PropertyEditor

在将 Bean 属性设置为字符串值时,Spring IoC 容器最终会使用标准 JavaBeans PropertyEditor 实现将这些字符串转换为属性的复杂类型。Spring 预先注册了许多自定义 PropertyEditor 实现(例如,将表示为字符串的类名转换为 Class 对象)。此外,Java 的标准 JavaBeans PropertyEditor 查找机制允许为某个类命名 PropertyEditor 并将其放置在提供支持的类的同一个包中,以便可以自动找到它。

如果需要注册其他自定义 PropertyEditors,则可以使用多种机制。最手动的方法(通常不方便或不推荐)是使用 ConfigurableBeanFactory 接口的 registerCustomEditor() 方法,假设您有 BeanFactory 引用。另一种(稍微更方便)的机制是使用一个称为 CustomEditorConfigurer 的特殊 Bean 工厂后处理器。尽管您可以将 Bean 工厂后处理器与 BeanFactory 实现一起使用,但 CustomEditorConfigurer 具有嵌套属性设置,因此我们强烈建议您将其与 ApplicationContext 一起使用,您可以在其中以与任何其他 Bean 相同的方式部署它,并且可以自动检测和应用它。

请注意,所有 Bean 工厂和应用程序上下文都通过使用 BeanWrapper 处理属性转换来自动使用许多内置属性编辑器。BeanWrapper 注册的标准属性编辑器在上一节中列出。此外,ApplicationContext 还覆盖或添加其他编辑器以处理资源查找,其方式适合于特定的应用程序上下文类型。

标准 JavaBeans PropertyEditor 实例用于将表示为字符串的属性值转换为属性的实际复杂类型。您可以使用 CustomEditorConfigurer(一个 Bean 工厂后处理器)方便地为 ApplicationContext 添加对其他 PropertyEditor 实例的支持。

考虑以下示例,它定义了一个名为 ExoticType 的用户类和另一个名为 DependsOnExoticType 的类,该类需要 ExoticType 作为属性设置。

  • Java

  • Kotlin

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;
	}
}
package example

class ExoticType(val name: String)

class DependsOnExoticType {

	var type: ExoticType? = null
}

当设置正确时,我们希望能够将 type 属性分配为字符串,PropertyEditor 将其转换为实际的 ExoticType 实例。以下 Bean 定义显示了如何设置此关系。

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

PropertyEditor 实现可能类似于以下内容。

  • Java

  • Kotlin

package example;

import java.beans.PropertyEditorSupport;

// converts string representation to ExoticType object
public class ExoticTypeEditor extends PropertyEditorSupport {

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

import java.beans.PropertyEditorSupport

// converts string representation to ExoticType object
class ExoticTypeEditor : PropertyEditorSupport() {

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

最后,以下示例显示了如何使用 CustomEditorConfigurer 将新的 PropertyEditor 注册到 ApplicationContext,然后 ApplicationContext 将能够根据需要使用它。

<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。当您需要在几种不同的情况下使用相同的属性编辑器集时,此接口特别有用。您可以编写相应的注册器并在每种情况下重用它。PropertyEditorRegistrar 实例与名为 PropertyEditorRegistry 的接口一起工作,该接口由 Spring BeanWrapper(和 DataBinder)实现。PropertyEditorRegistrar 实例在与 CustomEditorConfigurer此处进行了描述)结合使用时特别方便,后者公开了一个名为 setPropertyEditorRegistrars(..) 的属性。以这种方式添加到 CustomEditorConfigurerPropertyEditorRegistrar 实例可以轻松地与 DataBinder 和 Spring MVC 控制器共享。此外,它避免了对自定义编辑器进行同步的需要:PropertyEditorRegistrar 预计会为每次 Bean 创建尝试创建新的 PropertyEditor 实例。

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

  • Java

  • Kotlin

package com.foo.editors.spring;

public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {

	public void registerCustomEditors(PropertyEditorRegistry registry) {

		// it is expected that new PropertyEditor instances are created
		registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());

		// you could register as many custom property editors as are required here...
	}
}
package com.foo.editors.spring

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

class CustomPropertyEditorRegistrar : PropertyEditorRegistrar {

	override fun registerCustomEditors(registry: PropertyEditorRegistry) {

		// it is expected that new PropertyEditor instances are created
		registry.registerCustomEditor(ExoticType::class.java, ExoticTypeEditor())

		// you could register as many custom property editors as are required here...
	}
}

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

下一个示例显示了如何配置 CustomEditorConfigurer 并将我们 CustomPropertyEditorRegistrar 的实例注入其中。

<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 与数据绑定 Web 控制器结合使用非常方便。以下示例在 @InitBinder 方法的实现中使用 PropertyEditorRegistrar

  • Java

  • Kotlin

@Controller
public class RegisterUserController {

	private final PropertyEditorRegistrar customPropertyEditorRegistrar;

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

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

	// other methods related to registering a User
}
@Controller
class RegisterUserController(
	private val customPropertyEditorRegistrar: PropertyEditorRegistrar) {

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

	// other methods related to registering a User
}

这种 PropertyEditor 注册风格可以带来简洁的代码(@InitBinder 方法的实现只有一行),并且允许将常见的 PropertyEditor 注册代码封装在一个类中,然后在任意数量的控制器之间共享。