Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion(一)

题外话:本篇是对之前那篇的重排版。并拆分成两篇,免得没了看的兴趣。

前言

在Spring Framework官方文档中,这三者是放到一起讲的,但没有解释为什么放到一起。大概是默认了读者都是有相关经验的人,但事实并非如此,例如我。好在闷着头看了一遍,又查资料又敲代码,总算明白了。

其实说穿了一文不值,我们用一个例子来解释:

假定,现有一个app,功能是接收你输入的生日,然后显示你的年龄。看起来app只要用当前日期减去你输入的日期就是年龄,应该很简单对吧?可惜事实不是这样的。

这里面有三个问题:

问题一:我们输入的永远是字符串,字符串需要转成日期格式才能被我们的app用使用。--对应 类型转换

问题二:我们输入的字符串转成的日期怎么给app后台逻辑使用? --对应 数据绑定

问题三:人的年龄是有限制的,不能为负数,不能太大(例如超过了200)。 --对应 校验

同样的问题也出现在浏览器与服务器的交互之中,因为请求与响应,大都是被解析成字符串。

现在,你应该已经明白Validation、Data Binding、Type Conversion三者之间的关系了,它们彼此独立,但又互相配合。

前提

在了解更多之前,你应该先知道两个关键的概念:JavaBeanProperty

JavaBean是一个简单类,无参构造,命名惯例(SETTER/GETTER) -- 其标准由Oracle提供!详见 JavaBeansJavaBean wiki

SETTER/GETTER 对应的部分称为Property(属性)。

另外,org.springframework.beans 包 遵守Oracle提供的JavaBean标准。但是JavaBean 和 Spring的bean 不是同一个概念!

概览

现在我们来看看具体的定义以及Spring中提供的工具:

Validation 校验:对Property进行校验。--【谁的Property?JavaBean的!】

Spring提供了Validator接口,可在任意layer使用。

Data Binding 数据绑定:将数据绑定到Property上。--【谁的Property?JavaBean的!】

Spring提供了DataBinder来完成具体的数据绑定工作。

Validator和DataBinder都在org.springframework.validation包中。

Type Conversion 类型转换:将一种类型的对象转成另一种类型的对象,例如String与Date之间。--【谁的类型?Property的!】

Spring提供了PropertyEditors 以及core.convert 包和format 包。后两者是Spring 3 引入的,可以看作PropertyEditor 的替代品,更简单。

注意到没有,这三者其实都是在操作JavaBean的Property。

那么问题又来了,Spring如何操作JavaBean及其Property?答案是通过BeanWrapper接口和其实现BeanWrapperImpl,其内部通过PropertyEditors来解析和格式化Property。

BeanWrapper这个东西是很底层的概念,用户一般不必直接使用它,了解即可。

另,PropertyEditor是JavaBeans specification的一部分!

深入

下面来研究下Spring这些工具的具体用法:

1、Validator,查看源码可知,该接口只有两个方法,supports(Class<?> clazz)用于判断是否支持某类;validate(Object target, Errors errors)则用于校验,如有错误信息则报告给Errors -- 建议配合工具类ValidationUtils来使用。

实现该接口即可定义自己的Validator,代码如下:

Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion(一)
1 public class Person {
2
3 private String name;
4 private int age;
5
6 // getters and setters...略
7 }
Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion(一)
 1 public class PersonValidator implements Validator {
2
3 public boolean supports(Class clazz) {
4 return Person.class.equals(clazz); // 仅支持Person类
5 }
6
7 public void validate(Object obj, Errors e) {
8 ValidationUtils.rejectIfEmpty(e, "name", "name.empty"); // 使用工具类,效果和下面类似
9 Person p = (Person) obj;
10 if (p.getAge() < 0) { // 年龄不能小于0
11 e.rejectValue("age", "negativevalue");
12 } else if (p.getAge() > 110) { // 年龄不能大于110
13 e.rejectValue("age", "too.darn.old");
14 }
15 }
16 }

注意,如果是复合类的校验,还可以注入已有的Validator -- 复用、高效。

2、Resolving code to error message

如果我们想要通过MessageSource输出error message,我们会使用之前填入error code来索引。

当我们直接或间接的调用Errors的reject方法时,其实现不仅会注册我们传入的code,同时还会注册一些额外的error code。具体注册的error code是由MessageCodesResolver决定的。

默认情况下,会使用DefaultMessageCodesResolver,它不仅注册了你传入的code,还注册了字段名!

例如,你使用rejectValue(”age”, ”too.darn.old”),不仅会注册 ”too.darn.old”,还会注册 ”too.darn.old.age” 和 ”too.darn.old.age.int”。

更多策略见MessageCodesResolver和DefaultMessageCodesResolver的JavaDoc。

上面这两个,怎么说呢,没有涉及到反射之类的。这与下面要说的有所不同,提前说一下。

3、BeanWrapper,位于org.springframework.beans包中。

