(英文原版. Spring 3.2.14 Reference - 7 Validation, Data Binding, and Type Conversion)
(如有翻譯不準确的地方,請私信我,共同提高;轉載請注明出處,謝謝)
7 驗證、資料綁定和類型轉換
7.1 介紹
對于把校驗作為業務邏輯有正面意見也有負面意見,而Spring提供的驗證設計(和資料綁定)不考慮這些。特别是,驗證不應該依賴于Web層,它應該很容易被調用,可以插入任何可用的驗證器。基于上述的考慮,Spring提供了一個Validator接口,這是一個基礎元件,可以用于應用程式的每一層。
資料綁定機制允許将使用者輸入動态綁定到應用程式的域模型對象(或用來處理使用者輸入的任何對象)。Spring提供了DataBinder類實作資料綁定機制。
Validator和DataBinder組成了validation包,主要但不限于在MVC framework中應用。BeanWrapper是Spring Framework中的一個基礎元件,在很多地方都用到,但是開發者一般不會直接使用。因為這是一個參考文檔,是以本章對BeanWrapper進行一些必要的解釋。如果開發者想使用它,最可能是在資料綁定場景下使用。
Spring的DataBinder和底層的BeanWrapper都使用PropertyEditor來解析和格式化屬性值。PropertyEditor是JavaBean所特有的,在本章也會進行必要的解釋。Spring 3引入了“core.convert”包,既提供了一個通用類型轉換工具,也提供了進階“format”包來格式化UI字段值。Spring中的這些新的程式包用起來可能比PropertyEditor簡單,在本章也會讨論。
7.2 Spring Validator接口
Spring 引入了Validator接口,可以用來驗證對象。Validator接口與Errors對象一起使用,驗證時Validator可以報告驗證錯誤到Errors對象。
讓我們考慮一個簡單資料對象。
public class Person {
private String name;
private int age;
// the usual getters and setters...
}
通過實作org.springframework.validation.Validator接口的兩個方法為Person類提供驗證:
- supports(Class ) - Validator是否支援驗證Class類型,傳回true或者false。
- validate(Object, org.springframework.validation.Errors) - 驗證Object對象,如果驗證錯誤,将錯誤注冊到Errors對象參數。
實作Validator比較簡單,特别是借助Spring Framework提供的ValidationUtils幫助類時。
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,如下所示:
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中使用,可以使用标簽來注入錯誤消息(error message),當然你也可以自行檢查errors對象。更多關于這些方法的資訊可以從Javadoc中找到。
7.3 錯誤碼與錯誤消息
我們讨論了資料綁定和驗證。對應驗證錯誤的消息輸出是我們需要讨論的最後一件事。在之前的例子中,我們拒絕了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。
7.4 Bean 處理和BeanWrapper
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進行操作,例如設定和通路屬性。
7.4.1 設定和擷取基本屬性和嵌套屬性
使用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");
7.4.2 内置的PropertyEditor實作
Spring使用PropertyEditor來處理Object和String之間的轉換。有時候使用PropertyEditor比使用對象本身可以更友善的表示屬性。例如,可以用易讀的方式(如“2007-14-09”)表示Date,也可以将其轉換回原始Date對象(更進一步:将任意易讀的時間格式轉換為Date對象),而這種效果通過注冊java.beans.PropertyEditor類型的自定義editor就可以實作。在BeanWrapper或IoC容器上注冊自定義editor,可以轉換對象的屬性為指定的類型。如果需要擷取PropertyEditor更多的資訊,可以閱讀由Sun提供的java.beans包的Javadoc文檔。
Spring中使用屬性編輯的兩個例子:
- 使用PropertyEditor設定bean屬性。在XML檔案中注入bean的某些屬性的值為java.lang.String類型,如果該屬性的指派方法中的參數是Class類型,Spring會使用ClassEditor将String類型轉換為Class類型。
-
Spring MVC架構中使用所有可用的PropertyEditor解析處理來自用戶端的HTTP請求參數,開發者可以在CommandController的所有子類中手工綁定PropertyEditor。
表7.2中列出了Spring 架構中org.springframework.beans.propertyeditors 包中内置的所有PropertyEditor。大多數(并非全部,見表說明)在BeanWrapperImpl中已經預設注冊。配置了PropertyEditor之後,開發者也可以注冊自定義PropertyEditor來覆寫預設類。
表7.2. 内置的PropertyEditor清單
類 | 說明 |
---|---|
ByteArrayPropertyEditor | 将字元串轉換為位元組數組(byte[])。BeanWrapper中已經預設注冊。 |
ClassEditor | 将字元串類名轉換為java.lang.Class。當找不到對應的類時,抛出ellegalArgumentException異常。BeanWrapper中已經預設注冊。 |
CustomBooleanEditor | Boolean類型的自定義屬性編輯器, BeanWrapper中已經預設注冊。可以注冊自定義的執行個體覆寫原有執行個體。 |
CustomCollectionEditor | Collection類型的自定義屬性編輯器,将Collection轉換為給定T類型的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());
}
}
}
注冊自定義PropertyEditor
當為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>
使用PropertyEditorRegistrar
另一個在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)。
7.5 Spring 3 類型轉換
Spring 3 引入了core.convert包,提供通用的類型轉換系統。這個系統定義了一個SPI(Service Provide API)來實作類型轉換邏輯,類似在運作時執行類型轉換的API。在Spring容器中,該系統可以作為PropertyEditor的替代品來轉換外部bean屬性的字元串值為需要的屬性類型。這個公共的API可以用在應用程式任何需要類型轉換的地方。
7.5.1 Converter SPI
用來實作類型轉換邏輯的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);
}
}
7.5.2 ConverterFactory
當需要為整個類層次結構集中類型轉換時,例如,當将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)方法,其中T是R的子類。
下面是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());
}
}
}
7.5.3 GenericConverter
當需要實作一個比較複雜的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即可。
ConditionalGenericConverter
有時候,你可能希望在某些條件下才執行類型轉換。例如,你可能在隻有當注解出現在目标字段上的時候才執行類型轉換。或者,你可能在隻有目标類包含一個靜态方法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方法是否存在。
7.5.4 ConversionService API
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配置。
7.5.5 配置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檢視該部分詳細内容。
7.5.6 通過程式設計使用ConversionService
通過程式設計使用ConversionService,在你想要的任何bean的屬性中注入ConversionService。
@Service
public class MyService {
@Autowired
public MyService(ConversionService conversionService) {
this.conversionService = conversionService;
}
public void doIt() {
this.conversionService.convert(...)
}
}
7.6 Spring 3 字段格式化
在之前章節裡讨論過,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接口。
7.6.1 Formatter SPI
Formatter SPI實作字段格式化很簡單,且為強類型。
package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
Formatter SPI繼承了Printer和Parser的接口。
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;
}
}
7.6.2 注解驅動(annotation-driven)格式化
正如您将看到,字段格式化可以通過字段類型或者注解(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;
}
Format Annotation API
格式化注解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;
}
7.6.3 FormatterRegistry SPI
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,實作“一次定義,按需應用”。
7.6.4 FormatterRegistrar SPI
FormatterRegistrar SPI用來利用FormatterRegistry注冊formatter和converter。
package org.springframework.format;
public interface FormatterRegistrar {
void registerFormatters(FormatterRegistry registry);
}
在為給定的一組分類注冊多個相關的converter和formatter時,FormatterRegistrar比較有用,例如Date格式化。同樣在聲明注冊不足以表示時比較有用。例如,當formatter需要在不同于它自 己的泛型的特定字段類型下建立索引,或者注冊一個Printer/Parser對兒時。下一節對converter和formatter的注冊提供了更多的資訊。
7.6.5 在Spring MVC中配置格式化
Spring MVC應用程式中,開發者可以顯式配置一個自定義的ConversionService執行個體,作為元素的一個屬性。這樣一來,在Controller模型綁定期間,可以随時調用ConversionService進行類型轉換。如果沒有顯式配置,Spring MVC會為常見類型(例如數字Number和日期Date)自動注冊一個預設的formatter和converter。
為了使用預設的格式化規則,不在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>
隻需要配置一行,Spring會自動加載用于數字和日期的預設formatter,包括@NumberFormat和@DateTimeFormat注解。如果classpath中包含Joda Time,那麼完全支援Joda Time格式化庫也被加載。
如果使用自定義的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>
7.7 配置全局date&time格式
沒有被@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中配置格式化”擷取更詳細的資訊。
7.8 Spring 3 Validation
Spring 3 增強了驗證支援庫。第一,完全支援JSR-303 Bean Validation API;第二,當使用代碼方式時,Spring的DataBinder除了綁定對象,還可以驗證對象。第三,Spring MVC現在已經支援聲明式驗證@Controller輸入。
7.8.1 JSR-303 Bean Validation API概覽
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,請閱讀下面章節。
7.8.2 配置Bean Validation Implementation
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,它會被自動檢測。
注入Validator
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。
7.8.3 配置DataBinder
從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”章節。
7.8.4 Spring MVC 3 Validation
由Spring 3開始,Spring MVC可以自動驗證@Controller輸入。在之前的版本,需要開發者手動觸發驗證邏輯。
觸發@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特有的。
通過Spring MVC配置Validator
當@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());
}
}
通過Spring MVC配置JSR-303 Validator
通過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 表單标簽渲染。