当前位置: 首页 > 文档资料 > 跟我学 Spring MVC >

Spring3 Web MVC下的数据类型转换(第一篇)——《跟我学Spring3 Web MVC》抢先看

优质
小牛编辑
117浏览
2023-12-01

基于spring-framework-3.1.1.RELEAgen-wo-xue-spring/content/SE

7.1、简介

在编写可视化界面项目时,我们通常需要对数据进行类型转换、验证及格式化。

一、在gen-wo-xue-spring/content/Spring3之前,我们使用如下架构进行类型转换、验证及格式化:

流程:

①:类型转换:首先调用PropertyEditor的setAsText(gen-wo-xue-spring/content/String),内部根据需要调用setValue(Object)方法进行设置转换后的值;

②:数据验证:需要显示调用gen-wo-xue-spring/content/Spring的Validator接口实现进行数据验证;

③:格式化显示:需要调用PropertyEditor的getText进行格式化显示。

使用如上架构的缺点是:

(1、PropertyEditor被设计为只能gen-wo-xue-spring/content/String<——>Object之间转换,不能任意对象类型<——>任意类型,如我们常见的Long时间戳到Date类型的转换是办不到的;

(2、PropertyEditor是线程不安全的,也就是有状态的,因此每次使用时都需要创建一个,不可重用;

(3、PropertyEditor不是强类型的,setValue(Object)可以接受任意类型,因此需要我们自己判断类型是否兼容;

(4、需要自己编程实现验证,gen-wo-xue-spring/content/Spring3支持更棒的注解验证支持;

(5、在使用gen-wo-xue-spring/content/SpEL表达式语言或DataBinder时,只能进行gen-wo-xue-spring/content/String<--->Object之间的类型转换;

(6不支持细粒度的类型转换/格式化,如UserModel的registerDate需要转换/格式化类似“``2012-05-01``”的数据,而OrderModel的orderDate需要转换/格式化类似“2012-05-01 15:11:13”的数据,因为大家都为java.util.Date类型,因此不太容易进行细粒度转换/格式化。

**在gen-wo-xue-spring/content/Spring Web MVC环境中,数据类型转换、验证及格式化通常是这样使用的:**

流程:

①、类型转换:首先表单数据(全部是字符串)通过WebDataBinder进行绑定到命令对象,内部通过PropertyEditor实现;

②:数据验证:在控制器中的功能处理方法中,需要显示的调用gen-wo-xue-spring/content/Spring的Validator实现并将错误信息添加到BindingResult对象中;

③:格式化显示:在表单页面可以通过如下方式展示通过PropertyEditor格式化的数据和错误信息:

<%@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>标签,自动调用Conversiongen-wo-xue-spring/content/Service并选择相应的Converter gen-wo-xue-spring/content/SPI进行格式化展示
<spring:eval expression="dataBinderTest.phoneNumber"></spring:eval>

如上代码能工作的前提是在RequestMappingHandlerMapping配置了Conversiongen-wo-xue-spring/content/ServiceExposingInterceptor,它的作用是暴露conversiongen-wo-xue-spring/content/Service到请求中以便如<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、gen-wo-xue-spring/content/Spring3之前的PropertyEditor

PropertyEditor介绍请参考【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 gen-wo-xue-spring/content/String test(DataBinderTestModel command) {
            //输出command对象看看是否绑定正确
        gen-wo-xue-spring/content/System.out.println(command);
        model.addAttribute("dataBinderTest", command);
        return "bind/success";
    }
}

(3、gen-wo-xue-spring/content/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
//此处的参数也可以是gen-wo-xue-spring/content/ServletRequestDataBinder类型
public void initBinder(WebDataBinder binder) throws Exception {
    //注册自定义的属性编辑器
    //1、日期
    DateFormat df = new gen-wo-xue-spring/content/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、使用**WebBindingInitializer批量注册**PropertyEditor

和【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(全局共享)】。

接下来我们看一下gen-wo-xue-spring/content/Spring3提供的更强大的类型转换支持。

7.2.2、gen-wo-xue-spring/content/Spring3开始的类型转换系统

gen-wo-xue-spring/content/Spring3引入了更加通用的类型转换系统,其定义了gen-wo-xue-spring/content/SPI接口(Converter等)和相应的运行时执行类型转换的API(Conversiongen-wo-xue-spring/content/Service等),在gen-wo-xue-spring/content/Spring中它和PropertyEditor功能类似,可以替代PropertyEditor来转换外部Bean属性的值到Bean属性需要的类型。

该类型转换系统是gen-wo-xue-spring/content/Spring通用的,其定义在org.springframework.core.convert包中,不仅仅在gen-wo-xue-spring/content/Spring Web MVC场景下。目标是完全替换PropertyEditor,提供无状态、强类型且可以在任意类型之间转换的类型转换系统,可以用于任何需要的地方,如gen-wo-xue-spring/content/SpEL、数据绑定。

Converter gen-wo-xue-spring/content/SPI完成通用的类型转换逻辑,如java.util.Date<---->java.lang.Long或java.lang.gen-wo-xue-spring/content/String---->PhoneNumberModel等。

7.2.2.1、架构

1、类型转换器:提供类型转换的实现支持。

一个有如下三种接口:

(1、Converter:类型转换器,用于转换gen-wo-xue-spring/content/S类型到T类型,此接口的实现必须是线程安全的且可以被共享。

package org.springframework.core.convert.converter;
public interface Converter<gen-wo-xue-spring/content/S, T> { //① gen-wo-xue-spring/content/S是源类型 T是目标类型
    T convert(gen-wo-xue-spring/content/S source); //② 转换gen-wo-xue-spring/content/S类型的source到T目标类型的转换方法
}

示例:请参考cn.javass.chapter7.converter.support.gen-wo-xue-spring/content/StringToPhoneNumberConverter转换器,用于将gen-wo-xue-spring/content/String--->PhoneNumberModel。

此处我们可以看到Converter接口实现只能转换一种类型到另一种类型,不能进行多类型转换,如将一个数组转换成集合,如(gen-wo-xue-spring/content/String[] ----> List<gen-wo-xue-spring/content/String>、gen-wo-xue-spring/content/String[]----->List<PhoneNumberModel>等)。

(2、GenericConverter和ConditionalGenericConverter:GenericConverter接口实现能在多种类型之间进行转换,ConditionalGenericConverter是有条件的在多种类型之间进行转换。

package org.springframework.core.convert.converter;
public interface GenericConverter {
    gen-wo-xue-spring/content/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实现,如在gen-wo-xue-spring/content/String[]<---->List<gen-wo-xue-spring/content/String>、gen-wo-xue-spring/content/String[]<---->List<PhoneNumberModel>等之间进行类型转换。

对于我们大部分用户来说一般不需要自定义GenericConverter, 如果需要可以参考内置的GenericConverter来实现自己的。

(3、ConverterFactory:工厂模式的实现,用于选择将一种gen-wo-xue-spring/content/S源类型转换为R类型的子类型T的转换器的工厂接口。

package org.springframework.core.convert.converter;
public interface ConverterFactory<gen-wo-xue-spring/content/S, R> {
    <T extends R> Converter<gen-wo-xue-spring/content/S, T> getConverter(Class<T> targetType);
}

gen-wo-xue-spring/content/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、Conversiongen-wo-xue-spring/content/Service:运行时类型转换服务接口,提供运行期类型转换的支持。

package org.springframework.core.convert;
public interface Conversiongen-wo-xue-spring/content/Service {
    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:将源对象转换为目标类型的目标对象。

gen-wo-xue-spring/content/Spring提供了两个默认实现(其都实现了ConverterRegistryConversiongen-wo-xue-spring/content/Service接口):

DefaultConversiongen-wo-xue-spring/content/Service:默认的类型转换服务实现;

DefaultFormattingConversiongen-wo-xue-spring/content/Service:带数据格式化支持的类型转换服务实现,一般使用该服务实现即可。

7.2.2.2、gen-wo-xue-spring/content/Spring内建的类型转换器如下所示:

类名说明
第一组:标量转换器
gen-wo-xue-spring/content/StringToBooleanConvertergen-wo-xue-spring/content/String----->Booleantrue:true/on/yes/1; false:false/off/no/0
ObjectTogen-wo-xue-spring/content/StringConverterObject----->gen-wo-xue-spring/content/String调用togen-wo-xue-spring/content/String方法转换
gen-wo-xue-spring/content/StringToNumberConverterFactorygen-wo-xue-spring/content/String----->Number(如Integer、Long等)
NumberToNumberConverterFactoryNumber子类型(Integer、Long、Double等)<——> Number子类型(Integer、Long、Double等)
gen-wo-xue-spring/content/StringToCharacterConvertergen-wo-xue-spring/content/String----->java.lang.Character取字符串第一个字符
NumberToCharacterConverterNumber子类型(Integer、Long、Double等)——> java.lang.Character
CharacterToNumberFactoryjava.lang.Character ——>Number子类型(Integer、Long、Double等)
gen-wo-xue-spring/content/StringToEnumConverterFactorygen-wo-xue-spring/content/String----->enum类型通过Enum.valueOf将字符串转换为需要的enum类型
EnumTogen-wo-xue-spring/content/StringConverterenum类型----->gen-wo-xue-spring/content/String返回enum对象的name()值
gen-wo-xue-spring/content/StringToLocaleConvertergen-wo-xue-spring/content/String----->java.util.Local
PropertiesTogen-wo-xue-spring/content/StringConverterjava.util.Properties----->gen-wo-xue-spring/content/String默认通过Igen-wo-xue-spring/content/SO-8859-1解码
gen-wo-xue-spring/content/StringToPropertiesConvertergen-wo-xue-spring/content/String----->java.util.Properties默认使用Igen-wo-xue-spring/content/SO-8859-1编码
第二组:集合、数组相关转换器
ArrayToCollectionConverter任意gen-wo-xue-spring/content/S数组---->任意T集合(List、gen-wo-xue-spring/content/Set)
CollectionToArrayConverter任意T集合(List、gen-wo-xue-spring/content/Set)---->任意gen-wo-xue-spring/content/S数组
ArrayToArrayConverter任意gen-wo-xue-spring/content/S数组<---->任意T数组
CollectionToCollectionConverter任意T集合(List、gen-wo-xue-spring/content/Set)<---->任意T集合(List、gen-wo-xue-spring/content/Set)即集合之间的类型转换
MapToMapConverterMap<---->Map之间的转换
ArrayTogen-wo-xue-spring/content/StringConverter任意gen-wo-xue-spring/content/S数组---->gen-wo-xue-spring/content/String类型
gen-wo-xue-spring/content/StringToArrayConvertergen-wo-xue-spring/content/String----->数组默认通过“,”分割,且去除字符串的两边空格(trim)
ArrayToObjectConverter任意gen-wo-xue-spring/content/S数组---->任意Object的转换(如果目标类型和源类型兼容,直接返回源对象;否则返回gen-wo-xue-spring/content/S数组的第一个元素并进行类型转换)
ObjectToArrayConverterObject----->单元素数组
CollectionTogen-wo-xue-spring/content/StringConverter任意T集合(List、gen-wo-xue-spring/content/Set)---->gen-wo-xue-spring/content/String类型
gen-wo-xue-spring/content/StringToCollectionConvertergen-wo-xue-spring/content/String----->集合(List、gen-wo-xue-spring/content/Set)默认通过“,”分割,且去除字符串的两边空格(trim)
CollectionToObjectConverter任意T集合---->任意Object的转换(如果目标类型和源类型兼容,直接返回源对象;否则返回gen-wo-xue-spring/content/S数组的第一个元素并进行类型转换)
ObjectToCollectionConverterObject----->单元素集合
第三组:默认(fallback)转换器:之前的转换器不能转换时调用
ObjectToObjectConverterObject(gen-wo-xue-spring/content/S)----->Object(T)首先尝试valueOf进行转换、没有则尝试new 构造器(gen-wo-xue-spring/content/S)
IdToEntityConverterId(gen-wo-xue-spring/content/S)----->Entity(T)查找并调用public static T findEntityName获取目标对象,EntityName是T类型的简单类型
FallbackObjectTogen-wo-xue-spring/content/StringConverterObject----->gen-wo-xue-spring/content/StringConversiongen-wo-xue-spring/content/Service作为恢复使用,即其他转换器不能转换时调用(执行对象的togen-wo-xue-spring/content/String()方法)

gen-wo-xue-spring/content/S:代表源类型,T:代表目标类型

如上的转换器在使用转换服务实现DefaultConversiongen-wo-xue-spring/content/Service和DefaultFormattingConversiongen-wo-xue-spring/content/Service时会自动注册。

7.2.2.3、示例

(1、自定义gen-wo-xue-spring/content/String----->PhoneNumberModel的转换器

package cn.javass.chapter7.web.controller.support.converter;
//省略import
public class gen-wo-xue-spring/content/StringToPhoneNumberConverter implements Converter<gen-wo-xue-spring/content/String, PhoneNumberModel> {
    Pattern pattern = Pattern.compile("^(\\d{3,4})-(\\d{7,8})$");
    @Override
    public PhoneNumberModel convert(gen-wo-xue-spring/content/String source) {        
        if(!gen-wo-xue-spring/content/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(gen-wo-xue-spring/content/String.format("类型转换失败,需要格式[010-12345678],但格式是[%s]", source));
        }
    }
}

gen-wo-xue-spring/content/String转换为Date的类型转换器,请参考cn.javass.chapter7.web.controller.support.converter.gen-wo-xue-spring/content/StringToDateConverter。

(2、测试用例(cn.javass.chapter7.web.controller.support.converter.ConverterTest)

@Test
public void testgen-wo-xue-spring/content/StringToPhoneNumberConvert() {
    DefaultConversiongen-wo-xue-spring/content/Service conversiongen-wo-xue-spring/content/Service = new DefaultConversiongen-wo-xue-spring/content/Service();
    conversiongen-wo-xue-spring/content/Service.addConverter(new gen-wo-xue-spring/content/StringToPhoneNumberConverter());

    gen-wo-xue-spring/content/String phoneNumbergen-wo-xue-spring/content/Str = "010-12345678";
    PhoneNumberModel phoneNumber = conversiongen-wo-xue-spring/content/Service.convert(phoneNumbergen-wo-xue-spring/content/Str, PhoneNumberModel.class);

    Assert.assertEquals("010", phoneNumber.getAreaCode());
}

类似于PhoneNumberEditor将字符串“010-12345678”转换为PhoneNumberModel。

@Test
public void testOtherConvert() {
    DefaultConversiongen-wo-xue-spring/content/Service conversiongen-wo-xue-spring/content/Service = new DefaultConversiongen-wo-xue-spring/content/Service();

    //"1"--->true(字符串“1”可以转换为布尔值true)
    Assert.assertEquals(Boolean.valueOf(true), conversiongen-wo-xue-spring/content/Service.convert("1", Boolean.class));

    //"1,2,3,4"--->List(转换完毕的集合大小为4)
    Assert.assertEquals(4, conversiongen-wo-xue-spring/content/Service.convert("1,2,3,4", List.class).size());
}

其他类型转换器使用也是类似的,此处不再重复。

7.2.2.4、集成到gen-wo-xue-spring/content/Spring Web MVC环境

(1、注册Conversiongen-wo-xue-spring/content/Service实现和自定义的类型转换器

<!-- ①注册Conversiongen-wo-xue-spring/content/Service -->
<bean id="conversiongen-wo-xue-spring/content/Service" class="org.springframework.format.support.
                                                       FormattingConversiongen-wo-xue-spring/content/ServiceFactoryBean">
    <property name="converters">
       <list>
            <bean class="cn.javass.chapter7.web.controller.support.
                                   converter.gen-wo-xue-spring/content/StringToPhoneNumberConverter"/>
            <bean class="cn.javass.chapter7.web.controller.support.
                                   converter.gen-wo-xue-spring/content/StringToDateConverter">
                <constructor-arg value="yyyy-MM-dd"/>
            </bean>
        </list>
    </property>
</bean>

FormattingConversiongen-wo-xue-spring/content/ServiceFactoryBean:是FactoryBean实现,默认使用DefaultFormattingConversiongen-wo-xue-spring/content/Service转换器服务实现;

converters:注册我们自定义的类型转换器,此处注册了gen-wo-xue-spring/content/String--->PhoneNumberModel和gen-wo-xue-spring/content/String--->Date的类型转换器。

(2、通过ConfigurableWebBindingInitializer注册Conversiongen-wo-xue-spring/content/Service

<!-- ②使用ConfigurableWebBindingInitializer注册conversiongen-wo-xue-spring/content/Service -->
<bean id="webBindingInitializer" class="org.springframework.web.bind.support.
                                                                        ConfigurableWebBindingInitializer">
    <property name="conversiongen-wo-xue-spring/content/Service" ref="conversiongen-wo-xue-spring/content/Service"/>
</bean>

此处我们通过ConfigurableWebBindingInitializer绑定初始化器进行Conversiongen-wo-xue-spring/content/Service的注册;

3、注册ConfigurableWebBindingInitializer到RequestMappingHandlerAdapter

<bean class="org.springframework.web.servlet.mvc.method.annotation.
                                                            RequestMappingHandlerAdapter">
<property name="webBindingInitializer" ref="webBindingInitializer"/>
</bean>

通过如上配置,我们就完成了gen-wo-xue-spring/content/Spring3.0的类型转换系统与gen-wo-xue-spring/content/Spring Web MVC的集成。此时可以启动服务器输入之前的URL测试了。

此时可能有人会问,如果我同时使用PropertyEditor和Conversiongen-wo-xue-spring/content/Service,执行顺序是什么呢?内部首先查找PropertyEditor进行类型转换,如果没有找到相应的PropertyEditor再通过Conversiongen-wo-xue-spring/content/Service进行转换。

如上集成过程看起来比较麻烦,后边我们会介绍<mvc:annotation-driven>和@EnableWebMvc,Conversiongen-wo-xue-spring/content/Service会自动注册,后续章节再详细介绍。