5.4 Bean操作和BeanWrapper {#toc_4}
org.springframework.beans
包遵循Oracle提供的JavaBeans标准。一个JavaBean只是一个包含默认无参构造器的类,它遵循一个命名约定(通过一个例子):一个名为bingoMadness
属性将有一个设置方法setBingoMadness(..)
和一个获取方法getBingoMadness(..)
。有关JavaBeans和其规范的更多信息,请参考Oracle的网站(javabeans)。
beans包里一个非常重要的类是BeanWrapper
接口和它的相应实现(BeanWrapperImpl
)。引用自java文档,BeanWrapper
提供了设置和获取属性值(单独或批量)、获取属性描述符以及查询属性以确定它们是可读还是可写的功能。BeanWrapper
还提供对嵌套属性的支持,能够不受嵌套深度的限制启用子属性的属性设置。然后,BeanWrapper
提供了无需目标类代码的支持就能够添加标准JavaBeans的PropertyChangeListeners
和VetoableChangeListeners
的能力。最后然而并非最不重要的是,BeanWrapper
提供了对索引属性设置的支持。BeanWrapper
通常不会被应用程序的代码直接使用,而是由DataBinder
和BeanFactory
使用。
BeanWrapper
的名字已经部分暗示了它的工作方式:它包装一个bean以对其执行操作,比如设置和获取属性。
5.4.1 设置并获取基本和嵌套属性 {#toc_5}
使用setPropertyValue(s)
和getPropertyValue(s)
可以设置并获取属性,两者都带有几个重载方法。在Spring自带的java文档中对它们有更详细的描述。重要的是要知道对象属性指示的几个约定。几个例子:
表 5.1. 属性示例
表达式 | 说明 |
---|---|
name |
表示属性name 与方法getName() 或isName() 和setName() 相对应 |
account.name |
表示属性account 的嵌套属性name 与方法getAccount().setName() 或getAccount().getName() 相对应 |
account[2] |
表示索引属性account 的第三个元素。索引属性可以是array 、list 或其他自然排序的集合 |
account[COMPANYNAME] |
表示映射属性account 被键COMPANYNAME索引到的映射项的值 |
下面你会发现一些使用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 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());
// 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 = new 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实现 {#toc_6}
Spring使用PropertyEditor
的概念来实现Object
和String
之间的转换。如果你考虑到它,有时候换另一种方式表示属性可能比对象本身更方便。举个例子,一个Date
可以以人类可读的方式表示(如String'2007-14-09'
),同时我们依然能把人类可读的形式转换回原始的时间(甚至可能更好:将任何以人类可读形式输入的时间转换回Date
对象)。这种行为可以通过注册类型为PropertyEditor
的自定义编辑器来实现。在BeanWrapper
或上一章提到的特定IoC容器中注册自定义编辑器,可以使其了解如何将属性转换为期望的类型。请阅读Oracle为java.beans
包提供的java文档来获取更多关于PropertyEditor
的信息。
这是Spring使用属性编辑的两个例子:
- 使用
PropertyEditor
来完成bean的属性设置。当提到将java.lang.String
作为你在XML文件中声明的某些bean的属性值时,Spring将会(如果相应的属性的设置方法具有一个Class
参数)使用ClassEditor
尝试将参数解析成Class
对象。 - 在Spring的MVC框架中解析HTTP请求的参数是由各种
PropertyEditor
完成的,你可以把它们手动绑定到CommandController
的所有子类。
Spring有一些内置的PropertyEditor
使生活变得轻松。它们中的每一个都已列在下面,并且它们都被放在org.springframework.beans.propertyeditors
包中。大部分但并不是全部(如下所示),默认情况下会由BeanWrapperImpl
注册。在某种方式下属性编辑器是可配置的,那么理所当然,你可以注册你自己的变种来覆盖默认编辑器:
Table 5.2. 内置PropertyEditor
类 | 说明 |
---|---|
ByteArrayPropertyEditor |
针对字节数组的编辑器。字符串会简单地转换成相应的字节表示。默认情况下由BeanWrapperImpl 注册。 |
ClassEditor |
将类的字符串表示形式解析成实际的类形式并且也能返回实际类的字符串表示形式。如果找不到类,会抛出一个IllegalArgumentException 。默认情况下由BeanWrapperImpl 注册。 |
CustomBooleanEditor |
针对Boolean 属性的可定制的属性编辑器。默认情况下由BeanWrapperImpl 注册,但是可以作为一种自定义编辑器通过注册其自定义实例来进行覆盖。 |
CustomCollectionEditor |
针对集合的属性编辑器,可以将原始的Collection 转换成给定的目标Collection 类型。 |
CustomDateEditor |
针对java.util.Date的可定制的属性编辑器,支持自定义的时间格式。不会被默认注册,用户必须使用适当格式进行注册。 |
CustomNumberEditor |
针对任何Number子类(比如Integer 、Long 、Float 、Double )的可定制的属性编辑器。默认情况下由BeanWrapperImpl 注册,但是可以作为一种自定义编辑器通过注册其自定义实例来进行覆盖。 |
FileEditor |
能够将字符串解析成java.io.File 对象。默认情况下由BeanWrapperImpl 注册。 |
InputStreamEditor |
一次性的属性编辑器,能够读取文本字符串并生成(通过中间的ResourceEditor 以及Resource )一个InputStream 对象,因此InputStream 类型的属性可以直接以字符串设置。请注意默认的使用方式不会为你关闭InputStream !默认情况下由BeanWrapperImpl 注册。 |
LocaleEditor |
能够将字符串解析成Locale 对象,反之亦然(字符串格式是[country][variant],这与Locale提供的toString()方法是一样的)。默认情况下由BeanWrapperImpl 注册。 |
PatternEditor |
能够将字符串解析成java.util.regex.Pattern 对象,反之亦然。 |
PropertiesEditor |
能够将字符串(按照java.util.Properties 类的java文档定义的格式进行格式化)解析成Properties 对象。默认情况下由BeanWrapperImpl 注册。 |
StringTrimmerEditor |
用于缩减字符串的属性编辑器。有选择性允许将一个空字符串转变成null 值。不会进行默认注册,需要在用户有需要的时候注册。 |
URLEditor |
能够将一个URL的字符串表示解析成实际的URL 对象。默认情况下由BeanWrapperImpl 注册。 |
Spring使用java.beans.PropertyEditorManager
来设置可能需要的属性编辑器的搜索路径。搜索路径中还包括了sun.bean.editors
,这个包里面包含如Font
、Color
类型以及其他大部分基本类型的PropertyEditor
实现。还要注意的是,如果PropertyEditor
类与它们所处理的类位于同一个包并且除了’Editor’后缀之外拥有相同的名字,那么标准的JavaBeans基础设施会自动发现这些它们(不需要你显式的注册它们)。例如,有人可能会有以下的类和包结构,这已经足够识别出FooEditor
类并将其作为Foo
类型属性的PropertyEditor
。
com
chank
pop
Foo
FooEditor // the PropertyEditor for the Foo class
要注意的是在这里你也可以使用标准JavaBeans机制的BeanInfo
(在in not-amazing-detail here有描述)。在下面的示例中,你可以看使用BeanInfo
机制为一个关联类的属性显式注册一个或多个PropertyEditor
实例。
com
chank
pop
Foo
FooBeanInfo // the BeanInfo for the Foo class
这是被引用到的FooBeanInfo
类的Java源代码。它会将一个CustomNumberEditor
同Foo
类的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());
}
}
}
注册额外的自定义PropertyEditor {#toc_7}
当bean属性设置成一个字符串值时,Spring IoC容器最终会使用标准JavaBeans的PropertyEditor
将这些字符串转换成复杂类型的属性。Spring预先注册了一些自定义PropertyEditor
(例如将一个以字符串表示的类名转换成真正的Class
对象)。此外,Java的标准JavaBeansPropertyEditor
查找机制允许一个PropertyEditor
只需要恰当的命名并同它支持的类位于相同的包,就能够自动发现它。
如果需要注册其他自定义的PropertyEditor
,还有几种可用机制。假设你有一个BeanFactory
引用,最人工化的方式(但通常并不方便或者推荐)是直接使用ConfigurableBeanFactory
接口的registerCustomEditor()
方法。另一种略为方便的机制是使用一个被称为CustomEditorConfigurer
的特殊的bean factory后处理器(post-processor)。虽然bean factory后处理器可以与BeanFactory
实现一起使用,但是因为CustomEditorConfigurer
有一个嵌套属性设置过程,所以强烈推荐它与ApplicationContext
一起使用,这样就可以采用与其他bean类似的方式来部署它,并自动检测和应用。
请注意所有的bean工厂和应用上下文都会自动地使用一些内置属性编辑器,这些编辑器通过一个被称为BeanWrapper
的接口来处理属性转换。BeanWrapper
注册的那些标准属性编辑器已经列在上一部分。 此外,针对特定的应用程序上下文类型,ApplicationContext
会用适当的方法覆盖或添加一些额外的编辑器来处理资源查找。
标准的JavaBeansPropertyEditor
实例用于将字符串表示的属性值转换成实际的复杂类型属性。CustomEditorConfigurer
,一个bean factory后处理器,可以为添加额外的PropertyEditor
到ApplicationContext
提供便利支持。
考虑一个用户类ExoticType
和另外一个需要将ExoticType
设为属性的类DependsOnExoticType
:
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 {
public void setAsText(String text) {
setValue(new ExoticType(text.toUpperCase()));
}
}
最后,我们使用CustomEditorConfigurer
将一个新的PropertyEditor
注册到ApplicationContext
,那么在需要的时候就能够使用它:
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
</map>
</property>
</bean>
使用PropertyEditorRegistrar {#toc_8}
另一种将属性编辑器注册到Spring容器的机制是创建和使用一个PropertyEditorRegistrar
。当你需要在几个不同场景里使用同一组属性编辑器,这个接口会特别有用:编写一个相应的registrar并在每个用例里重用。PropertyEditorRegistrar
与一个被称为PropertyEditorRegistry
的接口配合工作,后者被Spring的BeanWrapper
(以及DataBinder
)实现。当与CustomEditorConfigurer
配合使用的时候,PropertyEditorRegistrar
特别方便(这里有介绍),因为前者暴露了一个方法setPropertyEditorRegistrars(..)
:以这种方式添加到CustomEditorConfigurerd
的PropertyEditorRegistrar
可以很容易地在DataBinder
和Spring MVCControllers
之间共享。另外,它避免了在自定义编辑器上的同步需求:一个PropertyEditorRegistrar
可以为每一次bean创建尝试创建新的PropertyEditor
实例。
使用PropertyEditorRegistrar
可能最好还是以一个例子来说明。首先,你需要创建你自己的PropertyEditorRegistrar
实现:
package com.foo.editors.spring;
public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
public void registerCustomEditors(PropertyEditorRegistry registry) {
// it is expected that new PropertyEditor instances are created
registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());
// you could register as many custom property editors as are required here...
}
}
也可以查看org.springframework.beans.support.ResourceEditorRegistrar
当作一个PropertyEditorRegistrar
实现的示例。注意在它的registerCustomEditors(..)
方法实现里是如何为每个属性编辑器创建新的实例的。
接着我们配置了一个CustomEditorConfigurerd
并将我们的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’s MVC web framework的那些人,使用PropertyEditorRegistrar
与数据绑定的Controller
(比如SimpleFormController
)配合使用会非常方便。下面是一个在initBinder(..)
方法的实现里使用PropertyEditorRegistrar
的例子:
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);
}
// other methods to do with registering a User
}
这种PropertyEditor
注册的风格可以导致简洁的代码(initBinder(..)
的实现仅仅只有一行!),同时也允许将通用的PropertyEditor
注册代码封装到一个类里然后根据需要在尽可能多的Controller
之间共享。