Chapter 5. 校验,数据绑定,BeanWrapper,与属性编辑器

5.1. 简介

对是否把校验当作业务逻辑这个问题,存在着正和反两种意见,而Spring提供的验证模式(和数据绑定)的设计对这两种意见都不排斥。特别是,校验应该不应该被强制绑定在Web层,而且应该很容易本地化并且可以方便地加入新的验证逻辑。基于上述的考虑,Spring提供了一个Validator接口。这是一个基础的接口并且适用于应用程序的任何一个层面。

数据绑定(Data binding)非常有用,它可以动态把用户输入与应用程序的域模型(或者你用于处理用户输入的对象)绑定起来。Spring针对此提供了所谓的DataBinder来完成这一功能。由ValidatorDataBinder组成的validation验证包,主要被用于Spring的MVC框架。当然,他们同样可以被用于其他需要的地方。

BeanWrapper作为一个基础组件被用在了Spring框架中的很多地方。不过,你可能很少会需要直接使用BeanWrapper。由于这是一篇参考文档,因而我们觉得对此稍作解释还是有必要的。我们在这一章节里对BeanWrapper的说明,或许到了你日后试图进行类似对象与数据之间的绑定这种与BeanWrapper非常相关的操作时会有一些帮助。

Spring大量地使用了PropertyEditor(属性编辑器)。PropertyEditor的概念是JavaBean规范的一部分。正如上面提到的BeanWrapper一样,由于它与BeanWrapper以及DataBinder三者之间有着密切的联系,我们在这里同样对PropertyEditor作一番解释。

5.2. 使用Spring的Validator接口进行校验

你可以使用Spring提供的validator接口进行对象的校验。Validator接口与Errors协同工作,在Spring做校验的时候,它会将所有的校验错误汇总到Errors对象中去。

来看这个简单的数据对象:

public class Person {

  private String name;
  private int age;

  // the usual getters and setters...
}

实现org.springframework.validation.Validator接口中的两个方法,我们将为对Person类加上校验行为:

  • supports(Class):表示这个Validator是否支持该Class的实例?

  • validate(Object, org.springframework.validation.Errors):对提供的对象进行校验,并将校验的错误注册到传入的Errors 对象中。

实现一个Validator也比较简单,尤其是当你学会了Spring所提供的ValidationUtils以后。我们一起来看一下如何才能创建一个校验器。

public class PersonValidator implements Validator {
    
    /**
    * This Validator validates just Person instances
    */
    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }
    
    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        Person p = (Person) obj;
        if (p.getAge() < 0) {
            e.rejectValue("age", "negativevalue");
        } else if (p.getAge() > 110) {
            e.rejectValue("age", "too.darn.old");
        }
    }
}

如你所见,我们使用了ValidationUtils中的一个静态方法rejectIfEmpty(..)来对name属性进行校验,假若'name'属性是 null 或者空字符串的话,就拒绝验证通过 。请参照ValidationUtils相关的JavaDoc,查看一下除了例子中介绍过的之外其他的一些功能。

对复杂对象来说,实现Validator类来验证其内置的属性类当然也是可行的,但是为每个内置类的示例实现Validator可能是个更好的主意。关于这样的一个'rich'这样的例子是Customer类,它包含两个String属性(first name 和second name),还有一个复杂的Address对象。Address对象可能独立于Customer对象,因此独立实现了一个AddressValidator。假若你希望你的CustomerValidator重用AddressValidator内部的逻辑,但是又不想通过拷贝粘贴来实现,你可以在你的CustomerValidator中依赖注入AddressValidator对象,或者创建一个。然后这样用:

public class CustomerValidator implements Validator {

   private final Validator addressValidator;

   public UserValidator(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;
   }

    /**
    * This Validator validates Customer instances, and any subclasses of Customer too
    */
   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();
      }
   }
}

验证错误会被报告到传来的Errors对象中。在Spring Web MVC中,你可以使用<spring:bind/>标签来检查错误信息,当然你也可以自行处理错误。可以在它的Javadoc中找到它提供的方法的描述。

5.3. 从错误代码到错误信息

