数据绑定
数据绑定对于将用户输入绑定到目标对象非常有用,其中用户输入是一个以属性路径为键的映射,遵循 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
可以以人类可读的方式表示(作为 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
对象。 -
在 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 子类(如 Integer 、Long 、Float 或 Double )提供可定制的属性编辑器。默认由 BeanWrapperImpl 注册,但可以通过注册其自定义实例作为自定义编辑器来覆盖。 |
FileEditor | 将字符串解析为 java.io.File 对象。默认由 BeanWrapperImpl 注册。 |
InputStreamEditor | 单向属性编辑器,可以接受字符串并生成(通过中间的 ResourceEditor 和 Resource )一个 InputStream ,以便可以直接将 InputStream 属性设置为字符串。请注意,默认使用不会为您关闭 InputStream 。默认由 BeanWrapperImpl 注册。 |
LocaleEditor | 可以将字符串解析为 Locale 对象,反之亦然(字符串格式为 [language]_[country]_[variant] ,与 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
引用。另一种(稍微方便一些的)机制是使用一个特殊的 bean 工厂后处理器,称为 CustomEditorConfigurer
。虽然你可以在 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 属性分配为字符串,然后由 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
)实现。PropertyEditorRegistrar
实例在与 CustomEditorConfigurer
(在这里描述)结合使用时特别方便,它公开了一个名为 setPropertyEditorRegistrars(..)
的属性。以这种方式添加到 CustomEditorConfigurer
的 PropertyEditorRegistrar
实例可以很容易地与 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
注册代码封装在一个类中,然后在需要的多个控制器之间共享。