数据绑定
数据绑定(Data binding)对于将用户输入与目标对象进行关联非常有用,其中用户输入是一个以属性路径(property paths)作为键的映射(map),遵循JavaBeans规范。DataBinder是支持这一功能的主要类,它提供了两种方式来绑定用户输入:
你可以同时应用构造函数绑定和属性绑定,或者只应用其中之一。
构造函数绑定
要使用构造函数绑定:
- 创建一个
DataBinder,将null作为目标对象。 - 将
targetType设置为目标类。 - 调用
construct方法。
目标类应该有一个公共构造函数,或者有一个带参数的非公共构造函数。如果存在多个构造函数,则会使用默认构造函数(如果有的话)。
默认情况下,参数值是通过构造函数参数名称来查找的。Spring MVC 和 WebFlux 支持通过在构造函数参数或字段上使用 @BindParam 注解来自定义名称映射(如果存在的话)。如有必要,你还可以在 DataBinder 上配置一个 NameResolver 来自定义使用的参数名称。
类型转换会根据需要应用来转换用户输入。如果构造函数参数是一个对象,那么会以相同的方式递归地构建它,但通过嵌套的属性路径来进行。这意味着构造函数绑定不仅创建了目标对象,还会创建该对象所包含的任何其他对象。
构造函数绑定支持List、Map和数组类型的参数,这些参数可以由单个字符串转换而来(例如,逗号分隔的列表),也可以基于索引键来获取,比如accounts[2].name或account[KEY].name。
绑定和转换错误会反映在DataBinder的BindingResult中。如果目标对象成功创建,那么在调用construct方法之后,target将被设置为创建的实例。
使用 BeanWrapper 进行属性绑定
org.springframework.beans包遵循JavaBeans标准。JavaBean是一个具有默认无参数构造函数的类,其命名规则为:例如,名为bingoMadness的属性将拥有一个setter方法setBingoMadness(..)和一个getter方法getBingoMadness()。有关JavaBeans及其规范的更多信息,请参阅javabeans。
在beans包中,有一个相当重要的类,那就是BeanWrapper接口及其相应的实现类BeanWrapperImpl。根据javadoc的描述,BeanWrapper提供了设置和获取属性值(单独或批量)、获取属性描述符、以及查询属性以确定它们是否可读或可写的功能。此外,BeanWrapper还支持嵌套属性,能够对子属性进行无限深度的设置。BeanWrapper还支持添加标准的JavaBeans PropertyChangeListeners和VetoableChangeListeners,而无需在目标类中编写额外的支持代码。最后但同样重要的是,BeanWrapper还提供了对索引属性的支持。通常情况下,应用程序代码不会直接使用BeanWrapper,而是由DataBinder和BeanFactory来使用它。
BeanWrapper 的工作方式从它的名字中就可以部分地看出来:它包裹一个 Bean,以便对该 Bean 执行一些操作,比如设置和获取属性。
设置和获取基本属性及嵌套属性
设置和获取属性是通过BeanWrapper的setPropertyValue和.getPropertyValue这两个重载方法来完成的。详情请参阅它们的Javadoc文档。下表展示了一些使用这些方法的示例:
表1. 属性示例
| 表达式 | 说明 |
|---|---|
name | 表示与 getName() 或 isName() 以及 setName(..) 方法相对应的属性 name。 |
account.name | 表示属性 account 的嵌套属性 name,例如与 getAccount().setName() 或 getAccount().getName() 方法相对应。 |
accounts[2] | 表示索引属性 account 中的第三个元素。索引属性可以是 array、list 或其他自然有序的集合类型。 |
accounts[KEY] | 表示由 KEY 值索引的映射条目的值。 |
(如果你不打算直接使用BeanWrapper,那么接下来的这一部分对你来说并不是至关重要的。如果你只使用DataBinder和BeanFactory以及它们的默认实现,你可以直接跳到关于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
}
以下代码片段展示了一些如何检索和操作已实例化的Company(公司)和Employee(员工)对象的部分属性的示例:
- 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 的概念来实现 Object 与 String 之间的转换。有时候,以不同于对象本身的方式来表示属性会非常方便。例如,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 子类的属性编辑器(如 Integer、Long、Float 或 Double)。默认由 BeanWrapperImpl 注册,但可以通过注册其自定义实例来覆盖该编辑器。 |
FileEditor | 将字符串解析为 java.io.File 对象。默认由 BeanWrapperImpl 注册。 |
InputStreamEditor | 单向属性编辑器,可以将字符串通过中间层的 ResourceEditor 和 Resource 转换为 InputStream,以便可以直接将 InputStream 属性设置为字符串。注意,默认情况下不会关闭 InputStream。默认由 BeanWrapperImpl 注册。 |
LocaleEditor | 可以将字符串解析为 Locale 对象,反之亦然(字符串格式为 [语言]_[国家]_[变体],与 Locale 的 toString() 方法相同)。也接受空格作为分隔符,作为下划线的替代方案。默认由 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,其中包含了针对 Font、Color 以及大多数基本数据类型之类的属性的 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源代码,它将一个CustomNumberEditor与Something类的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工厂后处理器。虽然可以在任何实现了BeanFactory的类上使用bean工厂后处理器,但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 property)赋值为字符串,而 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 中,之后就可以根据需要使用它了:
<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 实现:
- 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.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:
- 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注册代码封装到一个类中,然后在需要的控制器之间共享。