根据其JavaDoc可知,BeanWrapper提供的功能包括:set/get property values (单个/多个), get property descriptor, 以及查询判断property是可读的还是可写的。还支持nested property。还支持添加standard JavaBeans PropertyChangeListeners and VetoableChangeListeners,无需在目标类中编码(这不是废话么,类似aop的监听器)。最后还支持the setting of indexed properties。

BeanWrapper一般不直接用在代码中,而是用在DataBinder 和 BeanFactory 中。

另外,顾名思义,BeanWrapper的工作方式是wrap一个bean以执行操作。

下面讲一下其具体功能:

3.1、Setting and getting basic and nested properties

就是set/get property values(基本的和嵌套的),通过BeanWrapper的setPropertyValues()和getPropertyValues()方法完成 -- 详见JavaDoc。

这里需要重点了解的就是几个约定,例子如下:

Expression 解释
name Property name。
account.name nested property name of the property account
account[2] the 3rd element of the indexed property account
account[COMPANYNAME] map

因为我们基本用不到它,仅作了解即可,下面的代码可以略过。

Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion(一)
 1 BeanWrapper company = new BeanWrapperImpl(new Company());
2
3 // 设置公司名字
4 company.setPropertyValue("name", "Some Company Inc.");
5
6 // 也可以这样做
7 PropertyValue value = new PropertyValue("name", "Some Company Inc.");
8 company.setPropertyValue(value);
9
10 // 创建director,绑到公司
11 BeanWrapper jim = new BeanWrapperImpl(new Employee());
12 jim.setPropertyValue("name", "Jim Stravinsky");
13 company.setPropertyValue("managingDirector", jim.getWrappedInstance());
14
15 // 获取公司中managingDirector的salary
16 Float salary = (Float) company.getPropertyValue("managingDirector.salary"); // nested property

3.2、内建的PropertyEditor实现

必须再说一遍,PropertyEditor是JavaBeans specification的一部分,不是Spring的东西! 其全限定名:java.beans.PropertyEditor。

Spring是利用这个概念进行Object与String之间的转换而已。但它本身是个接口(abstraction啦),所以Spring提供了一堆实现供大家使用 -- 需要注册到BeanWrapper或者IoC容器中。

下面是两个使用PropertyEditor的例子:

1,你在xml中定义的bean,其class属性是通过ClassEditor 来转成相应的类。

2,Spring MVC中对HTTP 请求的各种解析。

再来看看Spring提供的实现,它们位于org.springframework.beans.propertyeditors 包中。默认情况下,其中的多数都已由BeanWrapper注册了,可以直接使用。当然,你仍然可以注册自己的变体来覆盖掉默认的。--【这里有个很大的陷阱,所谓的注册,与ApplicationContext无关!!!】

如下:

Built-in PropertyEditors

解释
ByteArrayPropertyEditor 默认被BeanWrapperImpl注册。
ClassEditor 默认被BeanWrapperImpl注册。
CustomBooleanEditor 默认被BeanWrapperImpl注册。可被覆盖!
CustomCollectionEditor  
CustomDateEditor 默认没有注册!!!
CustomNumberEditor 默认被BeanWrapperImpl注册。可被覆盖!
FileEditor 默认被BeanWrapperImpl注册。
InputStreamEditor 默认被BeanWrapperImpl注册。默认不关闭InputStream!
LocaleEditor 默认被BeanWrapperImpl注册。
PatternEditor  
PropertiesEditor 默认被BeanWrapperImpl注册。
StringTrimmerEditor trim string,且可选将empty string转成null。默认没有注册!
URLEditor 默认被BeanWrapperImpl注册。

Spring使用 java.beans.PropertyEditorManager 来设置搜索路径,搜索路径默认包含了sun.bean.editors -- 这里有针对Font、Color以及大多数常见类型的PropertyEditor实现!

注意,standard JavaBeans infrastructure 会自动发现同路径下的PropertyEditor,前提是它们和对应的类名一致,且以’Editor’结尾。例如:

cn.larry.domain.User

cn.larry.domain.UserEditor //这个Editor会被自动发现。

还可以使用standard BeanInfo JavaBeans mechanism,注册一个或多个PropertyEditor。如下:

cn.larry.domain.Foo

cn.larry.domain.FooBeanInfo

Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion(一)
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());
}
}
}

补充:

java.beans.BeanInfo 接口,用于提供bean的method、property、events等信息。建议使用SimpleBeanInfo。

java.beans.Introspector先查找与target bean class同包路径下的BeanInfo(以BeanInfo结尾),如果没有,再查找每个包中是否存在。

-- 例如,对"sun.xyz.OurButton"来说,先查找"sun.xyz.OurButtonBeanInfo",如果失败再查找其他包中是否存在一个OurButtonBeanInfo class。

3.2.1、注册其他自定义的PropertyEditors

注意,这里的其他是指除了上面(3.2)提到的两种方式,你可以选择上面的方式,也可以选择这里的方式。

有三种方法:

