(英文原版. Spring 3.2.14 Reference - 7 Validation, Data Binding, and Type Conversion)
(若有翻譯不許確的地方,請私信我,共同提升;轉載請註明出處,謝謝)html
對於把校驗做爲業務邏輯有正面意見也有負面意見,而Spring提供的驗證設計(和數據綁定)不考慮這些。特別是,驗證不該該依賴於Web層,它應該很容易被調用,能夠插入任何可用的驗證器。基於上述的考慮,Spring提供了一個Validator接口,這是一個基礎組件,能夠用於應用程序的每一層。前端
數據綁定機制容許將用戶輸入動態綁定到應用程序的域模型對象(或用來處理用戶輸入的任何對象)。Spring提供了DataBinder類實現數據綁定機制。java
Validator和DataBinder組成了validation包,主要但不限於在MVC framework中應用。BeanWrapper是Spring Framework中的一個基礎組件,在不少地方都用到,可是開發者通常不會直接使用。由於這是一個參考文檔,因此本章對BeanWrapper進行一些必要的解釋。若是開發者想使用它,最多是在數據綁定場景下使用。web
Spring的DataBinder和底層的BeanWrapper都使用PropertyEditor來解析和格式化屬性值。PropertyEditor是JavaBean所特有的,在本章也會進行必要的解釋。Spring 3引入了「core.convert」包,既提供了一個通用類型轉換工具,也提供了高級「format」包來格式化UI字段值。Spring中的這些新的程序包用起來可能比PropertyEditor簡單,在本章也會討論。spring
Spring 引入了Validator接口,能夠用來驗證對象。Validator接口與Errors對象一塊兒使用,驗證時Validator能夠報告驗證錯誤到Errors對象。
讓咱們考慮一個簡單數據對象。express
public class Person { private String name; private int age; // the usual getters and setters... }
經過實現org.springframework.validation.Validator接口的兩個方法爲Person類提供驗證:編程
實現Validator比較簡單,特別是藉助Spring Framework提供的ValidationUtils幫助類時。api
public class PersonValidator implements Validator { /** * This Validator validates *just* Person instances */ public boolean supports(Class clazz) { return Person.class.equals(clazz); } public void validate(Object obj, Errors e) { ValidationUtils.rejectIfEmpty(e, "name", "name.empty"); Person p = (Person) obj; if (p.getAge() < 0) { e.rejectValue("age", "negativevalue"); } else if (p.getAge() > 110) { e.rejectValue("age", "too.darn.old"); } } }
正如你所看到的,若是name屬性爲null或者空字符串,ValidationUtils類的static rejectIfEmpty(..)方法會拒絕 name屬性,並在Erros對象中返回驗證失敗。查閱ValidationUtils類的Javadoc文檔,查看更多方法和使用。數組
開發者有可能會在一個Validator類中驗證複雜對象(rich object)的每個內嵌對象,最好將每個內嵌對象的驗證邏輯封裝在各自的類實現中。咱們來看一個簡單的「複雜對象」,Customer對象是由兩個字符串(first name和second name)以及一個Address對象組成。Address對象與Custom對象是相互獨立的,須要單獨實現AddressValidator。若是開發者須要在CustomerValidator中重用AddressValidator中的驗證邏輯,但不是經過複製粘貼代碼的方式,可使用依賴注入(DI),或者在CustomerValidator中實例化一個AddressValidator,以下所示:spring-mvc
public class CustomerValidator implements Validator { private final Validator addressValidator; public CustomerValidator(Validator addressValidator) { if (addressValidator == null) { throw new IllegalArgumentException( "The supplied [Validator] is required and must not be null."); } if (!addressValidator.supports(Address.class)) { throw new IllegalArgumentException( "The supplied [Validator] must support the validation of [Address] instances."); } this.addressValidator = addressValidator; } /** * This Validator validates Customer instances, and any subclasses of Customer too * */ public boolean supports(Class clazz) { return Customer.class.isAssignableFrom(clazz); } public void validate(Object target, Errors errors) { ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required"); ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required"); Customer customer = (Customer) target; try { errors.pushNestedPath("address"); ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors); } finally { errors.popNestedPath(); } } } }
驗證錯誤會報告給Errors對象,並傳遞給validator對象。若是是Spring Web MVC中使用,可使用
咱們討論了數據綁定和驗證。對應驗證錯誤的消息輸出是咱們須要討論的最後一件事。在以前的例子中,咱們拒絕了name和age字段。若是咱們使用MessageSource輸出錯誤消息,咱們會用到驗證字段(name和age字段)錯誤時返回的錯誤碼(error code)。當你調用(直接或者間接,使用ValidationUtils類)rejectValue或者Erros接口的其它reject方法,底層實現不會只註冊傳遞過來的代碼(code),還有一些額外的錯誤代碼(error code)。註冊哪些錯誤碼是由使用的MessageCodesResolver決定的。默認使用DefaultMessageCodesResolver,它不只註冊傳遞過來的代碼的消息,還包括傳遞給reject方法的字段名稱。所以當你使用rejectValue("age","to.darn.old")拒絕字段時,不光是too.darn.old代碼,Spring還會註冊too.darn.old.age和too.darn.old.age.int(第一個包含字段名稱,第二個包含字段類型);這樣作是爲了方便開發者定位該類錯誤消息。
關於MessageCodesResolver更多信息和默認策略,能夠分別查看MessageCodesResolver和DefaultMessageCodesResolver的在線Javadoc。
org.springframework.beans包由Sun提供,遵循JavaBean標準。JavaBean是一種簡單的類,包含一個默認的無參構造函數,且遵循必定的命名規範。例如,若是有一個屬性名爲bingoMadness,必須包含一個賦值方法setBingoMadness(...),和一個獲取屬性值的方法getBingoMadness()。若是但願獲取更多的關於JavaBean的規範,請參考Sun網站(java.sun.com/products/javabeans)。
org.springframework.beans包中一個很是重要的類是BeanWrapper接口和它對應的實現類(BeanWrapperImpl)。正如在Javadoc中提到,BeanWrapper提供了一些功能,包括設置和獲取屬性值(單個屬性或者組合屬性),獲取屬性描述,查詢屬性是否可讀或可寫。同時,BeanWrapper提供嵌套屬性的支持,能夠對無限制深度的子屬性進行設置。這樣,不須要在目標類添加支持代碼,BeanWrapper便可支持在目標類上添加標準JavaBean類JavaBeanPropertyChangeListeners和VetoableChangeListeners。不止如此,BeanWrapper還爲索引屬性提供設置支持。BeanWrapper一般不會在應用中直接使用,而是經過DataBinder和BeanFactory來使用。
從名稱上能夠看出BeanWrapper的工做機制,它封裝了Bean,使得能夠對Bean進行操做,例如設置和訪問屬性。
使用setPropertyValues(s)和getPropertyValue(s)方法設置和獲取屬性值,這些方法能夠負載幾個變量。這些在以後的Spring Javadoc文檔中有更詳細的描述。重要的是,表示一個對象的屬性有一些規範。表7.1爲屬性表示樣例。
表7.1 屬性表示樣例
屬性 | 說明 |
---|---|
name | 表示屬性name, 對應的方法爲getName() 或 isName() ,以及 setName(..) |
account.name | 表示屬性account的嵌套屬性name, 對應的方法爲getAccount().setName() 或者getAccount().getName() |
account[2] | 表示索引屬性account的第3個元素。索引屬性可使array、list或者天然排序的collection。 |
account[COMPANYNAME] | 表示Map類型的屬性account中key爲COMPANYNAME 的Map條目的value。 |
使用BeanWrapper設置和獲取屬性值的樣例以下:
考慮如下兩個類:
public class Company { private String name; private Employee managingDirector; public String getName() { returnthis.name; } public void setName(String name) { this.name = name; } public Employee getManagingDirector() { returnthis.managingDirector; } public void setManagingDirector(Employee managingDirector) { this.managingDirector = managingDirector; } } public class Employee { private String name; private float salary; public String getName() { return this.name; } public void setName(String name) { this.name = name; } public float getSalary() { return salary; } public void setSalary(float salary) { this.salary = salary; } }
下面的代碼片斷顯示瞭如何獲取和處理companies和employees對象的一些屬性。
BeanWrapper company = BeanWrapperImpl(new Company()); // 設置company的name屬性.. company.setPropertyValue("name", "Some Company Inc."); // ... 也能夠這樣設置: PropertyValue value = new PropertyValue("name", "Some Company Inc."); company.setPropertyValue(value); // 建立一個employee對象,賦值給managingDirector: BeanWrapper jim = BeanWrapperImpl(new Employee()); jim.setPropertyValue("name", "Jim Stravinsky"); company.setPropertyValue("managingDirector", jim.getWrappedInstance()); // 獲取company對象的managingDirector屬性的salary屬性(嵌套屬性) Float salary = (Float) company.getPropertyValue("managingDirector.salary");
Spring使用PropertyEditor來處理Object和String之間的轉換。有時候使用PropertyEditor比使用對象自己能夠更方便的表示屬性。例如,能夠用易讀的方式(如「2007-14-09」)表示Date,也能夠將其轉換回原始Date對象(更進一步:將任意易讀的時間格式轉換爲Date對象),而這種效果經過註冊java.beans.PropertyEditor類型的自定義editor就能夠實現。在BeanWrapper或IoC容器上註冊自定義editor,能夠轉換對象的屬性爲指定的類型。若是須要獲取PropertyEditor更多的信息,能夠閱讀由Sun提供的java.beans包的Javadoc文檔。
Spring中使用屬性編輯的兩個例子:
表7.2. 內置的PropertyEditor清單
類 | 說明 |
---|---|
ByteArrayPropertyEditor | 將字符串轉換爲字節數組(byte[])。BeanWrapper中已經默認註冊。 |
ClassEditor | 將字符串類名轉換爲java.lang.Class。當找不到對應的類時,拋出ellegalArgumentException異常。BeanWrapper中已經默認註冊。 |
CustomBooleanEditor | Boolean類型的自定義屬性編輯器, BeanWrapper中已經默認註冊。能夠註冊自定義的實例覆蓋原有實例。 |
CustomCollectionEditor | Collection類型的自定義屬性編輯器,將Collection
|
CustomDateEditor | java.util.Date 類型的自定義屬性編輯器,支持自定義DateFormat,BeanWrapper中沒有默認註冊,須要開發者自行註冊。 |
CustomNumberEditor | Integer, Long, Float, Double等Number類型的自定義屬性編輯器BeanWrapper中已經默認註冊。能夠註冊自定義的實例覆蓋原有實例。 |
FileEditor | java.io.File 類型的屬性編輯器,解析字符串爲File對象。BeanWrapper中已經默認註冊。 |
InputStreamEditor | InputStream類型的單向屬性編輯器,輸入字符串,輸出InputStream對象(經過ResourceEditor和Resource)。注意默認實現中Spring沒有關閉InputStream,須要由開發者自行調用關閉。BeanWrapper中已經默認註冊。 |
LocaleEditor | java.utils.Locale類型的屬性編輯器。解析字符串爲Locale對象,反之亦然。String字符串格式爲[language][country][variant],與Locale的toString()方法同樣。BeanWrapper中已經默認註冊。 |
PatternEditor | JDK 1.5 Pattern類型的屬性編輯器。解析字符串爲Pattern對象,反之亦然。 |
PropertiesEditor | java.lang.Properties 類型的屬性編輯器。解析字符串爲Properties對象。String格式遵循java.lang.Properties的Javadoc定義。BeanWrapper中已經默認註冊。 |
StringTrimmerEditor | 用於trim 字符串。開發者能夠選擇是否將空字符串轉換爲null。BeanWrapper中沒有默認註冊,須要開發者自行註冊。 |
URLEditor | URL類型的屬性編輯器。解析字符串爲URL對象。BeanWrapper中已經默認註冊。 |
Spring使用java.beans.PropertyEditorManager來設置所須要的PropertyEditor實現類的查找路徑。查找路徑中包括sun.bean.editors,包含了Font,Color類型以及大多數的基本類型的PropertyEditor接口實現。同時注意,若是PropertyEditor類文件和屬性的類文件在同一個包下面,且PropertyEditor實現類類名爲屬性類名加Editor後綴,那麼標準JavaBeans框架會自動發現PropertyEditor類(不須要顯式註冊)。例如,下面這個包結構和類,FooEditor會被默認認爲是Foo類型的PropertyEditor屬性編輯器。
com chank pop Foo FooEditor // the PropertyEditor for the Foo class
注意也可使用標準BeanInfo JavaBeans機制。下面是使用BeanInfo機制的例子,顯式註冊了一個或者多個PropertyEditor實例處理對應類的屬性。
com chank pop Foo FooBeanInfo // the BeanInfo for the Foo class
下面是FooBeanInfo類的Java 源碼參考。這裏關聯了一個處理age屬性的CustomNumberEditor。
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()); } } }
當爲Bean的屬性賦值爲字符串時,Spring IoC容器最終會使用標準JavaBeans PropertyEditor將其轉換爲複雜類型。Spring預先註冊了一些內置的PropertyEditor(例如,將用字符串表示的類名轉換爲實際的Class對象)。另外,Java的標準JavaBeans PropertyEditor查找機制容許對PropertyEditor進行簡單適當的命名,而後與它支持的類放在同一個包下,PropertyEditor將被自動發現。
若是須要註冊其它自定義的PropertyEditor,有如下幾種方法。最經常使用的手動方法(不方便也不推薦),若是有一個BeanFactory實現類,可使用ConfigurableBeanFactory接口的registerCustomEditor()方法,。另外一個比較簡便的方法,是使用CustomEditorConfigurer這個特殊的bean工廠後置處理器。雖然bean工廠後置處理器能夠與BeanFactory實現類一塊兒使用,可是CustomEditorConfigurer存在一個嵌套屬性設置,因此強烈建議在ApplicationContext中使用,能夠用相似的方式部署給其它Bean,會被自動檢測和應用。
注意全部的bean工廠和應用程序上下文自動使用一些內置屬性編輯器,經過使用BeanWrapper處理屬性值的類型轉換。上一章節羅列了BeanWrapper註冊的標準的屬性編輯器。另外,ApplicationConext也覆蓋或者增長了額外的編輯器來處理資源查找(resource lookup)問題,以適合特定的應用上下文類型。
PropertyEditor實例用來轉換字符串表示的屬性值爲實際的屬性類型。能夠經過CustomEditorConfigurer向ApplicationContext方便的添加額外的PropertyEditor實例。
考慮一個用戶類ExoticType,以及DependsOnExoticType類,後者具備一個ExoticType的屬性須要設置。
package example; public class ExoticType { private String name; public ExoticType(String name) { this.name = name; } } public class DependsOnExoticType { private ExoticType type; public void setType(ExoticType type) { this.type = type; } }
當設置完成後,咱們但願可以將字符串賦值給該屬性,由PropertyEditor完成字符串到ExoticType實例的轉換。
<bean id="sample" class="example.DependsOnExoticType"> <property name="type" value="aNameForExoticType"/> </bean>
PropertyEditor的實現可能相似這樣:
// 轉換字符串爲ExoticType; public class ExoticTypeEditor extends PropertyEditorSupport { public void setAsText(String text) { setValue(new ExoticType(text.toUpperCase())); } }
最後,咱們使用CustomEditorConfigurer在ApplicationContext中註冊新的PropertyEditor,以即可以在須要的時候使用。
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer"> <property name="customEditors"> <map> <entry key="example.ExoticType" value="example.ExoticTypeEditor"/> </map> </property> </bean>
另外一個在Spring容器中註冊屬性編輯器(property editor)的機制是建立和使用PropertyEditorRegistrar接口。當須要在不一樣場景下重複使用一組屬性編輯器時比較有效,只須要寫一個registrar,重用便可。
PropertyEditorRegistrars與PropertyEditorRegistry接口聯合使用,這個接口由Spring BeanWrapper(和DataBinder)實現。
PropertyEditorRegistrars在與CustomEditorConfigurer聯合使用時特別方便(在這裏有介紹),CustomEditorConfigurer接口包含一個設置PropertyEditorRegistrars的方法setPropertyEditorRegistrars(..):添加的PropertyEditorRegistrars可以很容易的共享給DataBinder和Spring MVC Controller。此外,再也不須要同步自定義編輯器:每一次嘗試建立bean時,PropertyEditorRegistrar會建立一個新的PropertyEditor實例。
最好用一個例子來講明PropertyEditorRegistrars。首先,你須要建立本身的PropertyEditorRegistrar實現:
package com.foo.editors.spring; public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar { public void registerCustomEditors(PropertyEditorRegistry registry) { // it is expected that new PropertyEditor instances are created registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor()); // you could register as many custom property editors as are required here... } }
也能夠將org.springframework.beans.support.ResourceEditorRegistrar做爲一個實現PropertyEditorRegistrar的例子。注意它的registerCustomEditors(...)方法是如何建立每個屬性編輯器的。
下一步咱們配置CustomEditorConfigurer,注入CustomPropertyEditorRegistrars實例。
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer"> <property name="propertyEditorRegistrars"> <list> <ref bean="customPropertyEditorRegistrar"/> </list> </property> </bean> <bean id="customPropertyEditorRegistrar" class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>
最後,稍微離開一點本章節的焦點,當你使用Spring MVC web框架時,與數據綁定控制器(例如SimpleFormController)聯合使用會很是方便。看下面在initBinder(..)方法中使用 PropertyEditorRegistrar的例子。
public final class RegisterUserController extends SimpleFormController { private final PropertyEditorRegistrar customPropertyEditorRegistrar; public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) { this.customPropertyEditorRegistrar = propertyEditorRegistrar; } protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception { this.customPropertyEditorRegistrar.registerCustomEditors(binder); } // other methods to do with registering a User }
PropertyEditor註冊將會比較簡潔(initBinder()方法中只有一行),而且容許經常使用的PropertyEditor註冊代碼放在類中封裝,而後共享給須要的控制器(Controller)。
Spring 3 引入了core.convert包,提供通用的類型轉換系統。這個系統定義了一個SPI(Service Provide API)來實現類型轉換邏輯,相似在運行時執行類型轉換的API。在Spring容器中,該系統能夠做爲PropertyEditor的替代品來轉換外部bean屬性的字符串值爲須要的屬性類型。這個公共的API能夠用在應用程序任何須要類型轉換的地方。
用來實現類型轉換邏輯的SPI定義很簡單可是頗有效:
package org.springframework.core.convert.converter public interface Converter<S,T>{ public <T> convert(S source); }
建立Converter,實現上述接口便可。S爲轉換的源類型,T爲轉換的目標類型。每次調用convert(S),要保證S不能爲null。若是轉換失敗,Converter可能拋出異常。 拋出illegalArgumentException異常,表示源類型無效。請保證Converter的實現類是線程安全的。
爲方便起見,core.convert.converter包提供了一些Converter的實現類,包括String轉Number和其它常見類型。咱們來看下面的StringToInteger的樣例:
package org.springframework.core.convert.support; final class StringToInteger implements Converter<String, Integer> { public Integer convert(String source) { return Integer.valueOf(source); } }
當須要爲整個類層次結構集中類型轉換時,例如,當將String轉換爲java.lang.Enum對象時,實現ConverterFactory:
package org.springframework.core.convert.converter public interface ConverterFactory<S,R>{ <T extends R> Converter<S,T> getConverter(Class<T> targetType); }
S爲源類型,R爲一系列目標類型的基類。實現getConverter(Class
下面是StringToEnum ConvertFactory的樣例:
package org.springframework.core.convert.support; final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> { public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) { return new StringToEnumConverter(targetType); } private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> { private Class<T> enumType; public StringToEnumConverter(Class<T> enumType) { this.enumType = enumType; } public T convert(String source) { return (T) Enum.valueOf(this.enumType, source.trim()); } } }
當須要實現一個比較複雜的Converter,能夠考慮GenericConverter接口。更靈活,可是不是強類型,GenericConverter支持多種源類型和目標類型之間的轉換。另外,當實現轉換邏輯時,可使用GenericConverter的源和目標字段上下文。這些上下文容許註解驅動的類型轉換,或者在字段上聲明的通用信息。
實現GenericConverter接口,須要實現getConvertibleTypes()方法,返回支持的源和目的類型集合。須要實現convert(Object, TypeDescriptor, TypeDescriptor)方法,在方法中實現轉換邏輯。其中,3個參數依次表示待轉換的源、源類型,目標類型,返回值爲轉換後的值。使用GenericConverter接口的一個比較好的例子是在Java Array和Collection之間轉換。ArrayToCollection內省(introspect)聲明Collection類型的字段來解析Collection的元素類型,實如今返回Collection對象以前Array中的每個元素都已經轉換爲Collection中元素的類型。
注意:GenericConverter是一個很複雜的SPI接口,只有當你須要它的時候再使用。處理基本類型轉換時,使用Favor Converter或者ConverterFactory便可。
有時候,你可能但願在某些條件下才執行類型轉換。例如,你可能在只有當註解出如今目標字段上的時候才執行類型轉換。或者,你可能在只有目標類包含一個靜態方法ValueOf()的時候才能執行 類型轉換。
public interface ConditionalGenericConverter extends GenericConverter { boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType); }
使用ConditionGenericConverter接口的一個比較好的例子是EntityConverter,用於轉換持久化實體ID和實體引用。只有當實體類中聲明瞭一個靜態finder方法,如findAccount (Long),EntityConverter matches(TypeDescriptor,TypeDescriptor)方法纔會返回true。在實現matches(TypeDescriptor,TypeDescriptor )時,須要檢查finder方法是否存在。
ConversionService定義了一個統一的API,用來在運行時執行類型轉換邏輯。一般,Converter在下面facade接口後面執行。
package org.springframework.core.convert; public interface ConversionService { boolean canConvert(Class<?> sourceType, Class<?> targetType); <T> T convert(Object source, Class<T> targetType); boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType); Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); }
大多數ConversionService實現類實現了ConverterRegistry,它提供SPI來註冊converter。在內部,ConversionService實現類代理已註冊的轉換器(converter)處理類型轉換邏輯。
core.convert.support包提供了強壯的ConversionService實現。GenericConvertionService適合大多數環境下的通用實現。ConversionServiceFactory是ConversionService的工廠類,用來建立ConversionService配置。
ConversionService是一個無狀態的對象,設計爲應用程序啓動時被加載,而後在多個線程間共享。在Spring應用程序中,你能夠爲每一個Spring容器(或者ApplicationContext)配置一個ConversionService。該ConversionService會被Spring加載,而後在須要類型轉換的時候使用。你能夠注入ConversionService到任何一個bean直接調用它。
注意,若是ConversionService沒有在Spring註冊,那麼原始的基於PropertyEditor的系統會被使用。
爲了在Spring中注入默認的ConversionService,在XML文件中增長下面的bean,id爲conversionService。
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"/>
默認的ConversionService能夠轉換String,Numbers,enums,Collections,maps和其它經常使用的類型。爲了支持或者用自定義的轉換器(converter)覆蓋默認的轉換器,能夠設置converters屬性,屬性值能夠被Converter,ConverterFactory或者GenericConverter接口實現。
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"> <property name="converters"> <list> <bean class="example.MyCustomConverter"/> </list> </property> </bean>
Spring MVC中也常用ConversionService。7.6.5查看該部分詳細內容。
在一些狀況下,可能但願在類型轉換的時候進行格式轉換。7.6.3查看該部分詳細內容。
經過編程使用ConversionService,在你想要的任何bean的屬性中注入ConversionService。
@Service public class MyService { @Autowired public MyService(ConversionService conversionService) { this.conversionService = conversionService; } public void doIt() { this.conversionService.convert(...) } }
在以前章節裏討論過,core.convert包是一個通用的類型轉換系統。它提供了一個統一的轉換服務API(ConversionService API),相似強類型Converter SPI,實現了從一個類型到另外一個類型的轉換邏輯。Spring容器(Spring Container)使用這個系統綁定Bean屬性值。另外,Spring Expression Language(SpEL)和DataBinder使用這個系統綁定字段值。例如,當調用expression.setValue(Object bean, Object value) SpEL表達式,須要嘗試將Short類型強制轉換爲Long類型時,其強制轉換工做就是由core.convert完成的。
如今考慮典型的客戶端環境下的類型轉換需求,如web應用或者桌面應用。在這種環境下,經常須要將String類型進行轉換來支持客戶端的POST請求,以及轉換爲String類型來支持前端視圖渲染。另外,經常須要對String類型的值進行本地化。core.convert Converter SPI並不直接響應這樣的格式化。爲了直接響應,Spring 3爲客戶端環境引入了一個方便的Formatter SPI,給PropertyEditor提供了一個簡單好用的替代品。
通常而言,當須要實現通用類型轉換邏輯時,使用Converter SPI,例如須要在java.util.Date和java.lang.Long之間轉換時。當在客戶端環境時,使用Formatter SPI。例如Web應用,或者須要解析和打印本地化的字段值。ConversionService爲這兩種SPI提供一個統一的轉換API接口。
Formatter SPI實現字段格式化很簡單,且爲強類型。
package org.springframework.format; public interface Formatter<T> extends Printer<T>, Parser<T> { }
Formatter SPI繼承了Printer
public interface Printer<T> { String print(T fieldValue, Locale locale); } import java.text.ParseException; public interface Parser<T> { T parse(String clientValue, Locale locale) throws ParseException; }
建立自定義的Formatter時,實現Formatter接口便可。實例化T爲被格式化的對象類型,例如java.util.Date。實現print()方法打印T實例,用於客戶端本地顯示;實現parse()方法解析T實例,用於接收從客戶端傳遞過來的格式化後的表達式。當解析失敗後,Formatter實現類應該拋出ParseException或者IllegalArgumentException異常。注意,須保證Formatter實現類線程安全。
爲方便起見,在format子包中實現了部分Formatter。number包提供了NumberFormatter,CurrencyFormatter和PercentFormatter,使用java.text.NumberFormat來格式化java.lang.Number對象。datetime包提供了DateFormatter,使用java.text.DateFormat來格式化java.util.Date對象。基於Joda Time庫,datetime.joda包提供了更爲複雜的datetime格式化支持。
以DateFormatter爲例,來看Formatter接口的實現:
package org.springframework.format.datetime; public final class DateFormatter implements Formatter<Date> { private String pattern; public DateFormatter(String pattern) { this.pattern = pattern; } public String print(Date date, Locale locale) { if (date == null) { return ""; } return getDateFormat(locale).format(date); } public Date parse(String formatted, Locale locale) throws ParseException { if (formatted.length() == 0) { return null; } return getDateFormat(locale).parse(formatted); } protected DateFormat getDateFormat(Locale locale) { DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale); dateFormat.setLenient(false); return dateFormat; } }
正如您將看到,字段格式化能夠經過字段類型或者註解(annotation)來配置。爲了綁定註解到一個formatter,須要實現AnnotationFormatterFactory工廠接口。
package org.springframework.format; public interface AnnotationFormatterFactory<A extends Annotation> { Set<Class<?>> getFieldTypes(); Printer<?> getPrinter(A annotation, Class<?> fieldType); Parser<?> getParser(A annotation, Class<?> fieldType); }
實例化泛型A爲格式化邏輯相關聯的字段註解類型,例如
org.springframework.format.annotation.DateTimeFormat。
實現getFieldTypes(),返回註解能夠應用到的字段類型;實現getPrinter(),返回可以爲註解字段打印字段值的Printer;實現getParser(),返回可以爲註解字段解析客戶端上傳數據(clientValue)的Parser。
public final class NumberFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<NumberFormat> { public Set<Class<?>> getFieldTypes() { return new HashSet<Class<?>>(asList(new Class<?>[] { Short.class, Integer.class, Long.class, Float.class, Double.class, BigDecimal.class, BigInteger.class })); } public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) { return configureFormatterFrom(annotation, fieldType); } public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) { return configureFormatterFrom(annotation, fieldType); } private Formatter<Number> configureFormatterFrom(NumberFormat annotation, Class<?> fieldType) { if (!annotation.pattern().isEmpty()) { return new NumberFormatter(annotation.pattern()); } else { Style style = annotation.style(); if (style == Style.PERCENT) { return new PercentFormatter(); } else if (style == Style.CURRENCY) { return new CurrencyFormatter(); } else { return new NumberFormatter(); } } } }
爲了使用格式化,用@NumberFormat註解相應的字段。
public class MyModel { @NumberFormat(style=Style.CURRENCY) private BigDecimal decimal; }
格式化註解API(Format Annotation API)位於org.springframework.format.annotation包中。使用@NumberFormat來格式化java.lang.Number字段;使用@DateTimeFormat來格式化java.util.Date,java.util.Calendar,java.util.Long或者Joda Time字段。
下面的例子使用@DateTimeFormat來格式化java.util.Date爲ISO Date格式(yyyy-MM-dd):
public class MyModel { @DateTimeFormat(iso=ISO.DATE) private Date date; }
FormatterRegistry SPI用來註冊formatter和converter。
FormattingConversionService是FormatterRegistry的一個實現類,適用於絕大多數環境,經過代碼編程或者聲明FormattingConversionServiceFactoryBean爲Spring Bean來引入。這個類同時實現了ConversionService接口,所以配置後能夠被Spring的DataBinder和SpEL直接調用。
看一下FormatterRegistry SPI的代碼:
package org.springframework.format; public interface FormatterRegistry extends ConverterRegistry { void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser); void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter); void addFormatterForFieldType(Formatter<?> formatter); void addFormatterForAnnotation(AnnotationFormatterFactory<?, ?> factory); }
正如上面顯示,能夠經過fieldType或者註解來註冊Formatter。
FormatterRegistry SPI容許開發者集中配置格式化(Formatting)規則,而不是在控制器(Controller)中重複這些配置。例如,若是但願按照某種規則對全部的Date字段進行格式化,或者按照某種規則對註解字段進行格式化,能夠經過共享FormatterRegistry,實現「一次定義,按需應用」。
FormatterRegistrar SPI用來利用FormatterRegistry註冊formatter和converter。
package org.springframework.format; public interface FormatterRegistrar { void registerFormatters(FormatterRegistry registry); }
在爲給定的一組分類註冊多個相關的converter和formatter時,FormatterRegistrar比較有用,例如Date格式化。一樣在聲明註冊不足以表示時比較有用。例如,當formatter須要在不一樣於它自 己的泛型
Spring MVC應用程序中,開發者能夠顯式配置一個自定義的ConversionService實例,做爲
爲了使用默認的格式化規則,不在Spring MVC配置文件中添加自定義配置便可。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> <mvc:annotation-driven/> </beans>
只須要配置一行
若是使用自定義的formatter和converter,能夠注入自定義的ConversionService實例。在Spring配置文件中設置conversion-service屬性,類型爲 FormatteringConversionServiceFactoryBean,設置自定義的converter、formatter或者FormatterRegistrar做爲conversion-service的屬性。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> <mvc:annotation-driven conversion-service="conversionService"/> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="converters"> <set> <bean class="org.example.MyConverter"/> </set> </property> <property name="formatters"> <set> <bean class="org.example.MyFormatter"/> <bean class="org.example.MyAnnotationFormatterFactory"/> </set> </property> <property name="formatterRegistrars"> <set> <bean class="org.example.MyFormatterRegistrar"/> </set> </property> </bean> </beans>
沒有被@DateTimeFormat註解的date和time字段默認使用DateFormat.SHORT全局樣式。開發者能夠按照本身的需求從新定義全局格式。
Spring沒有註冊默認的formatter,你須要確保手動註冊全部的formatter。使用org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar
仍是org.springframework.format.datetime.DateFormatterRegistrar類,取決你是否使用了Joda Time庫。
例以下面的Java配置會註冊一個'yyyyMMdd'的全局格式,不依賴Joda Time庫。
@Configuration public class AppConfig { @Bean public FormattingConversionService conversionService() { // Use the DefaultFormattingConversionService but do not register defaults DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false); // Ensure @NumberFormat is still supported conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory()); // Register date conversion with a specific global format DateFormatterRegistrar registrar = new DateFormatterRegistrar(); registrar.setFormatter(new DateFormatter("yyyyMMdd")); registrar.registerFormatters(conversionService); return conversionService; } }
若是你傾向於基於XML的配置,你可使用FormattingConversionServiceFactoryBean。下面是相同的樣例,使用Joda Time。
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="registerDefaultFormatters" value="false" /> <property name="formatters"> <set> <bean class="org.springframework.format.number.NumberFormatAnnotationFormatterFactory" /> </set> </property> <property name="formatterRegistrars"> <set> <bean class="org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar"> <property name="dateFormatter"> <bean class="org.springframework.format.datetime.joda.DateTimeFormatterFactoryBean"> <property name="pattern" value="yyyyMMdd"/> </bean> </property> </bean> </set> </property> </bean> </beans>
在Spring MVC中應用的狀況下,要顯式配置conversion service。若是使用基於Java的@Configuration註解,須要擴展WebMvcConfigurationSupport類,覆蓋mvcConversionService()方法。若是使用XML配置文件,須要使用'mvc:annotation-driven'元素的'conversion-service'屬性。查閱7.6.5章節「在Spring MVC中配置格式化」獲取更詳細的信息。
Spring 3 加強了驗證支持庫。第一,徹底支持JSR-303 Bean Validation API;第二,當使用代碼方式時,Spring的DataBinder除了綁定對象,還能夠驗證對象。第三,Spring MVC如今已經支持聲明式驗證@Controller輸入。
JSR-303 爲Java平臺驗證定義了驗證約束聲明和元數據模型。使用該API,經過聲明式驗證約束來註解域模型(Domain Model)屬性,運行時平臺會強制執行。可使用API中的內置約束,也能夠定義本身的約束。
下面用一個簡單的包含兩個屬性的PersonForm模型演示說明。
public class PersonForm { private String name; private int age; }
JSR-303容許對這些屬性定義聲明式驗證約束。
public class PersonForm { @NotNull @Size(max=64) private String name; @Min(0) private int age; }
當該類的實例被JSR-303 Validator驗證時,這些約束就會執行。
若是須要了解JSR-303的更多信息,請瀏覽Bean Validation Specification(Bean驗證規範)。若是須要了解默認參考實現,請瀏覽Hibernate Validator(Hibernate驗證器)。學習如何將JSR-303實現框架設置爲Spring Bean,請閱讀下面章節。
Spring徹底支持JSR-303 Bean Validation API。這包括支持方便的注入JSR-303實現類爲Spring Bean,能夠按需將javax.validation.ValidatorFactory或者javax.validation.Validator注入到應用程序中。
使用LocalValidatorFactoryBean來配置一個默認的JSR-303實現類做爲Spring Bean。
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
以上的基本配置會使用默認的引導機制觸發JSR-303初始化。將JSR-303提供者(例如Hibernate Validation)的庫文件加入classpath,它會被自動檢測。
LocalValidatorFactoryBean實現了javax.validation.ValidatorFactory接口和javax.validation.Validator接口,以及Spring的org.springframework.validation.Validator接口。能夠將上述任意一個接口注入到須要驗證的Bean中,來啓動驗證邏輯。
若是你但願直接使用JSR-303 API,注入javax.validation.Validator。
import javax.validation.Validator; @Service public class MyService { @Autowired private Validator validator;
若是你的Bean須要使用Spring Validator API,注入org.springframework.validation.Validator
import org.springframework.validation.Validator;
@Service public class MyService { @Autowired private Validator validator; }
每一個JSR-303驗證約束包含兩部分。第一部分,@Constraint註解,聲明約束,以及可配置的屬性。第二部分,javax.validation.ConstraintValidator接口的實現,實現約束行爲。爲了將實現與聲明關聯起來,每個@Constraint註解指向一個對應的ValidationConstraint實現類。在運行時,當在域模型中遇到約束註解後,ConstraintValidatorFactory就實例化一個ConstraintValidator。
默認LocalValidatorFactoryBean配置一個SpringConstraintValidatorFactory,使用Spring建立ConstraintValidator實例。與普通Spring Bean同樣,能夠在自定義的ConstraintValidator中使用依賴注入。
下面是一個自定義的@Constraint聲明,後面跟着相關聯的ConstraintValidator實現,使用Spring進行依賴注入。
@Target({ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy=MyConstraintValidator.class) public @interface MyConstraint { } import javax.validation.ConstraintValidator; public class MyConstraintValidator implements ConstraintValidator { @Autowired; private Foo aDependency; ... }
正如你看到的,ConstraintValidator實現類中能夠像其它Spring Bean同樣使用Spring的依賴注入(@Autowired關鍵字)。
默認的LocaValidatorFactoryBean配置對於絕大多數狀況是足夠的。不一樣的JSR-303構造器還有一些其它配置項,如message interpolation和traversal resolution。對於這些選項的具體意義,能夠查看LocalValidatorFactoryBean 的JavaDoc。
從Spring 3開始,DataBinder實例能夠配置一個Validator。一旦配置,Validator能夠經過binder.validate()觸發。任何驗證錯誤都會自動添加到binder的BindingResult。
當使用代碼方式使用DataBinder時,在綁定一個目標對象後,能夠觸發驗證邏輯。
Foo target = new Foo(); DataBinder binder = new DataBinder(target); binder.setValidator(new FooValidator()); // bind to the target object binder.bind(propertyValues); // validate the target object binder.validate(); // get BindingResult that includes any validation errors BindingResult results = binder.getBindingResult();
經過dataBinder.addValidators和dataBinder.replaceValidators,DataBinder能夠配置多個Validator實例。對DataBinder實例中配置的局部Spring Validator和已配置的全局JSR-303 Bean Validator進行合併時,頗有用。瀏覽「經過Spring MVC配置Validator」章節。
由Spring 3開始,Spring MVC能夠自動驗證@Controller輸入。在以前的版本,須要開發者手動觸發驗證邏輯。
爲了觸發@Controller輸入驗證,爲輸入參數配置註解,@Valid:
@Controller public class MyController { @RequestMapping("/foo", method=RequestMethod.POST) public void processFoo(@Valid Foo foo) { /* ... */ }
在綁定以後,Spring MVC會驗證@Valid對象,由於一個合適的Validator已經被配置。
注意:@Valid註解是標準JSR-303 Bean Validation API中的一部分,不是Spring特有的。
當@Valid方法參數配置完成後,Validator實例能夠經過兩種方式配置。第一種,在@Controller的@InitBinder回調中調用binder.setValidator(Validator);這樣作能夠爲每個@Controller類配置一個 Validator實例。
@Controller public class MyController { @InitBinder protected void initBinder(WebDataBinder binder) { binder.setValidator(new FooValidator()); } @RequestMapping("/foo", method=RequestMethod.POST) public void processFoo(@Valid Foo foo) { ... } }
第二種,能夠在全局WebBindingInitializer中調用setValidator(Validator),能夠爲全部@Controller配置一個Validator實例。可使用Spring MVC namespace很方便的配置。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> <mvc:annotation-driven validator="globalValidator"/> </beans>
同時應用全局和局部驗證器(validator),按上一步驟配置全局Validator,而後按下面步驟配置局部Validator。
@Controller public class MyController { @InitBinder protected void initBinder(WebDataBinder binder) { binder.addValidators(new FooValidator()); } }
經過JSR-303,一個javax.validation.Validator實例能夠驗證全部聲明驗證約束的模型對象。只須要在classpath中增長JSR-303 Provider的包,例如Hibernate Validator,Spring MVC就會檢測到,並自動爲全部Controller提供JSR-303支持。
Spring MVC配置支持JSR-303以下所示:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> <!-- JSR-303 support will be detected on classpath and enabled automatically --> <mvc:annotation-driven/> </beans>
使用最少配置,當遇到@Valid @Controller輸入時,由JSR-303 provider進行驗證。反過來,JSR-303會強制對聲明驗證的輸入進行約束。當BindingResult中有錯誤時,ConstraintValidation會自動顯示,經過標準的Spring MVC 表單標籤渲染。