我们已经讨论了数据绑定和校验。最后我们来讨论一下与校验错误相对应的错误信息输出。在先前的示例中,我们对nameage字段进行了校验并发现了错误。如果我们想用MessageSource来输出错误信息,当某个字段校验出错时(在这个例子中是name和age)我们输出的是错误代码。无论你直接或者间接使用示例中的ValidationUtils 类来调用Errors接口中rejectValue方法或者任何一个其它的reject方法,底层的实现不仅为你注册了你传入的代码,还同时为你注册了许多额外的错误代码信息。而你使用的MessageCodesResolver将决定究竟注册什么样的错误代码。默认情况下,将会使用DefaultMessageCodesResolver。回到前面的例子,使用DefaultMessageCodesResolver,不仅会为你注册你提供的错误代码信息,同时还包含了你传入到reject方法中的字段信息。所以在这个例子中,你通过rejectValue("age", "too.darn.old")来注册一个字段校验错误。Spring不仅为你注册了too.darn.old这个代码,同时还为你注册了too.darn.old.agetoo.darn.old.age.int来分别表示字段名称和字段的类型。这种是现实为了方面开发者来定位错误信息。

更多有关MessageCodesResolver的信息以及默认的策略可以在线访问相应的JavaDocs: MessageCodesResolver DefaultMessageCodesResolver .

5.4. Bean处理和BeanWrapper

org.springframework.beans包遵循Sun发布的JavaBean标准。JavaBean是一个简单的含有一个默认无参数构造函数的Java类, 这个类中的属性遵循一定的命名规范,且具有setter和getter方法。例如,某个类拥有一个叫做bingoMadness的属性,并同时具有与该属性对应的setter方法:setBingoMadness(..)和getter方法:getBingoMadness()。 如果你需要了解JavaBean规范的详细信息可以访问Sun的网站 (java.sun.com/products/javabeans)。

这个包中的一个非常重要的概念就是BeanWrapper接口以及它对应的实现(BeanWrapperImpl)。根据JavaDoc中的说明,BeanWrapper提供了设置和获取属性值(单个的或者是批量的),获取属性描述信息、查询只读或者可写属性等功能。不仅如此,BeanWrapper还支持嵌套属性,你可以不受嵌套深度限制对子属性的值进行设置。所以,BeanWrapper无需任何辅助代码就可以支持标准JavaBean的PropertyChangeListenersVetoableChangeListeners。除此之外,BeanWrapper还提供了设置索引属性的支持。通常情况下,我们不在应用程序中直接使用BeanWrapper而是使用DataBinderBeanFactory

BeanWrapper这个名字本身就暗示了它的功能:封装了一个bean的行为,诸如设置和获取属性值等。

5.4.1. 设置和获取属性值以及嵌套属性

设置和获取属性可以通过使用重载的setPropertyValue(s)getPropertyValue(s)方法来完成。在Spring自带的JavaDoc中对它们有详细的描述。值得一提的是,在这其中存在一些针对对象属性的潜在约定规则。下面是一些例子:

Table 5.1. 属性示例

表达式说明
name指向属性name,与getName() 或 isName() 和 setName()相对应。
account.name指向属性account的嵌套属性name,与之对应的是getAccount().setName()和getAccount().getName()
account[2]指向索引属性account的第三个元素,索引属性可能是一个数组(array),列表(list)或其它天然有序的容器。
account[COMPANYNAME]指向一个Map实体account中以COMPANYNAME作为键值(key)所对应的值

在下面的例子中你将看到一些使用BeanWrapper设置属性的例子。

如果你不打算直接使用BeanWrapper,这部分不是很重要。如果你仅仅使用DataBinderBeanFactory或者他们的扩展实现,你可以跳过这部分直接阅读PropertyEditor的部分。

考虑下面两个类:

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 float salary;

    public float getSalary() {
        return salary;
    }
    public void setSalary(float salary) {
        this.salary = salary;
    }
}

下面的代码片断展示了如何获取和设置上面两个示例类 CompaniesEmployees的属性:

BeanWrapper company = 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 = 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");

5.4.2. 内建的PropertyEditor实现

Spring大量使用了PropertyEditor。有时候换一种方式来展示属性要比直接用对象自身根据容易让人理解。比如说,人们可以很容易理解标准的日期写法。当然,我们还是可以将这种人们比较容易理解的形式转化为原有的原始Date类型(甚至对于任何人们输入的可理解的日期形式都可以转化成相应的Date对象)。要做到这点,可以通过注册一个用户定制编辑器(类型为java.beans.PropertyEditor)来完成。注册一个用户自定义的编辑器可以告诉BeanWrapper我们将要把属性转换为哪种类型。正如在先前章节提到的,另外一种选择是在特定的IoC 容器中完成注册。你可以从Sun的JavaDoc中的java.beans包中了解到有关java.beans的细节。

