数据绑定
数据绑定对于将用户输入绑定到目标对象非常有用,其中用户输入是一个带有属性路径作为键的映射,遵循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
还支持添加标准的JavaBeansPropertyChangeListeners
和VetoableChangeListeners
,而无需在目标类中编写支持代码。最后,BeanWrapper
提供了设置索引属性的支持。通常,BeanWrapper
不会被应用程序代码直接使用,而是被DataBinder
和BeanFactory
使用。
BeanWrapper
的工作方式部分由其名称指示:它包装一个bean以对该bean执行操作,如设置和检索属性。
设置和获取基本和嵌套属性
通过BeanWrapper
的setPropertyValue
和getPropertyValue
重载方法变体来设置和获取属性。详细信息请参阅它们的Javadoc。下表显示了这些约定的一些示例:
表达式 | 解释 |
---|---|
|
表示与 |
|
表示属性 |
|
表示属性 |
|
表示 |
(如果您不打算直接使用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());
// 设置公司名称..
company.setPropertyValue("name", "Some Company Inc.");
// ...也可以这样做:
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);
// 好的,让我们创建董事并将其与公司关联:
BeanWrapper jim = new BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());
// 通过公司检索董事的薪水
Float salary = (Float) company.getPropertyValue("managingDirector.salary");
val company = BeanWrapperImpl(Company())
// 设置公司名称..
company.setPropertyValue("name", "Some Company Inc.")
// ...也可以这样做:
val value = PropertyValue("name", "Some Company Inc.")
company.setPropertyValue(value)
// 好的,让我们创建董事并将其与公司关联:
val jim = BeanWrapperImpl(Employee())
jim.setPropertyValue("name", "Jim Stravinsky")
company.setPropertyValue("managingDirector", jim.wrappedInstance)
// 通过公司检索董事的薪水
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
实现:
类 | 说明 |
---|---|
|
用于字节数组的编辑器。将字符串转换为其对应的字节表示。默认由 |
|
将表示类的字符串解析为实际类,反之亦然。当找不到类时,将抛出 |
|
用于 |
|
用于集合的属性编辑器,将任何源 |
|
用于 |
|
用于任何 |
|
将字符串解析为 |
|
单向属性编辑器,可以接受字符串并通过中间的 |
|
可以将字符串解析为 |
|
可以将字符串解析为 |
|
可以将字符串(按照 |
|
修剪字符串的属性编辑器。可选地允许将空字符串转换为 |
|
可以将URL的字符串表示解析为实际的 |
Spring使用java.beans.PropertyEditorManager
来设置可能需要的属性编辑器的搜索路径。搜索路径还包括sun.bean.editors
,其中包括Font
、Color
和大多数基本类型的PropertyEditor
实现。还要注意,标准的JavaBeans基础设施会自动发现PropertyEditor
类(无需显式注册),如果它们与它们处理的类在同一个包中,并且与该类具有相同的名称,只是在末尾添加了Editor
。例如,可以有以下类和包结构,这对于识别并用作Something
类型属性的PropertyEditor
的SomethingEditor
类已经足够。
com chank pop Something SomethingEditor // Something类的PropertyEditor
请注意,您还可以在这里使用标准的BeanInfo
JavaBeans机制(在此处描述到某种程度)。以下示例使用BeanInfo
机制显式向相关类的属性注册一个或多个PropertyEditor
实例:
com chank pop Something SomethingBeanInfo // Something类的BeanInfo
下面是所引用的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
,并将其放置在为其提供支持的类的相同包中,以便可以自动找到它。
如果需要注册其他自定义PropertyEditor
,有几种机制可用。最手动的方法,通常不方便或不推荐的方法,是使用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
}
当一切设置正确时,我们希望能够将类型属性分配为字符串,然后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
在ApplicationContext
中注册新的PropertyEditor
,然后可以根据需要使用它:
<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
实例特别方便,它公开了一个名为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) {
// 预计会创建新的PropertyEditor实例
registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());
// 您可以在此注册尽可能多的自定义属性编辑器...
}
}
package com.foo.editors.spring
import org.springframework.beans.PropertyEditorRegistrar
import org.springframework.beans.PropertyEditorRegistry
class CustomPropertyEditorRegistrar : PropertyEditorRegistrar {
override fun registerCustomEditors(registry: PropertyEditorRegistry) {
// 预计会创建新的PropertyEditor实例
registry.registerCustomEditor(ExoticType::class.java, ExoticTypeEditor())
// 您可以在此注册尽可能多的自定义属性编辑器...
}
}
另请参阅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框架的人来说,在数据绑定Web控制器中使用PropertyEditorRegistrar
可以非常方便。以下示例在@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);
}
// 与注册用户相关的其他方法
}
@Controller
class RegisterUserController(
private val customPropertyEditorRegistrar: PropertyEditorRegistrar) {
@InitBinder
fun initBinder(binder: WebDataBinder) {
this.customPropertyEditorRegistrar.registerCustomEditors(binder)
}
// 与注册用户相关的其他方法
}
这种PropertyEditor
注册风格可以导致简洁的代码(@InitBinder
方法的实现仅有一行代码),并且可以将常见的PropertyEditor
注册代码封装在一个类中,然后在需要时在多个控制器之间共享。