1. 程式人生 > >springmvc註解式控制器的資料驗證、型別轉換及格式化

springmvc註解式控制器的資料驗證、型別轉換及格式化

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之間的型別轉換;

(6不支援細粒度的型別轉換/格式化,如UserModel的registerDate需要轉換/格式化類似“2012-05-01”的資料,而OrderModel的orderDate需要轉換/格式化類似“2012-05-0115:11:13”的資料,因為大家都為java.util.Date型別,因此不太容易進行細粒度轉換/格式化。

在Spring Web MVC環境中,資料型別轉換、驗證及格式化通常是這樣使用的:



 流程:

①、型別轉換:首先表單資料(全部是字串)通過WebDataBinder進行繫結到命令物件,內部通過PropertyEditor實現;

②:資料驗證:在控制器中的功能處理方法中,需要顯示的呼叫Spring的Validator實現並將錯誤資訊新增到BindingResult物件中;

③:格式化顯示:在表單頁面可以通過如下方式展示通過PropertyEditor格式化的資料和錯誤資訊:

Java程式碼  收藏程式碼
  1. <%@taglib prefix="spring" uri="http://www.springframework.org/tags" %>  
  2. <%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>   

首先需要通過如上taglib指令引入spring的兩個標籤庫。

Java程式碼  收藏程式碼
  1. //1、格式化單個命令/表單物件的值(好像比較麻煩,真心沒有好辦法)  
  2. <spring:bind path="dataBinderTest.phoneNumber">${status.value}</spring:bind>   
Java程式碼  收藏程式碼
  1. //2、通過form標籤,內部的表單標籤會自動呼叫命令/表單物件屬性對應的PropertyEditor進行格式化顯示  
  2. <form:form commandName="dataBinderTest">  
  3.     <form:input path="phoneNumber"/><!-- 如果出錯會顯示錯誤之前的資料而不是空 -->  
  4. </form:form>   
Java程式碼  收藏程式碼
  1. //3、顯示驗證失敗後的錯誤資訊  
  2. <form:errors></form:errors>   

如上PropertyEditor和驗證API使用起來比較麻煩,而且有許多缺點,因此Spring3提供了更強大的型別轉換(TypeConversion)支援,它可以在任意物件之間進行型別轉換,不僅僅是String<——>Object;也提供了強大的資料驗證支援;同時提供了強大的資料格式化支援。

二、從Spring3開始,我們可以使用如下架構進行型別轉換、驗證及格式化:


 流程:

①:型別轉換:內部的ConversionService會根據S源型別/T目標型別自動選擇相應的ConverterSPI進行型別轉換,而且是強型別的,能在任意型別資料之間進行轉換;

②:資料驗證:支援JSR-303驗證框架,如將@Valid放在需要驗證的目標型別上即可;

③:格式化顯示:其實就是任意目標型別---->String的轉換,完全可以使用ConverterSPI完成。

Spring為了更好的詮釋格式化/解析功能提供了Formatter SPI,支援根據Locale資訊進行格式化/解析,而且該套SPI可以支援欄位/引數級別的細粒度格式化/解析,流程如下:

①:型別解析(轉換):String---->T型別目標物件的解析,和PropertyEditor類似;

③:格式化顯示:任意目標型別---->String的轉換,和PropertyEditor類似。

Formatter SPI最大特點是能進行欄位/引數級別的細粒度解析/格式化控制,即使是ConverterSPI也是粗粒度的(到某個具體型別,而不是其中的某個欄位單獨控制),目前Formatter SPI還不是很完善,如果您有好的想法可以到Spring官網提建議。

Formatter SPI內部實現實際委託給ConverterSPI進行轉換,即約束為解析/格式化String<---->任意目標型別。

在Spring Web MVC環境中,資料型別轉換、驗證及格式化通常是這樣使用的:


①、型別轉換:首先表單資料(全部是字串)通過WebDataBinder進行繫結到命令物件,內部通過Converter SPI實現;

②:資料驗證:使用JSR-303驗證框架進行驗證;

③:格式化顯示:在表單頁面可以通過如下方式展示通過內部通過Converter SPI格式化的資料和錯誤資訊:

Java程式碼  收藏程式碼
  1. <%@taglib prefix="spring" uri="http://www.springframework.org/tags" %>  
  2. <%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>   

首先需要通過如上taglib指令引入spring的兩個標籤庫。

Java程式碼  收藏程式碼
  1. //1、格式化單個命令/表單物件的值(好像比較麻煩,真心沒有好辦法)  
  2. <spring:bind path="dataBinderTest.phoneNumber">${status.value}</spring:bind>   
Java程式碼  收藏程式碼
  1. //2、<spring:eval>標籤,自動呼叫ConversionService並選擇相應的Converter SPI進行格式化展示  
  2. <spring:eval expression="dataBinderTest.phoneNumber"></spring:eval>   

如上程式碼能工作的前提是在RequestMappingHandlerMapping配置了ConversionServiceExposingInterceptor,它的作用是暴露conversionService到請求中以便如<spring:eval>標籤使用。

Java程式碼  收藏程式碼
  1. //3、通過form標籤,內部的表單標籤會自動呼叫命令/表單物件屬性對應的PropertyEditor進行格式化顯示  
  2. <form:form commandName="dataBinderTest">  
  3.     <form:input path="phoneNumber"/><!-- 如果出錯會顯示錯誤之前的資料而不是空 -->  
  4. </form:form>   
Java程式碼  收藏程式碼
  1. //4、顯示驗證失敗後的錯誤資訊  
  2. <form:errors></form:errors>   

接下來我們就詳細學習一下這些知識吧。

7.2、資料型別轉換

7.2.1、Spring3之前的PropertyEditor

PropertyEditor介紹請參考【4.16.1、資料型別轉換】。

一、測試之前我們需要準備好測試環境:

(1、模型物件,和【4.16.1、資料型別轉換】使用的一樣,需要將DataBinderTestModel模型類及相關類拷貝過來放入cn.javass.chapter7.model包中。

(2、控制器定義:         

Java程式碼  收藏程式碼
  1. package cn.javass.chapter7.web.controller;  
  2. //省略import  
  3. @Controller  
  4. public class DataBinderTestController {  
  5.     @RequestMapping(value = "/dataBind")  
  6.     public String test(DataBinderTestModel command) {  
  7.             //輸出command物件看看是否繫結正確  
  8.         System.out.println(command);  
  9.         model.addAttribute("dataBinderTest", command);  
  10.         return "bind/success";  
  11.     }  
  12. }  

 (3、Spring配置檔案定義,請參考chapter7-servlet.xml,並註冊DataBinderTestController:

Java程式碼  收藏程式碼
  1. <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-1816:48:48&state=blocked

二、註解式控制器註冊PropertyEditor:

1、使用WebDataBinder進行控制器級別註冊PropertyEditor(控制器獨享)

Java程式碼  收藏程式碼
  1. @InitBinder  
  2. //此處的引數也可以是ServletRequestDataBinder型別  
  3. public void initBinder(WebDataBinder binder) throws Exception {  
  4.     //註冊自定義的屬性編輯器  
  5.     //1、日期  
  6.     DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
  7.     CustomDateEditor dateEditor = new CustomDateEditor(df, true);  
  8.     //表示如果命令物件有Date型別的屬性,將使用該屬性編輯器進行型別轉換  
  9.     binder.registerCustomEditor(Date.class, dateEditor);      
  10.     //自定義的電話號碼編輯器(和【4.16.1、資料型別轉換】一樣)  
  11.     binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor());  
  12. }   

和【4.16.1、資料型別轉換】一節類似,只是此處需要通過@InitBinder來註冊自定義的PropertyEditor。

2、使用WebBindingInitializer批量註冊PropertyEditor

和【4.16.1、資料型別轉換】不太一樣,因為我們的註解式控制器是POJO,沒有實現任何東西,因此無法注入WebBindingInitializer,此時我們需要把WebBindingInitializer注入到我們的RequestMappingHandlerAdapter或AnnotationMethodHandlerAdapter,這樣對於所有的註解式控制器都是共享的。

Java程式碼  收藏程式碼
  1. <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">  
  2.   <property name="webBindingInitializer">  
  3.     <bean class="cn.javass.chapter7.web.controller.support.initializer.MyWebBindingInitializer"/>  
  4.   </property>  
  5. </bean>   

此時我們註釋掉控制器級別通過@InitBinder註冊PropertyEditor的方法。

3、全域性級別註冊PropertyEditor(全域性共享)

和【4.16.1、資料型別轉換】一節一樣,此處不再重複。請參考【4.16.1、資料型別轉換】的【全域性級別註冊PropertyEditor(全域性共享)】。

接下來我們看一下Spring3提供的更強大的型別轉換支援。

7.2.2、Spring3開始的型別轉換系統

Spring3引入了更加通用的型別轉換系統,其定義了SPI介面(Converter等)和相應的執行時執行型別轉換的API(ConversionService等),在Spring中它和PropertyEditor功能類似,可以替代PropertyEditor來轉換外部Bean屬性的值到Bean屬性需要的型別。

該型別轉換系統是Spring通用的,其定義在org.springframework.core.convert包中,不僅僅在SpringWeb MVC場景下。目標是完全替換PropertyEditor,提供無狀態、強型別且可以在任意型別之間轉換的型別轉換系統,可以用於任何需要的地方,如SpEL、資料繫結。

Converter SPI完成通用的型別轉換邏輯,如java.util.Date<---->java.lang.Long或java.lang.String---->PhoneNumberModel等。

7.2.2.1、架構

1、型別轉換器:提供型別轉換的實現支援。


一個有如下三種介面:

(1、Converter:型別轉換器,用於轉換S型別到T型別,此介面的實現必須是執行緒安全的且可以被共享。

Java程式碼  收藏程式碼
  1. package org.springframework.core.convert.converter;  
  2. public interface Converter<S, T> { //① S是源型別 T是目標型別  
  3.     T convert(S source); //② 轉換S型別的source到T目標型別的轉換方法  
  4. }   

示例:請參考cn.javass.chapter7.converter.support.StringToPhoneNumberConverter轉換器,用於將String--->PhoneNumberModel。

此處我們可以看到Converter介面實現只能轉換一種型別到另一種型別,不能進行多型別轉換,如將一個數組轉換成集合,如(String[]----> List<String>、String[]----->List<PhoneNumberModel>等)。

(2、GenericConverter和ConditionalGenericConverter:GenericConverter介面實現能在多種型別之間進行轉換,ConditionalGenericConverter是有條件的在多種型別之間進行轉換。

Java程式碼  收藏程式碼
  1. package org.springframework.core.convert.converter;  
  2. public interface GenericConverter {  
  3.     Set<ConvertiblePair> getConvertibleTypes();  
  4.     Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);  
  5. }  

getConvertibleTypes:指定了可以轉換的目標型別對;

convert:在sourceType和targetType型別之間進行轉換。

Java程式碼  收藏程式碼
  1. package org.springframework.core.convert.converter;  
  2. public interface ConditionalGenericConverter extends GenericConverter {  
  3.     boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);  
  4. }   

matches:用於判斷sourceType和targetType型別之間能否進行型別轉換。

示例:如org.springframework.core.convert.support.ArrayToCollectionConverter和CollectionToArrayConverter用於在陣列和集合間進行轉換的ConditionalGenericConverter實現,如在String[]<---->List<String>、String[]<---->List<PhoneNumberModel>等之間進行型別轉換。

對於我們大部分使用者來說一般不需要自定義GenericConverter, 如果需要可以參考內建的GenericConverter來實現自己的。

(3、ConverterFactory:工廠模式的實現,用於選擇將一種S源型別轉換為R型別的子型別T的轉換器的工廠介面。

Java程式碼  收藏程式碼
  1. package org.springframework.core.convert.converter;  
  2. public interface ConverterFactory<S, R> {  
  3.     <T extends R> Converter<S, T> getConverter(Class<T> targetType);  
  4. }   

S:源型別;R目標型別的父型別;T:目標型別,且是R型別的子型別;

getConverter:得到目標型別的對應的轉換器。

示例:如org.springframework.core.convert.support.NumberToNumberConverterFactory用於在Number型別子型別之間進行轉換,如Integer--->Double,Byte---->Integer, Float--->Double等。

對於我們大部分使用者來說一般不需要自定義ConverterFactory,如果需要可以參考內建的ConverterFactory來實現自己的。

2、型別轉換器註冊器、型別轉換服務:提供型別轉換器註冊支援,執行時型別轉換API支援。


一共有如下兩種介面:

(1、ConverterRegistry:型別轉換器註冊支援,可以註冊/刪除相應的型別轉換器。

Java程式碼  收藏程式碼
  1. package org.springframework.core.convert.converter;  
  2. public interface ConverterRegistry {  
  3.     void addConverter(Converter<?, ?> converter);  
  4.     void addConverter(Class<?> sourceType, Class<?> targetType, Converter<?, ?> converter);  
  5.     void addConverter(GenericConverter converter);  
  6.     void addConverterFactory(ConverterFactory<?, ?> converterFactory);  
  7.     void removeConvertible(Class<?> sourceType, Class<?> targetType);  
  8. }   

可以註冊:Converter實現,GenericConverter實現,ConverterFactory實現。

(2、ConversionService:執行時型別轉換服務介面,提供執行期型別轉換的支援。

Java程式碼  收藏程式碼
  1. package org.springframework.core.convert;  
  2. public interface ConversionService {  
  3.     boolean canConvert(Class<?> sourceType, Class<?> targetType);  
  4.     boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);  
  5.     <T> T convert(Object source, Class<T> targetType);  
  6.     Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);  
  7. }   

convert:將源物件轉換為目標型別的目標物件。

Spring提供了兩個預設實現(其都實現了ConverterRegistryConversionService介面):

DefaultConversionService:預設的型別轉換服務實現;

DefaultFormattingConversionService:帶資料格式化支援的型別轉換服務實現,一般使用該服務實現即可。

7.2.2.2、Spring內建的型別轉換器如下所示:

類名

說明

第一組:標量轉換器

StringToBooleanConverter

String----->Boolean

true:true/on/yes/1; false:false/off/no/0

ObjectToStringConverter

Object----->String

呼叫toString方法轉換

StringToNumberConverterFactory

String----->Number(如Integer、Long等)

NumberToNumberConverterFactory

Number子型別(Integer、Long、Double等)<——> Number子型別(Integer、Long、Double等)

StringToCharacterConverter

String----->java.lang.Character

取字串第一個字元

NumberToCharacterConverter

Number子型別(Integer、Long、Double等)——> java.lang.Character

CharacterToNumberFactory

java.lang.Character ——>Number子型別(Integer、Long、Double等)

StringToEnumConverterFactory

String----->enum型別

通過Enum.valueOf將字串轉換為需要的enum型別

EnumToStringConverter

enum型別----->String

返回enum物件的name()值

StringToLocaleConverter

String----->java.util.Local

PropertiesToStringConverter

java.util.Properties----->String

預設通過ISO-8859-1解碼

StringToPropertiesConverter

String----->java.util.Properties

預設使用ISO-8859-1編碼

第二組:集合、陣列相關轉換器

ArrayToCollectionConverter

任意S陣列---->任意T集合(List、Set)

CollectionToArrayConverter

任意T集合(List、Set)---->任意S陣列

ArrayToArrayConverter

任意S陣列<---->任意T陣列

CollectionToCollectionConverter

任意T集合(List、Set)<---->任意T集合(List、Set)

即集合之間的型別轉換

MapToMapConverter

Map<---->Map之間的轉換

ArrayToStringConverter

任意S陣列---->String型別

StringToArrayConverter

String----->陣列

預設通過“,”分割,且去除字串的兩邊空格(trim)

ArrayToObjectConverter

任意S陣列---->任意Object的轉換

(如果目標型別和源型別相容,直接返回源物件;否則返回S陣列的第一個元素並進行型別轉換)

ObjectToArrayConverter

Object----->單元素陣列

CollectionToStringConverter

任意T集合(List、Set)---->String型別

StringToCollectionConverter

String----->集合(List、Set)

預設通過“,”分割,且去除字串的兩邊空格(trim)

CollectionToObjectConverter

任意T集合---->任意Object的轉換

(如果目標型別和源型別相容,直接返回源物件;否則返回S陣列的第一個元素並進行型別轉換)

ObjectToCollectionConverter

Object----->單元素集合

第三組:預設(fallback)轉換器:之前的轉換器不能轉換時呼叫

ObjectToObjectConverter

Object(S)----->Object(T)

首先嚐試valueOf進行轉換、沒有則嘗試new 構造器(S)

IdToEntityConverter

Id(S)----->Entity(T)

查詢並呼叫public static T find[EntityName](S)獲取目標物件,EntityName是T型別的簡單型別

FallbackObjectToStringConverter

Object----->String

ConversionService作為恢復使用,即其他轉換器不能轉換時呼叫(執行物件的toString()方法)

S:代表源型別,T:代表目標型別

如上的轉換器在使用轉換服務實現DefaultConversionService和DefaultFormattingConversionService時會自動註冊。

7.2.2.3、示例

(1、自定義String----->PhoneNumberModel的轉換器

Java程式碼  收藏程式碼
  1. package cn.javass.chapter7.web.controller.support.converter;  
  2. //省略import  
  3. public class StringToPhoneNumberConverter implements Converter<String, PhoneNumberModel> {  
  4.     Pattern pattern = Pattern.compile("^(\\d{3,4})-(\\d{7,8})$");  
  5.     @Override  
  6.     public PhoneNumberModel convert(String source) {          
  7.         if(!StringUtils.hasLength(source)) {  
  8.             //①如果source為空 返回null  
  9.             return null;  
  10.         }  
  11.         Matcher matcher = pattern.matcher(source);  
  12.         if(matcher.matches()) {  
  13.             //②如果匹配 進行轉換  
  14.             PhoneNumberModel phoneNumber = new PhoneNumberModel();  
  15.             phoneNumber.setAreaCode(matcher.group(1));  
  16.             phoneNumber.setPhoneNumber(matcher.group(2));  
  17.             return phoneNumber;  
  18.         } else {  
  19.             //③如果不匹配 轉換失敗  
  20.             throw new IllegalArgumentException(String.format("型別轉換失敗,需要格式[010-12345678],但格式是[%s]", source));  
  21.         }  
  22.     }  
  23. }  

String轉換為Date的型別轉換器,請參考cn.javass.chapter7.web.controller.support.converter.StringToDateConverter。

(2、測試用例(cn.javass.chapter7.web.controller.support.converter.ConverterTest)

Java程式碼  收藏程式碼
  1. @Test  
  2. public void testStringToPhoneNumberConvert() {  
  3.     DefaultConversionService conversionService = new DefaultConversionService();  
  4.     conversionService.addConverter(new StringToPhoneNumberConverter());  
  5.     String phoneNumberStr = "010-12345678";  
  6.     PhoneNumberModel phoneNumber = conversionService.convert(phoneNumberStr, PhoneNumberModel.class);  
  7.     Assert.assertEquals("010", phoneNumber.getAreaCode());  
  8. }   

類似於PhoneNumberEditor將字串“010-12345678”轉換為PhoneNumberModel。  

Java程式碼  收藏程式碼
  1. @Test  
  2. public void testOtherConvert() {  
  3.     DefaultConversionService conversionService = new DefaultConversionService();  
  4.     //"1"--->true(字串“1”可以轉換為布林值true)  
  5.     Assert.assertEquals(Boolean.valueOf(true), conversionService.convert("1", Boolean.class));  
  6.     //"1,2,3,4"--->List(轉換完畢的集合大小為4)  
  7.     Assert.assertEquals(4, conversionService.convert("1,2,3,4", List.class).size());  
  8. }   

其他型別轉換器使用也是類似的,此處不再重複。

7.2.2.4、整合到Spring Web MVC環境

(1、註冊ConversionService實現和自定義的型別轉換器

Java程式碼  收藏程式碼
  1. <!-- ①註冊ConversionService -->  
  2. <bean id="conversionService" class="org.springframework.format.support.  
  3.                                              FormattingConversionServiceFactoryBean">  
  4.     <property name="converters">  
  5.        <list>  
  6.             <bean class="cn.javass.chapter7.web.controller.support.  
  7.                              converter.StringToPhoneNumberConverter"/>  
  8.             <bean class="cn.javass.chapter7.web.controller.support.  
  9.                              converter.StringToDateConverter">  
  10.                 <constructor-arg value="yyyy-MM-dd"/>  
  11.             </bean>  
  12.         </list>  
  13.     </property>  
  14. </bean>   

FormattingConversionServiceFactoryBean:是FactoryBean實現,預設使用DefaultFormattingConversionService轉換器服務實現;

converters:註冊我們自定義的型別轉換器,此處註冊了String--->PhoneNumberModel和String--->Date的型別轉換器。

(2、通過ConfigurableWebBindingInitializer註冊ConversionService

Java程式碼  收藏程式碼
  1. <!-- ②使用ConfigurableWebBindingInitializer註冊conversionService -->  
  2. <bean id="webBindingInitializer" class="org.springframework.web.bind.support.  
  3.                                                                         ConfigurableWebBindingInitializer">  
  4.     <property name="conversionService" ref="conversionService"/>  
  5. </bean>   

此處我們通過ConfigurableWebBindingInitializer繫結初始化器進行ConversionService的註冊;

3、註冊ConfigurableWebBindingInitializer到RequestMappingHandlerAdapter

Java程式碼  收藏程式碼
  1. <bean class="org.springframework.web.servlet.mvc.method.annotation.  
  2.                                                             RequestMappingHandlerAdapter">  
  3. <property name="webBindingInitializer" ref="webBindingInitializer"/>  
  4. </bean>   

通過如上配置,我們就完成了Spring3.0的型別轉換系統與Spring Web MVC的整合。此時可以啟動伺服器輸入之前的URL測試了。

此時可能有人會問,如果我同時使用PropertyEditor和ConversionService,執行順序是什麼呢?內部首先查詢PropertyEditor進行型別轉換,如果沒有找到相應的PropertyEditor再通過ConversionService進行轉換。

如上整合過程看起來比較麻煩,後邊我們會介紹<mvc:annotation-driven>和@EnableWebMvc,ConversionService會自動註冊,後續章節再詳細介紹。