属性编辑器主要应用在以下两个方面:

  • 使用PropertyEditors设置Bean属性。当你在XML文件中声明的bean的属性类型为java.lang.String时,Spring将使用ClassEditor将String解析成Class对象(如果setter方法需要一个Class参数的话)。

  • 在Spring MVC架构中使用各种PropertyEditors解析HTTP请求中的参数。你可以用各种CommandController的子类来进行手工绑定。

Spring提供了许多内建的PropertyEditors可以简化我们的工作。下面的列表列出了所有Spring自带的PropertyEditor,它们都位于org.springframework.beans.PropertyEditors包内。它们中的大多数已经默认在BeanWrapperImpl的实现类中注册好了。作为可配置的选项,你也可以注册你自己的属性编辑器实现去覆盖那些默认编辑器。

Table 5.2. 内建的PropertyEditors

类名说明
ByteArrayPropertyEditorbyte数组编辑器。字符串将被简单转化成他们相应的byte形式。在BeanWrapperImpl中已经默认注册好了。
ClassEditor 将以字符串形式出现的类名解析成为真实的Class对象或者其他相关形式。当这个Class没有被找到,会抛出一个IllegalArgumentException的异常,在BeanWrapperImpl中已经默认注册好了。
CustomBooleanEditorBoolean类型属性定制的属性编辑器。在BeanWrapperImpl中已经默认注册好了,但可以被用户自定义的编辑器实例覆盖其行为。
CustomCollectionEditor 集合(Collection)编辑器,将任何源集合(Collection)转化成目标的集合类型的对象。
CustomDateEditor 为java.util.Date类型定制的属性编辑器,支持用户自定义的DateFormat。默认没有被BeanWrapperImpl注册,需要用户通过指定恰当的format类型来注册。
CustomNumberEditorIntegerLongFloatDouble等Number的子类定制的属性编辑器。在BeanWrapperImpl中已经默认注册好了,但可以被用户自己定义的编辑器实例覆盖其行为。
FileEditor 能够将字符串转化成java.io.File对象,在BeanWrapperImpl中已经默认注册好了。
InputStreamEditor 一个单向的属性编辑器,能够把文本字符串转化成InputStream(通过ResourceEditorResource作为中介),因而InputStream属性可以直接被设置成字符串。注意在默认情况下,这个属性编辑器不会为你关闭InputStream。在BeanWrapperImpl中已经默认注册好了。
LocaleEditor在String对象和Locale 对象之间互相转化。(String的形式为[语言]_[国家]_[变量],这与Local对象的toString()方法得到的结果相同)在BeanWrapperImpl中已经默认注册好了。
PropertiesEditor 能将String转化为Properties对象(由JavaDoc规定的java.lang.Properties类型的格式)。在BeanWrapperImpl中已经默认注册好了。
StringArrayPropertyEditor 能够在一个以逗号分割的字符串与一个String数组之间进行互相转化。
StringTrimmerEditor 一个用于修剪(trim)String类型的属性编辑器,具有将一个空字符串转化为null值的选项。默认没有注册,必须由用户在需要的时候自行注册。
URLEditor 能将String表示的URL转化为一个具体的URL对象。在BeanWrapperImpl中已经默认注册好了。

Spring使用java.beans.PropertyEditorManager来为可能需要的属性编辑器设置查询路径。查询路径同时包含了sun.bean.editors, 这个包中定义了很多PropertyEditor>的具体实现,包括字体、颜色以及绝大多数的基本类型的具体实现。同样值得注意的是,标准的JavaBean基础构架能够自动识别PropertyEditor>类(无需做额外的注册工作),前提条件是,类和处理这个类的Editor位于同一级包结构,而Editor的命名遵循了在类名后加了“Editor”的规则。举例来说,当FooEditorFoo在同一级别包下的时候,FooEditor能够识别Foo类并作为它的PropertyEditor>。

com
  chank
    pop
      Foo
      FooEditor   // the PropertyEditor> for the Foo class

