题外话:本篇是对之前那篇的重排版。并拆分成两篇,免得没了看的兴趣。
前言
在Spring Framework官方文档中,这三者是放到一起讲的,但没有解释为什么放到一起。大概是默认了读者都是有相关经验的人,但事实并非如此,例如我。好在闷着头看了一遍,又查资料又敲代码,总算明白了。
其实说穿了一文不值,我们用一个例子来解释:
假定,现有一个app,功能是接收你输入的生日,然后显示你的年龄。看起来app只要用当前日期减去你输入的日期就是年龄,应该很简单对吧?可惜事实不是这样的。
这里面有三个问题:
问题一:我们输入的永远是字符串,字符串需要转成日期格式才能被我们的app用使用。--对应 类型转换
问题二:我们输入的字符串转成的日期怎么给app后台逻辑使用? --对应 数据绑定
问题三:人的年龄是有限制的,不能为负数,不能太大(例如超过了200)。 --对应 校验
同样的问题也出现在浏览器与服务器的交互之中,因为请求与响应,大都是被解析成字符串。
现在,你应该已经明白Validation、Data Binding、Type Conversion三者之间的关系了,它们彼此独立,但又互相配合。
前提
在了解更多之前,你应该先知道两个关键的概念:JavaBean 和 Property。
JavaBean是一个简单类,无参构造,命名惯例(SETTER/GETTER) -- 其标准由Oracle提供!详见 JavaBeans 或 JavaBean 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,代码如下:
1 public class Person {
2
3 private String name;
4 private int age;
5
6 // getters and setters...略
7 }
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 |
因为我们基本用不到它,仅作了解即可,下面的代码可以略过。
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
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。
例子,先来两个类:
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大概类似这样:
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):
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)会是非常方便的(--暂时不明白,以后再来看吧)。
见下例:
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工厂方法代替。