Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion(一)
题外话:本篇是对之前那篇的重排版。并拆分成两篇,免得没了看的兴趣。
前言
在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工厂方法代替。
Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion(一)的更多相关文章
- Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion(二)
接前一篇 Spring Framework 官方文档学习(四)之Validation.Data Binding.Type Conversion(一) 本篇主要内容:Spring Type Conver ...
- Spring Framework 官方文档学习(四)之Validation、Data Binding、Type Conversion
本篇太乱,请移步: Spring Framework 官方文档学习(四)之Validation.Data Binding.Type Conversion(一) 写了删删了写,反复几次,对自己的描述很不 ...
- Spring Framework 官方文档学习(二)之IoC容器与bean lifecycle
到目前为止,已经看了一百页.再次感慨下,如果想使用Spring,那可以看视频或者找例子,但如果想深入理解Spring,最好还是看官方文档. 原计划是把一些基本接口的功能.层次以及彼此的关系罗列一下.同 ...
- Spring Framework 官方文档学习(一)介绍
http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#overview-maven-bom ...
- Spring Framework 官方文档学习(三)之Resource
起因 标准JDK中使用 java.net.URL 来处理资源,但有很多不足,例如不能限定classpath,不能限定 ServletContext 路径. 所以,Spring提供了 Resource ...
- Spring 4 官方文档学习(十二)View技术
关键词:view technology.template.template engine.markup.内容较多,按需查用即可. 介绍 Thymeleaf Groovy Markup Template ...
- Spring 4 官方文档学习(十一)Web MVC 框架
介绍Spring Web MVC 框架 Spring Web MVC的特性 其他MVC实现的可插拔性 DispatcherServlet 在WebApplicationContext中的特殊的bean ...
- Spring Boot 官方文档学习(一)入门及使用
个人说明:本文内容都是从为知笔记上复制过来的,样式难免走样,以后再修改吧.另外,本文可以看作官方文档的选择性的翻译(大部分),以及个人使用经验及问题. 其他说明:如果对Spring Boot没有概念, ...
- Spring boot官方文档学习(一)
个人说明:本文内容都是从为知笔记上复制过来的,样式难免走样,以后再修改吧.另外,本文可以看作官方文档的选择性的翻译(大部分),以及个人使用经验及问题. 其他说明:如果对Spring Boot没有概念, ...
随机推荐
- 使用codedom 编写脚本解释器
本篇博客的目的是为了保存例子,怕自己忘记. private void dd(string code) { string path = "BonkerSpace"; if (File ...
- vue轮播图插件vue-awesome-swiper的使用与组件化
不管是APP还是移动端网页开发,轮播图在大部分项目当中都是存在的,这时候如果用vue开发项目,选择一款好的插件并且封装好是很重要的 1. 推荐使用vue-awesome-swiper 安装:cnpm ...
- Oracle数据库字符集问题解析
Oracle数据库字符集问题解析 经常看到一些朋友问ORACLE字符集方面的问题,我想以迭代的方式来介绍一下.第一次迭代:掌握字符集方面的基本概念.有些朋友可能会认为这是多此一举,但实际上正是由于对相 ...
- ClouderaManager启动NodeManager失败!报错Failed to initialize container executor
报错信息: 2016-07-27 10:53:14,102 WARN org.apache.hadoop.yarn.server.nodemanager.LinuxContainerExecutor: ...
- android中activity向service中传值
和activity中互相传值类似 在activity中 Intent regIntent = new Intent(this, ChatService.class); regIntent.putEx ...
- Redis简述
Redis 简单介绍 Redis 是全然开源免费的.遵守BSD协议,是一个高性能的key-value数据库. Redis 与其它 key - value 缓存产品有下面三个特点: Redis支持数据的 ...
- 迭代获取ViewState
string s=""; System.Collections.IDictionaryEnumerator ie=ViewState.GetEnumerator(); while ...
- C语言 · 十六进制转八进制
基础练习 十六进制转八进制 时间限制:1.0s 内存限制:512.0MB 锦囊1: 使用二进制. 问题描述 给定n个十六进制正整数,输出它们对应的八进制数. 输入格式 输入的 ...
- protobuf--数据序列化及反序列化
ProtoBuf是一种灵活高效的独立于语言平台的结构化数据表示方法,可用于表示通信协议和数据存储等各方面,与XML相比,ProtoBuF更小更快更简单.你可以用定义自己ProtoBuf的数据结构,用P ...
- KafkaStream实现wordcount
KTable应用 KTable wordCounts = textLines // Split each text line, by whitespace, into words. .flatMapV ...