Spring 数据绑定,校验,BeanWrapper,与属性编辑器
Data Binding
数据绑定(Data binding)非常有用,它可以动态把用户输入与应用程序的域模型(或者你用于处理用户输入的对象)绑定起来。Spring 针对此提供了所谓的 DataBinder 来完成这一功能。由 Validator 和 DataBinder 组成的 validation 验证包,主要被用于 Spring 的 MVC 框架。当然,他们同样可以被用于其他需要的地方。
Validation
从 Spring 4.0 版本开始,Spring Framework 支持 Bean Vaildation 1.0(JSR-303) 和 Bean Vaildation 1.1(JSR-349)
使用 Spring Validator 接口
Spring 框架提供了一个 Validator 验证对象的接口。可以使用它来验证对象,Validator 通过 Errors 来工作以便在进行验证时,Validator 可以向错误对象报告验证失败。
Errors:用于存储和暴露于某个对象相关的数据绑定和错误校验信息。
public interface Validator {
boolean supports(Class<?> var1);
void validate(Object var1, Errors var2);
}
supports(Class var1):验证参数的 class 是否支持验证这个 class 的实例。
validate(Object var1, Errors var2):验证给定的对象,如果发生验证错误,它会将所有的校验错误汇总到 Errors 对象中去
示例
public class Person implements Validator {
private String name;
private int age;
@Override public boolean supports(Class<?> aClass) {
return Person.class.equals(aClass);
}
@Override public void validate(Object o, Errors errors) {
// 校验 name 属性是否为空
ValidationUtils.rejectIfEmpty(errors, "name", "name.empty");
Person p = (Person) o;
// 校验 age 是否符合业务规则
if (p.getAge() < 0) {
errors.rejectValue("age", "negativevalue");
} else if (p.getAge() > 110) {
errors.rejectValue("age", "too.darn.old");
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
我们使用了 ValidationUtils 中的一个静态方法 rejectIfEmpty(…) 来对 name 属性进行校验,假若 ‘name’ 属性是 null 或者空字符串的话,就拒绝验证通过。
实现单个 Validator 类来验证其内置的属性类当然也是可行的,但是最好为每一个内置类都实现以下 Validator。比如有一个 Customer 类有两个 String 属性 fristName 和 secondName,还有一个引用对象属性 Address 类。Address 对象可能独立于 Customer 对象,因此独立实现了一个AddressValidator。假若你希望你的 CustomerValidator 重用 AddressValidator 内部的逻辑,但是又不想通过拷贝粘贴来实现,你可以在你的 CustomerValidator 中依赖注入AddressValidator 对象,或者创建一个。
public class CustomerValidator implements Validator {
private final Validator addressValidator;
public CustomerValidator(Validator addressValidator) {
if (addressValidator == null) {
throw new IllegalArgumentException("The supplied [Validator] is " +
"required and must not be null.");
}
if (!addressValidator.supports(Address.class)) {
throw new IllegalArgumentException("The supplied [Validator] must " +
"support the validation of [Address] instances.");
}
this.addressValidator = addressValidator;
}
public boolean supports(Class clazz) {
return Customer.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
Customer customer = (Customer) target;
try {
errors.pushNestedPath("address");
ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
} finally {
errors.popNestedPath();
}
}
}
Bean 处理和 BeanWrapper
org.springframework.beans 包遵循 JavaBean 标准,JavaBean 是一个具有默认无参构造器的类。类中的属性遵循命名规范,有 getter setter 方法。比如其中有一个属性名为 bingoMadness 将会由 getBingoMadness 和 setBingoMadness 方法。
org.springframework.beans 包中有一个非常重要的类 BeanWrapper 接口及其对应的实现类 BeanWrapperImpl。
BeanWrapper 提供了设置和获取属性值(单个的或者是批量的),获取属性描述信息、查询只读或者可写属性等功能,BeanWrapper 还支持嵌套属性。
BeanWrapper 还支持添加标准 javabean PropertyChangeListeners 和VetoableChangeListeners 的能力,而不需要在目标类中支持代码。BeanWrapper 还提供了设置索引属性的支持。通常情况下,我们不在应用程序中直接使用BeanWrapper而是使用DataBinder 和BeanFactory。
BeanWrapper 这个名字本身就暗示了它的功能:封装了一个 bean 的行为,诸如设置和获取属性值等。
设置和获取属性值和引用属性
设置和获取属性是通过使用 setPropertyValue、setPropertyValues、getPropertyValue 和getPropertyValues 方法来完成的。
属性示例
表达式 | 说明 |
---|---|
name | 表明属性 name 与方法 getName() 或 isName() 及 setName(…) 相对应。 |
account.name | 指向属性 account 的引用属性 name,与之对应的是 getAccount().setName()和getAccount().getName() |
account[2] | 指向引用属性 account 的第三个元素,索引属性可能是一个数组(array),列表(list)或其它天然有序的容器。 |
account[COMPANYNAME] | 指向一个 Map 实体 account 中以 COMPANYNAME 作为键值(key)所对应的值 |
示例类
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;
}
}
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;
}
}
示例如何获取和设置上面两个示例类 Companies 和 Employees 的属性:
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");
内建的 PropertyEditor 实现
Spring 使用了 PropertyEditor 以在对象和 String 之间进行有效地转化。例如,Date 可以用人更容易理解的方式表示(字符串:‘2007-14-09’)。与此同时我们可以将这种人们比较容易理解的形式转化为原有的原始 Date 类型(甚至对于任何人们输入的可理解的日期形式都可以转化成相应的 Date 对象)。如果需要这样的做话可以通过注册 java.bean.propertyeditor 类型的自定义编辑器来实现此行为。
属性编辑器主要应用在以下两个方面:
- 使用 PropertyEditor 来设置 bean 的属性。当使用 String 作为在 XML 文件中声明的某个bean 的属性的值时,Spring(如果相应属性的 setter 有一个类参数)使用 ClassEditor 尝试将该参数解析为一个类对象。
- 在 Spring 的 MVC 框架中解析 HTTP 请求参数是通过使用各种 PropertyEditor 实现来完成的,可以在 CommandController 的所有子类中手动绑定这些实现。
Spring 有许多 PropertyEditor 实现,简化我们的工作。都在 org.springframework.beans.propertyeditors 包中。默认情况下,大多数已经默认在BeanWrapperImpl 的实现类中注册好了。
类 | 说明 |
---|---|
ByteArrayPropertyEditor | byte 数组编辑器。字符串将被简单转化成他们相应的 byte 形式。在 BeanWrapperImpl 中已经默认注册好了。 |
ClassEditor | 将以字符串形式出现的类名解析成为真实的 Class 对象或者其他相关形式。当这个Class 没有被找到,会抛出一个 IllegalArgumentException 的异常,在 BeanWrapperImpl 中已经默认注册好了。 |
CustomBooleanEditor | 为 Boolean 类型属性定制的属性编辑器。在 BeanWrapperImpl 中已经默认注册好了,但可以被用户自定义的编辑器实例覆盖其行为。 |
CustomCollectionEditor | 集合(Collection)编辑器,将任何源集合(Collection)转化成目标的集合类型的对象。 |
CustomDateEditor | 为 java.util.Date 类型定制的属性编辑器,支持用户自定义的DateFormat。默认没有被 BeanWrapperImpl 注册,需要用户通过指定恰当的 format 类型来注册。 |
CustomNumberEditor | 为 Integer, Long, Float, Double 等 Number 的子类定制的属性编辑器。在 BeanWrapperImpl 中已经默认注册好了,但可以被用户自己定义的编辑器实例覆盖其行为。 |
FileEditor | 能够将字符串转化成 java.io.File 对象,在 BeanWrapperImpl 中已经默认注册好了。 |
InputStreamEditor | 一个单向的属性编辑器,能够把文本字符串转化成 InputStream(通过 ResourceEditor 和 Resource 作为中介),因而 InputStream 属性可以直接被设置成字符串。注意在默认情况下,这个属性编辑器不会为你关闭 InputStream。在 BeanWrapperImpl 中已经默认注册好了。 |
LocaleEditor | 在 String 对象和 Locale 对象之间互相转化。(String的形式为[语言][国家][变量],这与 Local 对象的 toString() 方法得到的结果相同)在 BeanWrapperImpl 中已经默认注册好了。 |
PatternEditor | 可以将字符串转化为 JDK 的 Pattern 对象,反之亦然。 |
PropertiesEditor | 能将 String 转化为 Properties 对象(由 JavaDoc 规定的 java.lang.Properties 类型的格式)。在 BeanWrapperImpl 中已经默认注册好了。 |
StringTrimmerEditor | 一个用于修剪(trim)String 类型的属性编辑器,具有将一个空字符串转化为 null 值的选项。默认没有注册,必须由用户在需要的时候自行注册。 |
URLEditor | 能将 String 表示的 URL 转化为一个具体的 URL 对象。在 BeanWrapperImpl 中已经默认注册好了。 |
注册自定义的 PropertyEditor
当将 bean 属性设置为字符串值时,Spring IoC 容器最终使用标准 JavaBeans PropertyEditor 实现将这些字符串转换为属性的复杂类型。Spring 预注册了许多定制的 PropertyEditor 实现。
如果你想注册自己定义的 PropertyEditor,那么有几种不同的机制:
- 最手动的方法(通常不方便也不推荐)是使用 ConfigurableBeanFactory 接口的registerCustomEditor() 方法。尽管 bean factory 的后置处理器可以半手工化的与BeanFactory 实现一起使用,但是它存在着一个引用属性的建立方式。因此,强烈推荐的一种做法是与 ApplicationContext 一起来使用它。这样就能使之与其他的 bean 一样以类似的方式部署同时被容器所感知并使用。
- 使用名为 CustomEditorConfigurer 的特殊 bean factory 后处理器
public class CustomEditorConfigurer implements BeanFactoryPostProcessor, Ordered {}
示例
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;
}
}
接下来我们需要在 xml 中声明 bean 指定一个字符串来设置 type 属性的值,然后PropertyEditor 将在幕后帮你将其转化为实际的 ExoticType 对象
<bean id="sample" class="example.DependsOnExoticType">
<property name="type" value="aNameForExoticType"/>
</bean>
PropertyEditor 的实现
public class ExoticTypeEditor extends PropertyEditorSupport {
public void setAsText(String text) {
setValue(new ExoticType(text.toUpperCase()));
}
}
最后使用 CustomEditorConfigurer 在 ApplicationContext 中注册新的 PropertyEditor,然后 ApplicationContext 就可以根据需要使用它了
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
</map>
</property>
</bean>
使用 PropertyEditorRegistrars
将属性编辑器注册到 Spring 容器的另一种方式是创建并使用 PropertyEditorRegistrar。 当你在不同情况下(如编写一个相应的注册器然后在多种情况下重用它)需要使用相同的属性编辑器时该接口特别有用。 PropertyEditorRegistrars 与 PropertyEditorRegistry 接口协同工作, PropertyEditorRegistry 由 Spring 的 BeanWrapper(及 DataBinder)实现的一个接口。 当与 CustomEditorConfigurer 协同使用时, PropertyEditorRegistrars 尤为方便, CustomEditorConfigurer 会公开一个叫做 setPropertyEditorRegistrars(…) 的方法: 在这种情况下 DataBinder 和 Spring MVC Controllers 会很容易地共享加到CustomEditorConfigurer 中的 PropertyEditorRegistrars。 此外,自定义编辑器无需再进行同步了:每次创建 bean时PropertyEditorRegistrar 都会创建一个新的 PropertyEditor。
如何创建自己的 PropertyEditorRegistrar 实现,在实现 registerCustomEditors(…) 方法时,它会创建每个属性编辑器的新实例。
public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
public void registerCustomEditors(PropertyEditorRegistry registry) {
registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());
}
}
如何配置 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框架来说)将 PropertyEditorRegistrars 与数据绑定控制器(如SimpleFormController)结合使用非常方便。下面的例子在 initBinder(…) 方法的实现中使用了propertyeditorregistry:
public final class RegisterUserController extends SimpleFormController {
private final PropertyEditorRegistrar customPropertyEditorRegistrar;
public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
this.customPropertyEditorRegistrar = propertyEditorRegistrar;
}
protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception {
this.customPropertyEditorRegistrar.registerCustomEditors(binder);
}
}
这种 PropertyEditor 注册方式可以使代码更加简洁(initBinder(…)的实现只有一行),并允许将通用的 PropertyEditor 注册代码封装在一个类中,然后根据需要在尽可能多的 Controllers 之间共享。