基于spring-framework-3.1.1.RELEASE 7.1、简介在编写可视化界面项目时,我们通常需要对数据进行类型转换、验证及格式化。
一、在Spring3之前,我们使用如下架构进行类型转换、验证及格式化: 流程: ①:类型转换:首先调用PropertyEditor的setAsText(String),内部根据需要调用setValue(Object)方法进行设置转换后的值; ②:数据验证:需要显示调用Spring的Validator接口实现进行数据验证; ③:格式化显示:需要调用PropertyEditor的getText进行格式化显示。
使用如上架构的缺点是: (1、PropertyEditor被设计为只能String<——>Object之间转换,不能任意对象类型<——>任意类型,如我们常见的Long时间戳到Date类型的转换是办不到的; (2、PropertyEditor是线程不安全的,也就是有状态的,因此每次使用时都需要创建一个,不可重用; (3、PropertyEditor不是强类型的,setValue(Object)可以接受任意类型,因此需要我们自己判断类型是否兼容; (4、需要自己编程实现验证,Spring3支持更棒的注解验证支持; (5、在使用SpEL表达式语言或DataBinder时,只能进行String<--->Object之间的类型转换;
流程:
②:数据验证:在控制器中的功能处理方法中,需要显示的调用Spring的Validator实现并将错误信息添加到BindingResult对象中; ③:格式化显示:在表单页面可以通过如下方式展示通过
<%@taglib prefix="spring" uri="http://www.springframework.org/tags" %> <%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %> 首先需要通过如上taglib指令引入spring的两个标签库。
//1、格式化单个命令/表单对象的值(好像比较麻烦,真心没有好办法) <spring:bind path="dataBinderTest.phoneNumber">${status.value}</spring:bind>
//2、<spring:eval>标签,自动调用ConversionService并选择相应的Converter SPI进行格式化展示 <spring:eval expression="dataBinderTest.phoneNumber"></spring:eval> 如上代码能工作的前提是在RequestMappingHandlerMapping配置了ConversionServiceExposingInterceptor,它的作用是暴露conversionService到请求中以便如<spring:eval>标签使用。
//3、通过form标签,内部的表单标签会自动调用命令/表单对象属性对应的PropertyEditor进行格式化显示 <form:form commandName="dataBinderTest"> <form:input path="phoneNumber"/><!-- 如果出错会显示错误之前的数据而不是空 --> </form:form>
//4、显示验证失败后的错误信息 <form:errors></form:errors> 接下来我们就详细学习一下这些知识吧。
7.2、数据类型转换7.2.1、Spring3之前的PropertyEditorPropertyEditor介绍请参考【4.16.1、数据类型转换】。
一、测试之前我们需要准备好测试环境: (1、模型对象,和【4.16.1、数据类型转换】使用的一样,需要将DataBinderTestModel模型类及相关类拷贝过来放入cn.javass.chapter7.model包中。 (2、控制器定义:
package cn.javass.chapter7.web.controller; //省略import @Controller public class DataBinderTestController { @RequestMapping(value = "/dataBind") public String test(DataBinderTestModel command) { //输出command对象看看是否绑定正确 System.out.println(command); model.addAttribute("dataBinderTest", command); return "bind/success"; } }
(3、Spring配置文件定义,请参考chapter7-servlet.xml,并注册DataBinderTestController:
<bean class="cn.javass.chapter7.web.controller.DataBinderTestController"/> (4、测试的URL:
http://localhost:9080/springmvc-chapter7/dataBind?username=zhang&bool=yes&schooInfo.specialty=computer&hobbyList[0]=program&hobbyList[1]=music&map[key1]=value1&map[key2]=value2&phoneNumber=010-12345678&date=2012-3-18 16:48:48&state=blocked
二、注解式控制器注册PropertyEditor: 1、使用WebDataBinder进行控制器级别注册PropertyEditor(控制器独享)
@InitBinder //此处的参数也可以是ServletRequestDataBinder类型 public void initBinder(WebDataBinder binder) throws Exception { //注册自定义的属性编辑器 //1、日期 DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); CustomDateEditor dateEditor = new CustomDateEditor(df, true); //表示如果命令对象有Date类型的属性,将使用该属性编辑器进行类型转换 binder.registerCustomEditor(Date.class, dateEditor); //自定义的电话号码编辑器(和【4.16.1、数据类型转换】一样) binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor()); } 和【4.16.1、数据类型转换】一节类似,只是此处需要通过@InitBinder来注册自定义的PropertyEditor。
2、使用 和【4.16.1、数据类型转换】不太一样,因为我们的注解式控制器是POJO,没有实现任何东西,因此无法注入WebBindingInitializer,此时我们需要把WebBindingInitializer注入到我们的RequestMappingHandlerAdapter或AnnotationMethodHandlerAdapter,这样对于所有的注解式控制器都是共享的。
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"> <property name="webBindingInitializer"> <bean class="cn.javass.chapter7.web.controller.support.initializer.MyWebBindingInitializer"/> </property> </bean>
此时我们注释掉控制器级别通过@InitBinder注册PropertyEditor的方法。
3、全局级别注册PropertyEditor(全局共享) 和【4.16.1、数据类型转换】一节一样,此处不再重复。请参考【4.16.1、数据类型转换】的【全局级别注册PropertyEditor(全局共享)】。
7.2.2、Spring3开始的类型转换系统Spring3引入了更加通用的类型转换系统,其定义了SPI接口(Converter等)和相应的运行时执行类型转换的API(ConversionService等),在Spring中它和PropertyEditor功能类似,可以替代PropertyEditor来转换外部Bean属性的值到Bean属性需要的类型。
该类型转换系统是Spring通用的,其定义在org.springframework.core.convert包中,不仅仅在Spring Web MVC场景下。目标是完全替换PropertyEditor,提供无状态、强类型且可以在任意类型之间转换的类型转换系统,可以用于任何需要的地方,如SpEL、数据绑定。
Converter SPI完成通用的类型转换逻辑,如java.util.Date<---->java.lang.Long或java.lang.String---->PhoneNumberModel等。 7.2.2.1、架构1、类型转换器:提供类型转换的实现支持。
一个有如下三种接口: (1、Converter:类型转换器,用于转换S类型到T类型,此接口的实现必须是线程安全的且可以被共享。
package org.springframework.core.convert.converter; public interface Converter<S, T> { //① S是源类型 T是目标类型 T convert(S source); //② 转换S类型的source到T目标类型的转换方法 } 示例:请参考cn.javass.chapter7.converter.support.StringToPhoneNumberConverter转换器,用于将String--->PhoneNumberModel。
此处我们可以看到Converter接口实现只能转换一种类型到另一种类型,不能进行多类型转换,如将一个数组转换成集合,如(String[] ----> List<String>、String[]----->List<PhoneNumberModel>等)。
(2、GenericConverter和ConditionalGenericConverter:GenericConverter接口实现能在多种类型之间进行转换,ConditionalGenericConverter是有条件的在多种类型之间进行转换。
package org.springframework.core.convert.converter; public interface GenericConverter { Set<ConvertiblePair> getConvertibleTypes(); Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); } getConvertibleTypes:指定了可以转换的目标类型对; convert:在sourceType和targetType类型之间进行转换。
package org.springframework.core.convert.converter; public interface ConditionalGenericConverter extends GenericConverter { boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType); } matches:用于判断sourceType和targetType类型之间能否进行类型转换。
示例:如org.springframework.core.convert.support.ArrayToCollectionConverter和CollectionToArrayConverter用于在数组和集合间进行转换的ConditionalGenericConverter实现,如在String[]<---->List<String>、String[]<---->List<PhoneNumberModel>等之间进行类型转换。
对于我们大部分用户来说一般不需要自定义GenericConverter, 如果需要可以参考内置的GenericConverter来实现自己的。
(3、ConverterFactory:工厂模式的实现,用于选择将一种S源类型转换为R类型的子类型T的转换器的工厂接口。
package org.springframework.core.convert.converter; public interface ConverterFactory<S, R> { <T extends R> Converter<S, T> getConverter(Class<T> targetType); } S:源类型;R目标类型的父类型;T:目标类型,且是R类型的子类型; getConverter:得到目标类型的对应的转换器。
示例:如org.springframework.core.convert.support.NumberToNumberConverterFactory用于在Number类型子类型之间进行转换,如Integer--->Double, Byte---->Integer, Float--->Double等。
对于我们大部分用户来说一般不需要自定义ConverterFactory,如果需要可以参考内置的ConverterFactory来实现自己的。
2、类型转换器注册器、类型转换服务:提供类型转换器注册支持,运行时类型转换API支持。 (1、ConverterRegistry:类型转换器注册支持,可以注册/删除相应的类型转换器。
package org.springframework.core.convert.converter; public interface ConverterRegistry { void addConverter(Converter<?, ?> converter); void addConverter(Class<?> sourceType, Class<?> targetType, Converter<?, ?> converter); void addConverter(GenericConverter converter); void addConverterFactory(ConverterFactory<?, ?> converterFactory); void removeConvertible(Class<?> sourceType, Class<?> targetType); } 可以注册:Converter实现,GenericConverter实现,ConverterFactory实现。
(2、ConversionService:运行时类型转换服务接口,提供运行期类型转换的支持。
package org.springframework.core.convert; public interface ConversionService { boolean canConvert(Class<?> sourceType, Class<?> targetType); boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType); <T> T convert(Object source, Class<T> targetType); Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); } convert:将源对象转换为目标类型的目标对象。
Spring提供了两个默认实现(其都实现了ConverterRegistry、ConversionService接口): DefaultConversionService:默认的类型转换服务实现; DefaultFormattingConversionService:带数据格式化支持的类型转换服务实现,一般使用该服务实现即可。
7.2.2.2、Spring内建的类型转换器如下所示:
S:代表源类型,T:代表目标类型 如上的转换器在使用转换服务实现DefaultConversionService和DefaultFormattingConversionService时会自动注册。
7.2.2.3、示例(1、自定义String----->PhoneNumberModel的转换器
package cn.javass.chapter7.web.controller.support.converter; //省略import public class StringToPhoneNumberConverter implements Converter<String, PhoneNumberModel> { Pattern pattern = Pattern.compile("^(\\d{3,4})-(\\d{7,8})$"); @Override public PhoneNumberModel convert(String source) { if(!StringUtils.hasLength(source)) { //①如果source为空 返回null return null; } Matcher matcher = pattern.matcher(source); if(matcher.matches()) { //②如果匹配 进行转换 PhoneNumberModel phoneNumber = new PhoneNumberModel(); phoneNumber.setAreaCode(matcher.group(1)); phoneNumber.setPhoneNumber(matcher.group(2)); return phoneNumber; } else { //③如果不匹配 转换失败 throw new IllegalArgumentException(String.format("类型转换失败,需要格式[010-12345678],但格式是[%s]", source)); } } }
String转换为Date的类型转换器,请参考cn.javass.chapter7.web.controller.support.converter.StringToDateConverter。
(2、测试用例(cn.javass.chapter7.web.controller.support.converter.ConverterTest)
@Test public void testStringToPhoneNumberConvert() { DefaultConversionService conversionService = new DefaultConversionService(); conversionService.addConverter(new StringToPhoneNumberConverter()); String phoneNumberStr = "010-12345678"; PhoneNumberModel phoneNumber = conversionService.convert(phoneNumberStr, PhoneNumberModel.class); Assert.assertEquals("010", phoneNumber.getAreaCode()); } 类似于PhoneNumberEditor将字符串“010-12345678”转换为PhoneNumberModel。
@Test public void testOtherConvert() { DefaultConversionService conversionService = new DefaultConversionService(); //"1"--->true(字符串“1”可以转换为布尔值true) Assert.assertEquals(Boolean.valueOf(true), conversionService.convert("1", Boolean.class)); //"1,2,3,4"--->List(转换完毕的集合大小为4) Assert.assertEquals(4, conversionService.convert("1,2,3,4", List.class).size()); } 其他类型转换器使用也是类似的,此处不再重复。 7.2.2.4、集成到Spring Web MVC环境(1、注册ConversionService实现和自定义的类型转换器
<!-- ①注册ConversionService --> <bean id="conversionService" class="org.springframework.format.support. FormattingConversionServiceFactoryBean"> <property name="converters"> <list> <bean class="cn.javass.chapter7.web.controller.support. converter.StringToPhoneNumberConverter"/> <bean class="cn.javass.chapter7.web.controller.support. converter.StringToDateConverter"> <constructor-arg value="yyyy-MM-dd"/> </bean> </list> </property> </bean> FormattingConversionServiceFactoryBean:是FactoryBean实现,默认使用DefaultFormattingConversionService转换器服务实现; converters:注册我们自定义的类型转换器,此处注册了String--->PhoneNumberModel和String--->Date的类型转换器。
(2、通过ConfigurableWebBindingInitializer注册ConversionService
<!-- ②使用ConfigurableWebBindingInitializer注册conversionService --> <bean id="webBindingInitializer" class="org.springframework.web.bind.support. ConfigurableWebBindingInitializer"> <property name="conversionService" ref="conversionService"/> </bean> 此处我们通过ConfigurableWebBindingInitializer绑定初始化器进行ConversionService的注册;
3、注册ConfigurableWebBindingInitializer到RequestMappingHandlerAdapter
<bean class="org.springframework.web.servlet.mvc.method.annotation. RequestMappingHandlerAdapter"> <property name="webBindingInitializer" ref="webBindingInitializer"/> </bean> 通过如上配置,我们就完成了Spring3.0的类型转换系统与Spring Web MVC的集成。此时可以启动服务器输入之前的URL测试了。
此时可能有人会问,如果我同时使用PropertyEditor和ConversionService,执行顺序是什么呢?内部首先查找PropertyEditor进行类型转换,如果没有找到相应的PropertyEditor再通过ConversionService进行转换。
如上集成过程看起来比较麻烦,后边我们会介绍<mvc:annotation-driven>和@EnableWebMvc,ConversionService会自动注册,后续章节再详细介绍。
声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
返回顶楼 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
发表时间:2012-05-02 最后修改:2012-05-02
补充 PropertyEditor进行类型转换。
4.16.1、数据类型转换请求参数(String)——>命令对象属性(可能是任意类型)的类型转换,即数据绑定时的类型转换,使用PropertyEditor实现绑定时的类型转换。
一、Spring内建的PropertyEditor如下所示:
二、Spring内建的PropertyEditor支持的属性(符合JavaBean规范)操作:
三、示例: 接下来我们写自定义的属性编辑器进行数据绑定: (1、模型对象:
package cn.javass.chapter4.model; //省略import public class DataBinderTestModel { private String username; private boolean bool;//Boolean值测试 private SchoolInfoModel schooInfo; private List hobbyList;//集合测试,此处可以改为数组/Set进行测试 private Map map;//Map测试 private PhoneNumberModel phoneNumber;//String->自定义对象的转换测试 private Date date;//日期类型测试 private UserState state;//String——>Enum类型转换测试 //省略getter/setter } package cn.javass.chapter4.model; //如格式010-12345678 public class PhoneNumberModel { private String areaCode;//区号 private String phoneNumber;//电话号码 //省略getter/setter }
(2、PhoneNumber属性编辑器 前台输入如010-12345678自动转换为PhoneNumberModel。
package cn.javass.chapter4.web.controller.support.editor; //省略import public class PhoneNumberEditor extends PropertyEditorSupport { Pattern pattern = Pattern.compile("^(\\d{3,4})-(\\d{7,8})$"); @Override public void setAsText(String text) throws IllegalArgumentException { if(text == null || !StringUtils.hasLength(text)) { setValue(null); //如果没值,设值为null } Matcher matcher = pattern.matcher(text); if(matcher.matches()) { PhoneNumberModel phoneNumber = new PhoneNumberModel(); phoneNumber.setAreaCode(matcher.group(1)); phoneNumber.setPhoneNumber(matcher.group(2)); setValue(phoneNumber); } else { throw new IllegalArgumentException(String.format("类型转换失败,需要格式[010-12345678],但格式是[%s]", text)); } } @Override public String getAsText() { PhoneNumberModel phoneNumber = ((PhoneNumberModel)getValue()); return phoneNumber == null ? "" : phoneNumber.getAreaCode() + "-" + phoneNumber.getPhoneNumber(); } } PropertyEditorSupport:一个PropertyEditor的支持类; setAsText:表示将String——>PhoneNumberModel,根据正则表达式进行转换,如果转换失败抛出异常,则接下来的验证器会进行验证处理; getAsText:表示将PhoneNumberModel——>String。
(3、控制器 需要在控制器注册我们自定义的属性编辑器。 此处我们使用AbstractCommandController,因为它继承了BaseCommandController,拥有绑定流程。
package cn.javass.chapter4.web.controller; //省略import public class DataBinderTestController extends AbstractCommandController { public DataBinderTestController() { setCommandClass(DataBinderTestModel.class); //设置命令对象 setCommandName("dataBinderTest");//设置命令对象的名字 } @Override protected ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object command, BindException errors) throws Exception { //输出command对象看看是否绑定正确 System.out.println(command); return new ModelAndView("bindAndValidate/success").addObject("dataBinderTest", command); } @Override protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception { super.initBinder(request, binder); //注册自定义的属性编辑器 //1、日期 DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); CustomDateEditor dateEditor = new CustomDateEditor(df, true); //表示如果命令对象有Date类型的属性,将使用该属性编辑器进行类型转换 binder.registerCustomEditor(Date.class, dateEditor); //自定义的电话号码编辑器 binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor()); } } initBinder:第一个扩展点,初始化数据绑定器,在此处我们注册了两个属性编辑器; CustomDateEditor:自定义的日期编辑器,用于在String<——>日期之间转换; binder.registerCustomEditor(Date.class, dateEditor):表示如果命令对象是Date类型,则使用dateEditor进行类型转换; PhoneNumberEditor:自定义的电话号码属性编辑器用于在String<——> PhoneNumberModel之间转换; binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor()):表示如果命令对象是PhoneNumberModel类型,则使用PhoneNumberEditor进行类型转换;
<bean name="/dataBind" class="cn.javass.chapter4.web.controller.DataBinderTestController"/> (5、视图页面(WEB-INF/jsp/bindAndValidate/success.jsp)
EL phoneNumber:${dataBinderTest.phoneNumber}<br/> EL state:${dataBinderTest.state}<br/> EL date:${dataBinderTest.date}<br/> 视图页面的数据没有预期被格式化,如何进行格式化显示呢?请参考【第七章 注解式控制器的数据验证、类型转换及格式化】。
(6、测试: 1、在浏览器地址栏输入请求的URL,如 http://localhost:9080/springmvc-chapter4/dataBind?username=zhang&bool=yes&schooInfo.specialty=computer&hobbyList[0]=program&hobbyList[1]=music&map[key1]=value1&map[key2]=value2&phoneNumber=010-12345678&date=2012-3-18 16:48:48&state=blocked
2、控制器输出的内容: DataBinderTestModel [username=zhang, bool=true, schooInfo=SchoolInfoModel [schoolType=null, schoolName=null, specialty=computer], hobbyList=[program, music], map={key1=value1, key2=value2}, phoneNumber=PhoneNumberModel [areaCode=010, phoneNumber=12345678], date=Sun Mar 18 16:48:48 CST 2012, state=锁定] 类型转换如图所示:
四、注册PropertyEditor 1、使用WebDataBinder进行控制器级别注册PropertyEditor(控制器独享) 如“【三、示例】”中所使用的方式,使用WebDataBinder注册控制器级别的PropertyEditor,这种方式注册的PropertyEditor只对当前控制器独享,即其他的控制器不会自动注册这个PropertyEditor,如果需要还需要再注册一下。
2、使用 如果想在多个控制器同时注册多个相同的PropertyEditor时,可以考虑使用WebBindingInitializer。
示例: (1、实现WebBindingInitializer
package cn.javass.chapter4.web.controller.support.initializer; //省略import public class MyWebBindingInitializer implements WebBindingInitializer { @Override public void initBinder(WebDataBinder binder, WebRequest request) { //注册自定义的属性编辑器 //1、日期 DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); CustomDateEditor dateEditor = new CustomDateEditor(df, true); //表示如果命令对象有Date类型的属性,将使用该属性编辑器进行类型转换 binder.registerCustomEditor(Date.class, dateEditor); //自定义的电话号码编辑器 binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor()); } } 通过实现WebBindingInitializer并通过binder注册多个PropertyEditor。
(2、修改【三、示例】中的DataBinderTestController,注释掉initBinder方法;
(3、修改chapter4-servlet.xml配置文件:
<!-- 注册WebBindingInitializer实现 --> <bean id="myWebBindingInitializer" class="cn.javass.chapter4.web.controller.support.initializer.MyWebBindingInitializer"/> <bean name="/dataBind" class="cn.javass.chapter4.web.controller.DataBinderTestController"> <!-- 注入WebBindingInitializer实现 --> <property name="webBindingInitializer" ref="myWebBindingInitializer"/> </bean> (4、尝试访问“【三、示例】”中的测试URL即可成功。
使用WebBindingInitializer的好处是当你需要在多个控制器中需要同时使用多个相同的PropertyEditor可以在WebBindingInitializer实现中注册,这样只需要在控制器中注入WebBindingInitializer即可注入多个PropertyEditor。
3、全局级别注册PropertyEditor(全局共享) 只需要将我们自定义的PropertyEditor放在和你的模型类同包下即可,且你的Editor命名规则必须是“模型类名Editor”,这样Spring会自动使用标准JavaBean架构进行自动识别,如图所示:
此时我们把“DataBinderTestController”的“binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor());”注释掉,再尝试访问“【三、示例】”中的测试URL即可成功。
这种方式不仅仅在使用Spring时可用,在标准的JavaBean等环境都是可用的,可以认为是全局共享的(不仅仅是Spring环境)。
PropertyEditor被限制为只能String<——>Object之间转换,不能Object<——>Object,Spring3提供了更强大的类型转换(Type
如果我在地址栏输入错误的数据,即数据绑定失败,Spring Web MVC该如何处理呢?如果我输入的数据不合法呢?如用户名输入100个字符(超长了)那又该怎么处理呢?出错了需要错误消息,那错误消息应该是硬编码?还是可配置呢? |