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

5.1. 简介

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

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

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

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

5.2. 使用DataBinder进行数据绑定

DataBinder是构建于BeanWrapper之上。[3]

5.3. Bean处理和BeanWrapper

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

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

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

5.3.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,这部分不是很重要。如果你仅仅使用DataBinder和BeanFactory或者他们的扩展实现,你可以跳过这部分直接阅读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的属性:

Company c = new Company();
BeanWrapper bwComp = BeanWrapperImpl(c);
// setting the company name...
bwComp.setPropertyValue("name", "Some Company Inc.");
// ... can also be done like this:
PropertyValue v = new PropertyValue("name", "Some Company Inc.");
bwComp.setPropertyValue(v);

// ok, let's create the director and tie it to the company:
Employee jim = new Employee();
BeanWrapper bwJim = BeanWrapperImpl(jim);
bwJim.setPropertyValue("name", "Jim Stravinsky");
bwComp.setPropertyValue("managingDirector", jim);

// retrieving the salary of the managingDirector through the company
Float salary = (Float)bwComp.getPropertyValue("managingDirector.salary");

5.3.2. 内建的PropertyEditor实现

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

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

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

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

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

Table 5.2. 内建的PropertyEditor

类名说明
ByteArrayPropertyEditorbyte数组编辑器。字符串将被简单转化成他们相应的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 and Resource作为中介),因而InputStream属性将直接被设置成字符串。注意在默认情况下,这个属性编辑器不会为你关闭InputStream。在BeanWrapperImpl中已经默认注册好了。
LocaleEditor在String对象和Locale对象之间互相转化。(String的形式为[语言]_[国家]_[变量],这与Local对象的toString()方法得到的结果相同)在BeanWrapperImpl中已经默认注册好了。
PropertiesEditor 能将String转化为Properties对象(由JavaDoc规定的java.lang.Properties类型的格式)。在BeanWrapperImpl中已经默认注册好了。
StringArrayPropertyEditor 能够在一个以逗号分割的字符串与一个String数组之间进行互相转化。在BeanWrapperImpl中已经默认注册好了。
StringTrimmerEditor 一个用于修剪(trim)String类型的属性编辑器,具有将一个空字符串转化为null值的选项。默认没有在BeanWrapperImpl中注册,必须由用户在需要的时候自行注册。
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 PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
            PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Foo.class) {
                public PropertyEditor createPropertyEditor(Object bean) {
                    return numberPE;
                };
            };
            return new PropertyDescriptor[] { ageDescriptor };
        }
        catch (IntrospectionException ex) {
            throw new Error(ex.toString());
        }
    }
}

5.3.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,其中,后者需要将前者设置为它的属性:

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
public class ExoticTypeEditor extends PropertyEditorSupport {

    private String format;

    public void setFormat(String format) {
        this.format = format;
    }
    
    public void setAsText(String text) {
        if (format != null &amp;&amp; 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>

5.3.3. 其他值得一提的特性

关于BeanWrapper提供的功能,除了你在前面的章节看到的以外,可能还有一些你感兴趣的,这里我们就不详细讲解了:

  • 判别属性是否可读写:使用isReadable()isWritable()方法可以帮助你确定某个属性是否可读或者可写。

  • 获取PropertyDescriptor:通过使用getPropertyDescriptor(String)getPropertyDescriptors()方式,你将得到一个java.beans.PropertyDescriptor类型的对象,它有时或许对你有些用处。

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

你可以使用Spring提供的validator接口进行对象的校验。Validator接口的使用相当的直接,同时它能与一个所谓的Errors对象协同工作。换句话说,在Spring做校验的时候,它会将所有的校验错误汇总到Errors对象中去。

正如先前所说,Validator接口的使用相当的直接,你不妨实现一下这个接口,考虑下面这个简单的数据类:

public class Person {
  private String name;
  private int age;

  // the usual suspects: getters and setters
}

实现org.springframework.validation.Validator接口,我们将提供对Person 类的校验行为,下面就是这个Validator接口需要实现的方法:

  • supports(Class):表示这个校验器是否支持提供的object。

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

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

public class PersonValidator implements Validator {
    
    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", "tooold");
        }
    }
}

正如你在上面所看到的那样,我们使用了ValidationUtils中的一个静态方法来对name属性进行校验。请参照ValidationUtils相关的JavaDoc,查看一下除了例子中介绍过的,其他的一些功能。

5.5. Errors接口

校验错误信息被汇总成Errors对象传递到validator。如果你使用Spring Web MVC框架,你可以通过spring:bind这个Tag来获取详细的错误信息。当然你也可以用你自己的方式得到这些错误信息,不过Spring提供的访问方式更直接。更多信息请参照JavaDoc。

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

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

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



[3] 更多相关信息请查看the beans章节

Sponsored by