数据绑定
数据绑定对于将用户输入绑定到目标对象很有用,其中用户输入是具有属性路径作为键的映射,遵循 JavaBeans 约定。DataBinder
是支持此功能的主要类,它提供两种绑定用户输入的方式
您可以同时应用构造函数和属性绑定,也可以只应用其中一个。
构造函数绑定
要使用构造函数绑定
-
创建一个目标对象为
null
的DataBinder
。 -
将
targetType
设置为目标类。 -
调用
construct
。
目标类应该只有一个公共构造函数或一个带有参数的非公共构造函数。如果有多个构造函数,则使用默认构造函数(如果存在)。
默认情况下,构造函数参数名称用于查找参数值,但您可以配置 NameResolver
。Spring MVC 和 WebFlux 都依赖于允许通过构造函数参数上的 @BindParam
注解自定义要绑定的值的名称。
类型转换 根据需要应用,以转换用户输入。如果构造函数参数是对象,则以相同方式递归地构造它,但通过嵌套属性路径。这意味着构造函数绑定创建目标对象及其包含的任何对象。
绑定和转换错误反映在 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。下表显示了这些约定的示例
表达式 | 说明 |
---|---|
|
表示对应于 |
|
表示对应于(例如) |
|
表示索引属性 |
|
表示 |
(如果您不打算直接使用BeanWrapper
,那么下一部分对您来说并不重要。如果您只使用DataBinder
和BeanFactory
及其默认实现,则应跳到关于PropertyEditor
的部分。)
以下两个示例类使用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 中使用属性编辑的示例
-
通过使用 `PropertyEditor` 实现来设置 bean 上的属性。当您在 XML 文件中声明的某个 bean 的属性值使用 `String` 时,Spring(如果对应属性的 setter 具有 `Class` 参数)将使用 `ClassEditor` 尝试将参数解析为 `Class` 对象。
-
在 Spring 的 MVC 框架中解析 HTTP 请求参数是通过使用各种 `PropertyEditor` 实现来完成的,您可以手动将它们绑定到 `CommandController` 的所有子类中。
Spring 有许多内置的 `PropertyEditor` 实现,可以简化开发。它们都位于 `org.springframework.beans.propertyeditors` 包中。大多数(但并非全部,如以下表格所示)默认情况下由 `BeanWrapperImpl` 注册。如果属性编辑器以某种方式可配置,您仍然可以注册自己的变体来覆盖默认的编辑器。下表描述了 Spring 提供的各种 `PropertyEditor` 实现。
类 | 说明 |
---|---|
|
用于字节数组的编辑器。将字符串转换为其对应的字节表示形式。默认情况下由 `BeanWrapperImpl` 注册。 |
|
解析表示类的字符串,将其转换为实际的类,反之亦然。如果找不到类,则会抛出 `IllegalArgumentException`。默认情况下,由 `BeanWrapperImpl` 注册。 |
|
用于 `Boolean` 属性的可定制属性编辑器。默认情况下,由 `BeanWrapperImpl` 注册,但可以通过注册其自定义实例作为自定义编辑器来覆盖它。 |
|
用于集合的属性编辑器,将任何源 `Collection` 转换为给定的目标 `Collection` 类型。 |
|
用于 `java.util.Date` 的可定制属性编辑器,支持自定义 `DateFormat`。默认情况下不注册。必须根据需要使用适当的格式进行用户注册。 |
|
任何 |
|
将字符串解析为 |
|
单向属性编辑器,可以接受字符串并生成(通过中间的 |
|
可以将字符串解析为 |
|
可以将字符串解析为 |
|
可以将字符串(以 |
|
修剪字符串的属性编辑器。可以选择将空字符串转换为 |
|
可以将URL的字符串表示形式解析为实际的 |
Spring使用java.beans.PropertyEditorManager
来设置可能需要的属性编辑器的搜索路径。搜索路径还包括sun.bean.editors
,其中包括针对Font
、Color
以及大多数基本类型等类型的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
以下 Java 源代码为引用的 SomethingBeanInfo
类关联了一个 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 工厂后处理器。虽然您可以将 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
中,然后该上下文将能够根据需要使用它
<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
注册代码封装在一个类中,然后在尽可能多的控制器之间共享。