跳到主要内容
版本:7.0.3

数据绑定

Hunyuan 7b 中英对照 Data Binding

数据绑定(Data binding)对于将用户输入与目标对象进行关联非常有用,其中用户输入是一个以属性路径(property paths)作为键的映射(map),遵循JavaBeans规范DataBinder是支持这一功能的主要类,它提供了两种方式来绑定用户输入:

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

  • 属性绑定 - 将用户输入与设置器(setters)绑定,将用户输入中的键与目标对象结构的属性相匹配。

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

构造函数绑定

要使用构造函数绑定:

  1. 创建一个DataBinder,将null作为目标对象。
  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 执行一些操作,比如设置和获取属性。

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

设置和获取属性是通过BeanWrappersetPropertyValue.getPropertyValue这两个重载方法来完成的。详情请参阅它们的Javadoc文档。下表展示了一些使用这些方法的示例:

表1. 属性示例

表达式说明
name表示与 getName()isName() 以及 setName(..) 方法相对应的属性 name
account.name表示属性 account 的嵌套属性 name,例如与 getAccount().setName()getAccount().getName() 方法相对应。
accounts[2]表示索引属性 account 中的第三个元素。索引属性可以是 arraylist 或其他自然有序的集合类型。
accounts[KEY]表示由 KEY 值索引的映射条目的值。

(如果你不打算直接使用BeanWrapper,那么接下来的这一部分对你来说并不是至关重要的。如果你只使用DataBinderBeanFactory以及它们的默认实现,你可以直接跳到关于PropertyEditors的部分。)

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

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

以下代码片段展示了一些如何检索和操作已实例化的Company(公司)和Employee(员工)对象的部分属性的示例:

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");

PropertyEditor\

Spring 使用了 PropertyEditor 的概念来实现 ObjectString 之间的转换。有时候,以不同于对象本身的方式来表示属性会非常方便。例如,Date 对象可以以人类可读的形式(即字符串形式)进行表示(如 '2007-14-09'),同时我们仍然可以将这种人类可读的形式转换回原始的 Date 对象(或者更好的是,将任何以人类可读形式输入的日期转换回 Date 对象)。通过注册类型为 java.beans.PropertyEditor 的自定义编辑器,就可以实现这种功能。在 BeanWrapper 上注册自定义编辑器,或者如前一章所提到的,在特定的 IoC 容器中注册,就可以让该容器掌握如何将属性转换为所需的类型。有关 PropertyEditor 的更多信息,请参阅 Oracle 提供的 java.beans 包的 JavaDoc

以下是Spring中使用属性编辑的几个示例:

  • 通过使用PropertyEditor实现来为bean设置属性。当你在XML文件中声明某个bean的属性时,如果该属性的setter方法包含一个Class参数,Spring会使用ClassEditor尝试将该参数解析为一个Class对象。

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

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 对象,反之亦然(字符串格式为 [语言]_[国家]_[变体],与 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 实现。还需要注意的是,如果 PropertyEditor 类与它所处理的类位于同一个包中,并且类名后面加上了 “Editor” 这个后缀,那么标准的 JavaBeans 基础设施会自动发现这些 PropertyEditor 类(无需你显式进行注册)。例如,可以有以下类和包结构,这样的结构就足以让 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属性关联起来:

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

自定义 PropertyEditor

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

如果需要注册其他自定义的PropertyEditors,有几种可供选择的机制。最繁琐的方法是使用ConfigurableBeanFactory接口的registerCustomEditor()方法,但这种做法通常不太方便,也不被推荐(除非你确实有BeanFactory的引用)。另一种稍微方便一些的方法是使用一种名为CustomEditorConfigurer的特殊bean工厂后处理器。虽然可以在任何实现了BeanFactory的类上使用bean工厂后处理器,但CustomEditorConfigurer具有嵌套的属性设置机制,因此我们强烈建议将其与ApplicationContext一起使用:你可以像部署其他bean一样部署它,它会自动被检测并应用。

请注意,所有的bean工厂和应用上下文都会自动使用一些内置的属性编辑器,它们通过使用BeanWrapper来处理属性转换。BeanWrapper注册的标准属性编辑器在前一节中有列出。此外,ApplicationContext还会根据具体的应用上下文类型,覆盖或添加额外的编辑器来处理资源查找。

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

考虑以下示例,该示例定义了一个名为ExoticType的用户类,以及另一个名为DependsOnExoticType的类,后者需要将ExoticType设置为其属性:

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

当一切设置妥当后,我们希望能够将类型属性(type property)赋值为字符串,而 PropertyEditor 会将这个字符串转换成实际的 ExoticType 实例。以下 bean 定义展示了如何建立这种关系:

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

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

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

最后,以下示例展示了如何使用 CustomEditorConfigurer 将新的 PropertyEditor 注册到 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)实现。当与CustomEditorConfigurer(在此处中有描述)一起使用时,PropertyEditorRegistrar实例尤为方便,因为CustomEditorConfigurer暴露了一个名为setPropertyEditorRegistrars(..)的属性。以这种方式添加到CustomEditorConfigurer中的PropertyEditorRegistrar实例可以很容易地与DataBinder和Spring MVC控制器共享。此外,这种方法还避免了需要对自定义编辑器进行同步操作:PropertyEditor Registrar预期会在每次创建bean时都创建新的PropertyEditor实例。

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

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...
}
}

另请参阅org.springframework.beans.support.ResourceEditor Registrar,以了解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

@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
}

这种PropertyEditor的注册方式可以使得代码更加简洁(@InitBinder方法的实现只有一行长),并且可以将常见的PropertyEditor注册代码封装到一个类中,然后在需要的控制器之间共享。