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中对它们有详细的描述。值得一提的是,在这其中存在一些针对对象属性的潜在约定规则。下面是一些例子:

表 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的实现类中注册好了。作为可配置的选项,你也可以注册你自己的属性编辑器实现去覆盖那些默认编辑器。

表 5.2. 内建的PropertyEditors

类名 说明
ByteArrayPropertyEditor byte数组编辑器。字符串将被简单转化成他们相应的byte形式。在BeanWrapperImpl中已经默认注册好了。
ClassEditor 将以字符串形式出现的类名解析成为真实的Class对象或者其他相关形式。当这个Class没有被找到,会抛出一个IllegalArgumentException的异常,在BeanWrapperImpl中已经默认注册好了。
CustomBooleanEditor Boolean类型属性定制的属性编辑器。在BeanWrapperImpl中已经默认注册好了,但可以被用户自定义的编辑器实例覆盖其行为。
CustomCollectionEditor 集合(Collection)编辑器,将任何源集合(Collection)转化成目标的集合类型的对象。
CustomDateEditor 为java.util.Date类型定制的属性编辑器,支持用户自定义的DateFormat。默认没有被BeanWrapperImpl注册,需要用户通过指定恰当的format类型来注册。
CustomNumberEditor IntegerLongFloatDouble等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>