a> 最笨且最不推荐的方法:使用 ConfigurableBeanFactory 接口的 registerCustomEditor() 方法,前提是拥有BeanFactory引用。

b> 稍微方便点的方法:使用一个特殊的bean factory post-processor --- CustomEditorConfigurer。虽然可以在BeanFactory 实现中使用,但更建议在ApplicationContext中使用。

注意,所有的bean factories 和 application contexts 都会自动应用大量的内建property editors。

前面有提到,BeanWrapperImpl会自动注册一些,此外,具体的ApplicationContext 还会覆盖或者添加额外的editors。

例子,先来两个类:

Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion(一)
 1 package example;
2
3 public class Person {
4
5 private String name;
6
7 public Person(String name) {
8 this.name = name;
9 }
10 }
11
12 public class Team {
13
14 private Person person;
15
16 public void setPerson(Person person) {
17 this.person = person;
18 }
19 }

下面就会调用幕后的PropertyEditor --注意,这里的value是String,后台editor会将其转成 Person类型。

<bean id="sample" class="example.Team">
<property name="person" value="abc"/>
</bean>

该editor大概类似这样:

Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion(一)
1 // 将String转成Person对象
2 package example;
3
4 public class PersonEditor extends PropertyEditorSupport {
5
6 public void setAsText(String text) {
7 setValue(new Person(text.toUpperCase()));
8 }
9 }

关键是,如何将该editor注册到ApplicationContext中:

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="example.PersonType" value="example.PersonEditor"/>
</map>
</property>
</bean>

c> 使用PropertyEditorRegistrars,需要手动创建它。在复用时很有用,因为它打包了一组editor,拿来即用。(听起来,是类似map或者set之类的集合??)

直接上代码吧

首先,创建你的 PropertyEditorRegistrar (可以参考 org.springframework.beans.support.ResourceEditorRegistrar):

Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion(一)
 1 package cn.larry.editors.spring;
2
3 public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
4
5 public void registerCustomEditors(PropertyEditorRegistry registry) {
6
7 // 需要PropertyEditor实例
8 registry.registerCustomEditor(Person.class, new PersonEditor());
9
10 // 可以注册任意多的PropertyEditor...
11 }
12 }

然后,配置CustomEditorConfigurer ,注入我们的CustomPropertyEditorRegistrar :

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="propertyEditorRegistrars">
<list>
<ref bean="customPropertyEditorRegistrar"/>
</list>
</property>
</bean> <bean id="customPropertyEditorRegistrar" class="cn.larry.editors.spring.CustomPropertyEditorRegistrar"/>

最后,在使用Spring MVC框架时,使用CustomPropertyEditorRegistrars 配合data-binding Controllers(如SimpleFormController)会是非常方便的(--暂时不明白,以后再来看吧)。

见下例:

Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion(一)
 1 // 无语,Spring 4已经不再支持SimpleFormController了!!!使用@Controller代替
2 // 但是,仍然木有明白本类的作用!以及,为毛final???
3 // 有个带参构造,默认会调用这个创建实例,那么,注入的是???
4 // 也没见到@Autowired啊
5 // 另外,protected方法是干嘛的???
6 // -- 难道说,这个是给别的Controller调用的?
7 @Controller
8 public final class RegisterUserController /*extends SimpleFormController*/ {
9
10 private final PropertyEditorRegistrar customPropertyEditorRegistrar;
11
12 public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
13 this.customPropertyEditorRegistrar = propertyEditorRegistrar;
14 }
15
16 protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception {
17 this.customPropertyEditorRegistrar.registerCustomEditors(binder);
18 }
19
20 // other methods to do with registering a User
21 }

这种风格的PropertyEditor 注册能简洁代码(the implementation of initBinder(..) is just one line long!),且允许通用的PropertyEditor 注册代码包含在一个类中--然后由所有需要的Controllers 共享。(这个是重点吧???)

结束

为了限制篇幅长短,本篇到此为止,其他内容见下一篇。内容是:Spring Type Conversion(ConversionService)、Spring Field Formatting、globle date & time format、Spring Validation。

注意,

1、本篇提到的PropertyEditor是最早的类型转换,但仅限于Object与String之间。ConversionService则不限于此,更灵活方便,是PropertyEditor的替代品。

2、本篇只提到了Validator接口,但没提及如何集成到Spring中,下一篇会谈到。

下一篇:

Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion(二)

20161013补充:

1,关于JavaBeans,请见我的另一篇文章:JavaBeans 官方文档学习

2,所谓的注册PropertyEditor,是不是在ApplicationContext注册bean!而是让standard JavaBeans infrastructure能够发现相应的PropertyEditor!

3,关于BeanWrapperImpl,从Spring 2.5起,主要限于内部使用。建议使用PropertyAccessorFactory.forBeanPropertyAccess工厂方法代替。

上一篇:Oracle 存储过程(2)


下一篇:Web 前沿——HTML5 Form Data 对象的使用(转)