数据绑定

数据绑定对于将用户输入绑定到目标对象非常有用,其中用户输入是一个带有属性路径作为键的映射,遵循JavaBeans约定DataBinder 是支持此功能的主要类,它提供两种绑定用户输入的方式:

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

  • 属性绑定 - 将用户输入绑定到setter方法,将用户输入的键与目标对象结构的属性进行匹配。

您可以同时应用构造函数绑定和属性绑定,也可以只应用其中一个。

构造函数绑定

要使用构造函数绑定:

  1. 创建一个带有null作为目标对象的DataBinder

  2. targetType设置为目标类。

  3. 调用construct

目标类应该具有一个公共构造函数或一个带有参数的非公共构造函数。如果存在多个构造函数,则会使用默认构造函数(如果存在)。

默认情况下,构造函数参数名称用于查找参数值,但您可以配置一个NameResolver。Spring MVC和WebFlux都依赖于允许通过构造函数参数上的@BindParam注解来自定义要绑定的值的名称。

根据需要应用类型转换以转换用户输入。如果构造函数参数是对象,则会以相同方式递归构造它,但通过嵌套属性路径。这意味着构造函数绑定会创建目标对象及其包含的任何对象。

绑定和转换错误会反映在DataBinderBindingResult中。如果目标成功创建,则在调用construct后,target会设置为创建的实例。

BeanWrapper属性绑定

org.springframework.beans包遵循JavaBeans标准。JavaBean是一个具有默认无参构造函数并遵循命名约定的类,例如,一个名为bingoMadness的属性将具有setter方法setBingoMadness(..)和getter方法getBingoMadness()。有关JavaBeans和规范的更多信息,请参阅javabeans

beans包中一个非常重要的类是BeanWrapper接口及其相应的实现(BeanWrapperImpl)。根据javadoc的描述,BeanWrapper提供了设置和获取属性值(单独或批量)、获取属性描述符以及查询属性是否可读或可写的功能。此外,BeanWrapper还支持嵌套属性,使得可以无限深度地设置子属性的属性。BeanWrapper还支持添加标准的JavaBeansPropertyChangeListenersVetoableChangeListeners,而无需在目标类中编写支持代码。最后,BeanWrapper提供了设置索引属性的支持。通常,BeanWrapper不会被应用程序代码直接使用,而是被DataBinderBeanFactory使用。

BeanWrapper的工作方式部分由其名称指示:它包装一个bean以对该bean执行操作,如设置和检索属性。

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

通过BeanWrappersetPropertyValuegetPropertyValue重载方法变体来设置和获取属性。详细信息请参阅它们的Javadoc。下表显示了这些约定的一些示例:

表1. 属性示例
表达式 解释

name

表示与getName()isName()setName(..)方法对应的属性name

account.name

表示属性account的嵌套属性name,对应于(例如)getAccount().setName()getAccount().getName()方法。

account[2]

表示属性account的第个元素。索引属性可以是arraylist或其他自然有序的集合类型。

account[COMPANYNAME]

表示accountMap属性中由COMPANYNAME键索引的映射条目的值。

(如果您不打算直接使用BeanWrapper,则下一部分对您并不是非常重要。如果您只使用DataBinderBeanFactory及其默认实现,您应该跳到关于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
}

以下代码片段显示了如何检索和操作已实例化的CompanyEmployee的一些属性示例:

  • 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的概念来实现ObjectString之间的转换。以不同于对象本身的方式表示属性可能很方便。例如,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实现:

表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对象,反之亦然(字符串格式为[language]_[country]_[variant],与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实现。还要注意,标准的JavaBeans基础设施会自动发现PropertyEditor类(无需显式注册),如果它们与它们处理的类在同一个包中,并且与该类具有相同的名称,只是在末尾添加了Editor。例如,可以有以下类和包结构,这对于识别并用作Something类型属性的PropertyEditorSomethingEditor类已经足够。

com
  chank
    pop
      Something
      SomethingEditor // Something类的PropertyEditor

请注意,您还可以在这里使用标准的BeanInfo JavaBeans机制(在此处描述到某种程度)。以下示例使用BeanInfo机制显式向相关类的属性注册一个或多个PropertyEditor实例:

com
  chank
    pop
      Something
      SomethingBeanInfo // Something类的BeanInfo

下面是所引用的SomethingBeanInfo类的Java源代码,将CustomNumberEditorSomething类的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())
	}
}

最后,以下示例显示了如何使用CustomEditorConfigurerApplicationContext中注册新的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注册代码封装在一个类中,然后在需要时在多个控制器之间共享。