注意,你同样可以使用标准的BeanInfo JavaBean机制(详情见这里)。在下面的例子中,你可以看到一个通过使用BeanInfo机制来为相关类的属性明确定义一个或者多个PropertyEditor>实例

com
  chank
    pop
      Foo
      FooBeanInfo   // the BeanInfo for the Foo class

下面就是FooBeanInfo类的源码,它将CustomNumberEditorFoo中的age属性联系在了一起。

public class FooBeanInfo extends SimpleBeanInfo {
      
    public PropertyDescriptor[] getPropertyDescriptors() {
        try {
            final <interfacename>PropertyEditor</interfacename> numberPE = new CustomNumberEditor(Integer.class, true);
            PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Foo.class) {
                public <interfacename>PropertyEditor</interfacename> create<interfacename>PropertyEditor</interfacename>(Object bean) {
                    return numberPE;
                };
            };
            return new PropertyDescriptor[] { ageDescriptor };
        }
        catch (IntrospectionException ex) {
            throw new Error(ex.toString());
        }
    }
}

5.4.2.1. 注册用户自定义的PropertyEditor

当以一个字符串值来设置bean属性时,Spring IoC 容器最终使用标准的JavaBean PropertyEditor来将这些字符串转化成复杂的数据类型。Spring预先注册了一些PropertyEditor(举例来说,将一个以字符串表示的Class转化成Class对象)。除此之外,Java标准的JavaBean PropertyEditor>会识别在同一包结构下的类和它对应的命名恰当的Editor,并自动将其作为这个类的的Editor。

如果你想注册自己定义的PropertyEditor,那么有几种不同的机制供君选择。其中,最原始的手工方式是在你有一个BeanFactory的引用实例时,使用ConfigurableBeanFactoryregisterCustomEditor()方法。当然,通常这种方法不够方便,因而并不推荐使用。另外一个简便的方法是使用一个称之为CustomEditorConfigurer的特殊的bean factory后置处理器。尽管bean factory的后置处理器可以半手工化的与BeanFactory实现一起使用,但是它存在着一个嵌套属性的建立方式。因此,强烈推荐的一种做法是与ApplicationContext一起来使用它。这样就能使之与其他的bean一样以类似的方式部署同时被容器所感知并使用。

注意所有的bean factory和application context都会自动地使用一系列的内置属性编辑器,通过BeanWrapper来处理属性的转化。在这里列出一些在BeanWrapper中注册的标准的属性编辑器。除此之外,ApplicationContext覆盖了一些默认行为,并为之增加了许多编辑器来处理在某种意义上合适于特定的application context类型的资源查找。

标准的JavaBean的PropertyEditor>实例将以String表示的值转化成实际复杂的数据类型。CustomEditorConfigurer作为一个bean factory的后置处理器, 能够便捷地将一些额外的PropertyEditor>实例加入到ApplicationContext中去。

考虑用户定义的类ExoticTypeDependsOnExoticType,其中,后者需要将前者设置为它的属性:

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;
    }
}

在一切建立起来以后,我们希望通过指定一个字符串来设置type属性的值,然后PropertyEditor>将在幕后帮你将其转化为实际的ExoticType对象:

<bean id="sample" class="example.DependsOnExoticType">
    <property name="type" value="aNameForExoticType"/>
</bean>

PropertyEditor>的实现看上去就像这样:

// converts string representation to ExoticType object
package example;

public class ExoticTypeEditor extends PropertyEditorSupport {

    private String format;

    public void setFormat(String format) {
        this.format = format;
    }
    
    public void setAsText(String text) {
        if (format != null && format.equals("upperCase")) {
            text = text.toUpperCase();
        }
        ExoticType type = new ExoticType(text);
        setValue(type);
    }
}

最后,我们通过使用CustomEditorConfigurer来为ApplicationContext注册一个新的PropertyEditor>,这样,我们就可以在任何需要的地方使用它了:

<bean id="customEditorConfigurer" 
    class="org.springframework.beans.factory.config.CustomEditorConfigurer">
  <property name="customEditors">
    <map>
      <entry key="example.ExoticType">
        <bean class="example.ExoticTypeEditor">
          <property name="format" value="upperCase"/>
        </bean>
      </entry>
    </map>
  </property>
</bean>