Spring 驗證、數據綁定和類型轉換

1 簡介

JSR-303/JSR-349 bean驗證java

Spring Framework 4.0 支持 Bean驗證 1.0(JSR-303)和 Bean驗證 1.1(JSR-349),也可使用Spring的Validator接口進行驗證。web

應用程序能夠選擇一次開啓全局Bean驗證,在第8節Spring驗證中介紹,專門用於全部驗證需求。spring

也能夠爲每個DataBinder接口註冊額外的Spring Validator實例,在8.3節「配置DataBinder中介紹」。這個功能在不使用註解插入驗證邏輯時頗有用。express

考慮將驗證做爲業務邏輯有利有弊,Spring提供了一個不排除任何利弊的驗證(和數據綁定)設計。具體的驗證不該該與Web層相關聯,它應該易於本地化而且應當可以插入任何可用的驗證器。考慮以上需求,Spring提供了基礎的、在應用程序每一個層均可以使用的Validator接口。編程

數據綁定在把用戶輸入動態綁定到應用程序的域模型(或者任何用來處理用戶出入的對象)時頗有用。Spring提供提供了DataBinder完成這個任務。Validator和DataBinder組成了validation包,主要用於但不侷限於MVC框架。數組

BeanWrapper是Spring Framework中的一個基礎概念,而且被用於不少地方。然而也許並不須要直接使用BeanWrapper。安全

Spring的DataBinder和底層的BeanWrapper都使用PropertyEditors解析和格式化屬性值。PropertyEditor概念是JavaBean規範的一部分,而且也會在本章中講解。Spring 3 引入了「core.convert」包用於提供通常類型轉換設施,同時高級別的「format」包用於格式化UI 域數據。這些新包能夠用做PropertyEditors的簡單替代,也會在本章討論。多線程

2 使用Spring的Validator接口進行驗證

Spring提供Validator接口用於驗證對象。Validator接口使用Errors對象,以便在驗證時驗證器能夠將驗證失敗報告給Errors對象。mvc

考慮一個小的數據對象:app

public class Person {
    private String name;
    private ing age;

    // the usual getters and setters...
}

咱們將要經過實現org.springframework.validation.Validator接口的兩個方法爲Person類提供驗證行爲:

  • support(Class) 這個Validator驗證接口是否支持Class參數;
  • validate(Object, org.springframework.validation.Errors) 驗證給定的對象而且若是有驗證錯誤,使用給定的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");
        }
    }
}

正如所見,ValidationUtils類的靜態方法rejectIfEmpty(...)用於拒絕name屬性若是當它爲null或空字符串。能夠查看ValidationUtils的javadoc瞭解除上面的例子外還提供了什麼方法。

儘管能夠實現單個Validator類用於驗證在複雜對象中的每一個嵌入對象,在其本身的Validator實現中封裝每一個嵌入對象的驗證邏輯可能會更好。一個關於複雜對象的簡單例子是一個Customer類由兩個String屬性(first name 和 second name)和一個Address對象組成。Address對象可能會獨立於Customer對象使用,因此不一樣的AddressValidator被實現。若是但願CustomerValidator重用AddressValidator類中的代碼,不是經過複製粘貼,能夠經過依賴注入或在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對象。在Spring Web MVC中可使用spring:bind/標籤來檢查錯誤消息,可是固然能夠本身檢查錯誤消息。這個方法更多的信息能夠查看javadoc。

3 將錯誤碼解析爲錯誤消息

已經討論過數據綁定和驗證。關於驗證錯誤的輸出消息是須要討論的最後一個事情。在上面的例子中,驗證了name和age字段。若是想要使用MessageSource輸出錯誤消息,將在拒絕該字段(此處爲name和age)時給出錯誤代碼。當調用(直接或間接的,例如使用ValidationUtils類)rejectValue或者Errors接口的其它reject方法之一,底層實現不只註冊出錯的代碼,同時註冊一系列附加的錯誤代碼。會註冊什麼錯誤代碼取決於使用的MessageCodesResolver。默認的,使用DefaultMessageCodesResolver,它不只使用給定的代碼註冊消息,還包含傳遞給reject方法的域名字。因此若是使用rejectValue("age", "too.darn.old")拒絕一個域,除了too.darn.ord代碼,Spring還會註冊too.darn.ord.age和too.darn.old.age.int(前一個包含域名而且後一個包含域類型);這有助於幫助開發者定位錯誤信息。

更多的關於MessageCodesResolver和默認策略能夠分別查找MessageCodesResolver和DefaultMessageCodesResolber的javadoc。

4 Bean操做和BeanWrapper

org.springframework.beans包遵照Oracle提供了JavaBean標準。一個JavaBean僅僅是一個類,它擁有默認的無參構造函數,而且遵照命名約定,例如一個屬性名爲bingoMadness須要有一個setter方法setBingoMadness(...)和getter方法getBingoMadness()。關於更多的JavaBean信息,請參考Oracle的網站。

在beans包中至關重要的類是BeanWrapper接口和它的相關實現(BeanWrapperImpl)。正如javadoc中提到的,BeanWrapper提供設置和讀取值的功能(單獨或批量),獲取屬性描述和查詢屬性是否可讀或可寫。同時,BeanWrapper提供對嵌入屬性的支持,能夠在無限深度上設置屬性的子屬性。同時,BeanWrapper支持添加標準JavaBean PropertyChangeListeners和VetoableChangeListeners,不須要在目標類添加支持代碼。最後但並不是最不重要的是,BeanWrapper提供了對設置索引屬性的支持。BeanWrapper一般不被應用代碼直接使用,它被DataBinder和BeanFactory使用。

BeanWrapper的工做方式部分的被它的名字展示:它包裝一個bean以對該bean執行操做,例如設置和檢索屬性。

4.1 設置和獲取基本和嵌入屬性

使用setPropertyValue和getPropertyValue方法設置和獲取屬性,這兩個方法都有幾個重載版本。它們都在Spring的javadoc中有詳細的說明。重要的是指出一些對象表示屬性的約定。下面是一些例子:

表達式 解釋
name 表示name屬性的相關方法爲getName()或isName()和setName(...)
account 表示account屬性的嵌入屬性name的相關方法爲getAccount().setName或getAccount().getName()
account[2] 表示索引屬性account的第三個元素。索引屬性能夠是array,list或其它天然排序的集合
account[COMPANYNAME] 表示Map屬性account的鍵COMPANYNAME對應的值。

下面會看到一些使用BeanWrapper來獲取和設置屬性的例子。

(若是不打算直接使用BeanWrapper,下個章節不是十分重要。若是僅僅使用DataBinder、BeanFactory和它們開箱即用的實現,能夠跳過關於PropertyEditors的部分。)

考慮下面的兩個類:

public class Company {

    private String name;
    private Employee managingDirector;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Employee getManagingDirector() {
        return this.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;
    }
}

下面的代碼段展現了一些例子關於若是獲取和操做Company和Employee實例的一些屬性:

BeanWrapper company = new BeanWrapperImpl(new Company());
// setting the company name..
company.setPropertyValue("name", "Some Company Inc.");
// ... can also be done like this:
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);

// ok, let's create the director and tie it to the company:
BeanWrapper jim = new BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());

// retrieving the salary of the managingDirector through the company
Float salary = (Float) company.getPropertyValue("managingDirector.salary");

4.2 內建的PropertyEditor實現

Spring使用PropertyEditor的概念用於影響對象和字符串之間的轉換。若是考慮到這一點,有時可能會方便的用不一樣的方式方便的表示屬性,而不是對象自己。例如,一個Date對象能夠被表示爲人類可讀的方式(做爲字符串‘2007-09-14’),同時咱們仍可以轉換人類可讀的形式回到原始的日期對象(或者更好的:轉換任何日期對象爲人類可讀的方式,並能夠轉換回Date對象)。這種行爲能夠經過註冊自定義的編輯器得到,編輯器的類型是java.beans.PropertyEditor。在BeanWrapper上註冊自定義編輯器或者前面章節提到的在一個指定的IoC容器中註冊,而後告訴它如何轉換屬性到指望的類型。從Oracle提供的java.beans包的javadoc中能夠閱讀到有關PropertyEditors的更多信息。

在Spring中有一些關於屬性編輯的例子:

  • 在bean上設置屬性是經過PropertyEditors完成的。當java.lang.String做爲在XML文件中聲明的某個bean的屬性值時,Spring將會(若是相關屬性的設置方法有一個Class類型的參數)使用ClassEditor嘗試解析參數爲Class對象。

  • 在Spring MVC 框架中解析HTTP請求參數是使用能夠在CommandController的全部子類中手動綁定的各類PropertyEditor完成的。

Spring 有一系列內建的PropertyEditors。它們都在下面的表格中列出而且都位於org.springframework.beans.propertyeditors包中。其中大多數,但不是所有(以下所述),被BeanWrapperImpl自動註冊。屬性編輯器能夠以某種方式配置,固然能夠註冊本身的編輯器實現。

解析
ByteArrayPropertyEditor 字節數組的編輯器。字符串將會簡單的轉換爲它們相應的字節表示。被BeanWrapperImpl默認註冊。
ClassEditor 解析字符串表示的類爲實際的類,和相反的方向。當沒法找到類,會拋出IllegalArgumentException。被BeanWrapperImpl默認註冊
CustomBooleanEditor 用於Boolean屬性的可定製編輯器。被BeanWrapperImpl默認註冊,可是能夠經過註冊自定義的Boolean編輯器實例被覆蓋
CustomCollectionEditor 用於集合的屬性編輯器,轉換任何資源的集合爲給定目標Collection類型。
CustomDateEditor 用於java.util.Date的可定製屬性編輯器,支持自定義DaeFormat。不會被默認註冊。必須被用戶以正確格式註冊。
CustomNumberEditor 用於任何Number子類例如Integer、Long、Float、Double的可定製屬性編輯器。被BeanWrapperImpl默認註冊,可是能夠經過註冊自定義的實例被覆蓋。
FileEditor 解析字符串爲java.io.File對象。被BeanWrapperImpl默認註冊。
InputStreamEditor 單向的屬性編輯器,可以獲取文本字符串並生成(經過內部的ResourceEditor和Resource)InputStream,因此InputStream屬性能夠直接設置成字符串。請注意,默認的使用不會關閉InputStream。被BeanWrapperImpl默認註冊。
LocaleEditor 可以解析字符串爲Locale對象而且反之亦然(字符串的格式是[國家][變種],與Locale提供的toString()方法返回的結果相同)。被BeanWrapperImpl默認註冊。
PatternEditor 可以轉換字符串爲java.util.regex.Pattern對象而且反之亦然。
PropertiesEditor 可以轉換字符串(使用java.util.Properties類的javadoc中定義的格式進行格式化)爲Properties對象。被BeanWrapperImpl默認註冊。
StringTrimmerEditor 用於修剪字符串的屬性編輯器。可選的將空字符串轉換爲null值。不會被默認註冊;由用戶按需註冊。
URLEditor 可以解析字符串表示的URL爲實際的URL對象。被BeanWrapperImpl默認註冊。

Spring使用java.beans.PropertyEditorManager設置所需的屬性編輯器的搜索路徑。搜索路徑也包含sun.bean.editors,它包含了用於Font,Color和大多數基本類型的PropertyEditor實現。也請注意標準的JavaBeans基礎設置會自動發現PropertyEditor類(不須要你顯示註冊它們)若是它們位於它們要處理類的相同的包中,而且有與被處理的類有相同的名字並加上Editor後綴;例如能夠有以下的類和包結構,FooEditor類能夠被自動註冊而且做爲Foo類型屬性的PropertyEditor。

com
    chank
        pop
            Foo
            FooEditor // Foo類的屬性編輯器

請注意,在這裏你也可使用標準的BeanInfo JavaBeans機制。在下面的例子中,使用BeanInfo機制來顯示的註冊用於相關類屬性的一個或多個PropertyEditor實例。

com
    chank
        pop
            Foo
            FooBeanInfo // Foo類的BeanInfo

如下是引用FooBeanInfo類的Java源代碼。這將Foo類的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控制反轉容器最終使用標準的JavaBeans PropertyEditor來轉換這些字符串爲複雜類型的屬性。Spring預先註冊了一系列的自定義PropertyEditors(例如,轉換用字符串表示的類名爲實際的類對象)。例如,Java的標準JavanBeans PropertyEditor查找機制容許PropertyEditor被自動查找,只要它被正確的命名並於它提供支持的類位於同一包中。

若是須要註冊其它自定義的PropertyEditors,有幾種可行的機制。最手工的辦法,並非很便利且不被推薦,是使用ConfigurableBeanFactory接口的registerCustomerEditor()方法,須要你有一個BeanFactory引用。另一個方式,更方便一些,是使用名爲CustomEditorConfigurer的特殊的bean工廠後處理器。儘管bean工廠後處理器能夠於BeanFactory的實現一塊兒使用,但CustomEditorConfigurer具備嵌套屬性設置,因此強烈推薦它與ApplicationContext一塊兒使用,它將會與其它bean同樣的方式被部署,而且被自動發現和應用。

須要注意的是,全部bean工廠和應用上下文自動使用一些內建的屬性編輯器,經過使用BeanWrapper類處理屬性轉換。BeanWrapper註冊的標準的屬性編輯器在上一節中列出。額外的,ApplicationContext也能夠以適用於特定應用上下文類型的方式覆蓋或添加其它編輯器來處理資源。

標準JavaBeans PropertyEditor實例用於轉換字符串表示的屬性值胃複雜類型的屬性。CustomEditorConfigurer,一個bean工廠後處理器,能夠用於方便的爲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對象
package example;

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>

使用PropertyEdotirRegistrars

用於在Spring容器中註冊屬性編輯器的另外一個機制是建立和使用PropertyEditorRegistrar。這個接口尤爲有用當你須要使用相同屬性編輯器的集合在不一樣的場景中:編寫一個相應的註冊器而且在每一個場景中重用。PropertyEditorRegistrars與名爲PropertyEditorRegistry的接口配合工做,PropertyEditorRegistry接口被Spring的BeanWrapper(和DataBinder)實現。PropertyEditorRegistrars尤爲方便當與CustomEditorConfigurer一塊兒使用時,它暴露了名爲setPropertyEditorRegistrars(...)的屬性:以這種方式添加到CustomEditorConfigurer中的PropertyEditorRegistrars能夠輕鬆地與DataBinder和Spring MVC控制器共享。此外,它避免了在自定義編輯器上同步的須要:一個PropertyEditorRegistrar能夠爲每一個bean建立嘗試過程建立新的PropertyEditor實例。

使用PropertyEditorRegistrar最好經過一個例子來講明。首先,你須要建立你本身的PropertyEditorRegistrar實現:

package com.foo.editors.spring;

public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {

    public void registerCustomEditors(PropertyEditorRegistry registry) {

        // 指望新的PropertyEditor實例被建立
        registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());

        // 在這裏能夠註冊儘量多的自定義屬性編輯器...
    }
}

也能夠查看org.springframework.beans.support.ResourceEditorRegistrar做爲一個PropertyEditorRegistrar實現的例子。注意如何在registerCustomEditors(...)方法的實現中建立每一個屬性編輯器的新實例。

而後咱們配置CustomEditorCOnfigurer而且注入一個CustomPropertyEditorRegistrar的實例:

<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框架的人,PropertyEditorRegistrars與數據綁定Controllers(例如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);
    }

    // 註冊用戶的其它方法
}

這種PropertyEditor註冊方法能夠帶來簡潔的代碼(initBinder(...)的實現僅僅一行代碼的長度!),而且容許常見的PropertyEditor註冊代碼封裝在一個類中而後在須要的Contollers中共享。

5 Spring類型轉換

Spring 3引入了core.convert包提供通常類型轉換系統。系統定義了一個SPI實現類型轉換邏輯和一個API執行運行時類型轉換。在一個Spring容器中,這個系統能夠做爲PropertyEditor的替代方法用於轉換外部bean屬性值字符串爲須要的屬性類型。公共的API能夠在你的應用程序的任何須要類型轉換的地方使用。

5.1 轉換器SPI

實現類型轉換邏輯的SPI是簡單和強類型的:

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

    T convert(S source);

}

要建立你本身的轉換器,實現上面的接口就能夠了。範型參數S是要轉換的類型,T是要轉換到的類型。這樣一個轉換器也能夠透明的應用當一個S類型的集合或數組須要被轉換爲一個T類型的數組或集合,只要已經註冊了一個代理數組/集合的轉換器(默認狀況下是DefaultConversionService)。

對於convert(S)的每次調用,源參數保證不會空。你的轉換器能夠拋出任何未檢查異常若是轉換失敗;特別的,報告一個無效的源值須要拋出IllegalArgumentException。注意保證你的Converter實現是現成安全的。

在core.convert.support包中提供了一些轉換器的實現。它們包含了從字符串到數字和其它常見類型的轉換方法。以StringToInteger爲例,介紹典型的Converter實現:

package org.springframework.core.convert.support;

final class StringToInteger implements Converter<String, Integer>{
    
    public Integer convert(String source){
        return Integer.valueOf(source);
    }
}

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>),其中T是R的子類。

以StringToEnum的ConverterFactory做爲一個例子:

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());
        }
    }
}

5.3 通用轉換器

當你須要一個複雜的轉換器實現,能夠考慮GenericConverter接口。使用更靈活可是不強的類型簽名,一個GenericConverter支持在多種源和目標類型間轉換。此外,GenericConverter能夠提供源和目標域上下文,你能夠在實現轉換邏輯時使用。這種上下文容許類型轉換由域註解或在域簽名上聲明的通用信息來驅動。

package org.springframework.core.convert.converter;

public interface GenericConverter {

    public Set<ConvertiblePair> getConvertibleTypes();

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);

}

實現一個GenericConverter,使getConvertibleTypes()返回支持的源->目標類型對。而後用轉換邏輯實現convert(Object, TypeDescriptor, TypeDescriptor)方法。源TypeDescriptor提供對保存正在轉換的值的源字段的訪問。目標TypeDescriptor提供對將被設置轉換值的目標字段的訪問。

一個關於GenericConverter很好的例子是Java數組和集合之間的轉換器。這種ArrayToCollectionConverter內省聲明目標集合類型的字段以解析集合元素類型。這容許源數組中的每一個元素在集合目標字段設置以前轉換爲集合元素類型。

因爲GenericConverter時更復雜的SPI接口,僅當你須要的時候使用。優選選擇Converter或ConverterFactory用於基礎類型轉換的需求。

ConditionalGenericConverter

有時你僅須要一個轉換器在指定條件爲true時執行。例如,你可能想要在目標字段上提供指定的註解是執行Converter。或者想要在目標類中定義了指定的方法,例如一個靜態的valueOf方法是執行Converter。ConditionalGenericConverter是GenericConverter和ConditionalConverter接口的組合,容許你定義此類自定義匹配條件:

public interface ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);

}

public interface ConditionalGenericConverter
    extends GenericConverter, ConditionalConverter {

}

ConditionalGenericConverter的一個很好的例子是EntityConverter用於持久化標識符與一個實體引用間的轉換。這種EntityConverter僅完成匹配當目標實體類型聲明瞭一個靜態查找器方法例如findAccount(Long)。你須要執行這種查找方法檢查在matches(TypeDescriptor, TypeDescriptor)的實現中。

5.4 ConversionService API

ConversionService定義了統一的API用於執行運行時類型轉換邏輯。轉換器常常在這個正面接口的後面執行:

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用於註冊轉換器。在內部,一個ConversionService實現代理它註冊的轉換器用於攜帶轉換邏輯。

一個健壯的ConversionService實如今core.convert.support包中被提供。GenericConversionService是適用於大多數環境的通用實現。ConversionServiceFactory提供了一個方便的工廠用於建立常見的ConverionService配置。

5.5 配置一個ConversionService

ConversionService是一個無狀態的對象,被設計爲應用啓動階段實例化,而後在多線程間共享。在一個Spring應用中,典型的每一個Spring容器(或ApplicationContext)配置一個ConversionService實例。這個ConversionService將會被Spring打包而且在須要類型轉換的時候被框架使用。你能夠將ConversionService注入到你的任何bean而且直接使用它。

若是Spring沒有註冊ConversionService,原始的基於PropertyEditor系統會被使用。

爲了使用Spring註冊默認的ConversionService,添加下面的bean定義使用id conversionService:

<bean id="conversionService"
    class="org.springframework.context.support.ConversionServiceFactoryBean"/>

一個默認的ConversionService能夠在字符串、數字、枚舉、集合、映射和其餘常見類型之間轉換。爲了使用自定義的轉換器補充和覆蓋默認的轉換器,設置converters屬性。屬性值能夠實現了Converter、ConverterFactory或者GenericConverter接口的類。

<bean id="conversionService"
        class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="example.MyCustomConverter"/>
        </set>
    </property>
</bean>

在Spring MVC應用中使用ConversionService也是很常見的。參看Spring MVC 章節中的「轉換和格式化"。

在某些狀況下想應用在轉換中應用格式化,能夠查看6.3節「格式化器註冊 SPI"關於使用FormattingConversionServiceFactoryBean的更多信息。

5.6 編程的方式使用ConversionService

編程的方式使用ConversionService,像其餘任何bean同樣將它注入便可:

@Service
public class MyService {

    @Autowired
    public MyService(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    public void doIt() {
        this.conversionService.convert(...)
    }
}

在大多數用例中,convert方法指定的targetType能夠被使用,可是面對更復雜的類型它就不會工做,例如範型參數的集合。例如若是想要編程的方法轉換一個Integer的List到String的List,你須要提供正式源和目標類型的定義。

幸虧,TypeDescriptor提供了各類方法使之簡化:

DefaultConversionService cs = new DefaultConversionService();

List<Integer> input = ....
cs.convert(input,
    TypeDescriptor.forObject(input), // List<Integer> type descriptor
    TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));

須要注意的是DefaultConversionService自動註冊適用於大多數場景的轉換器。包含了集合轉換器、純量轉換器和基本的對象到字符串轉換器。可使用DefaultConversionService類上的靜態addDefaultConverters方法向任何ConverterRegistry註冊相同的轉換器。

值類型的轉換器會被數組和集合重用,因此沒有必要建立一個特定的轉換器用於轉換S的Collection到T的Collection,假設標準的集合處理是正確的。

6 Spring 字段格式化

如前面章節所述,core.convert是通常用途的類型轉換系統。它提供統一的ConversionService API和強類型的轉換SPI用於實現從一個類型到另外一個類型的轉換邏輯。Spring容器使用這個系統綁定bean屬性值。此外,Spring表達式語言(SpEL)和DataBinder使用這個系統綁定字段值。例如,當SpEL須要強轉一個Short到Long用於完成expression.setValue(Object bean, Object value)嘗試,core.convert系統執行這個強轉。

如今考慮一個典型的客戶端環境(如Web或桌面應用程序)的類型轉換要求。在這種環境中,一般會從字符串轉換來支持客戶回傳過程,同時轉換回字符串來支持視圖描繪過程。此外,常常須要本地化字符串的值。更通用的core.convert轉換器SPI不直接處理這中格式化需求。爲了直接處理它們,Spring 3進入了一個方便的格式化器SPI提供簡單而且強壯的用於客戶端環境的PropertyEditors的替代方法。

通常狀況下,使用轉換器SPI當須要實現通用目的的類型轉換邏輯;例如,用於在java.util.Date和java.lang.Long之間的轉換。使用格式化器SPI當在客戶端環境工做時,例如一個web應用,須要解析和打印出本地化字段值。ConversionService爲兩種SPI都提供統一的類型轉換API。

6.1 格式化器SPI

格式化器SPI用於實現字段格式化邏輯,它是簡單和強類型的:

package org.springframework.format;

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

Formatter接口擴展了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接口便可。範型參數T是但願格式化的對象的類型,例如,java.util.Date。實現print()方法以打印用於在客戶端區域中顯示的T的實例。實現parse()方法用於從在客戶端區域的格式化表示解析T的實例。格式化器若是解析失敗須要拋出ParseException或IllegalArgumentException。必定確保格式化器的實現是線程安全的。

集中格式化器的實如今format子包中被提供。number包提供了NumberFormatter、CurrencyFormatter和PercentFormatter用於使用java.text.NumberFormat格式化java.lang.Number對象。datetime包提供DateFormatter用於使用java.text.DateFormat格式化java.util.Date對象.datetime.joda包提供全面的日期時間格式化支持,它基於Joda Time library。

考慮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;
    }

}

Spring團隊歡迎社區驅動的格式化器貢獻;查詢jira.spring.io。

6.2 註解驅動的格式化

如你所見,字段格式化能夠被字段類型或註解配置。爲了綁定一個註解到格式化器,須要實現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()返回一個打印器用於打印註解字段的值。使getParser()返回一個解析器用於解析被註解字段的客戶端值。

下面AnnotationFormatterFactory實現的例子綁定@NumberFormat註解到格式化器。此註解容許指定數字樣式或模式:

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

一個便攜的格式化註解API在org.springframework.format.annotaiont包中。使用@NumberFormat格式化java.lang.Number字段。使用@DateTimeFormat格式化java.util.Date、java.util.Calendar、java.util.Long或Joda Time字段。

下面的例子使用@DateTimeFormat格式化一個java.util.Date字段爲ISO時間(yyyy-MM-dd):

public class MyModel {

    @DateTimeFormat(iso=ISO.DATE)
    private Date date;

}

6.3 FormatterRegistry SPI

FormatterRegistry是一個用於註冊格式化器和轉換器的SPI。FormattingConversionService是一個可用於大多數環境的FormatterRegistry套件的實現。這個實現可使用編程的方式配置或者使用FormattingConversionServiceFactoryBean聲明它爲一個Spring bean。由於這個實現也實現了ConversionService接口,它能夠被直接配置爲使用Spring的數據綁定器和Spring表達式語言。

下面是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);

}

如上所述,格式化器能夠被字段類型或註解註冊。

FormatterRegistry SPI容許你集中配置格式化規則,而不是在各個Controller中重複的配置。例如,你也許想要讓全部Date字段經過一個肯定的方法被格式化,或者使用指定註解的字段經過一個肯定的方法被格式化。使用共享的FormatterRegistry,你能夠定義這些規則一次而且它們被應用於任何須要格式化的時間。

6.4 FormatterRegistrar SPI

FormatterRegistrar是一個經過FormatterRegistry註冊格式化器和轉化器的SPI:

package org.springframework.format;

public interface FormatterRegistrar {

    void registerFormatters(FormatterRegistry registry);

}

FormatterRegistrar在爲一類給定的格式化註冊多個相關的轉換器和格式化器時是頗有用的,例如時間轉換器。它在聲明註冊沒法知足需求時也是有用的。例如當一個格式化器須要被一個不一樣於它的範型參數<T>的特定類型檢索時或者當註冊一個打印器/格式化器對時。下一節提供關於轉換器和格式化器註冊的更多信息。

6.5 在Spring MVC中配置格式化器

能夠查看Spring MVC一章中的「轉換和格式化」一節。

7 配置一個全局的日期和時間格式化

默認的,沒有被@DateTimeFormat註解註釋的日期和時間字段使用DateFormat.SHORT風格從字符串轉換。若是須要,能夠經過定義本身的全局格式化修改這種行爲。

須要保證Spring沒有註冊默認的格式化器,而且做爲替代你須要手動註冊全部的格式化器。根據是否使用Joda Time庫使用org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar或者org.springframework.format.datetime.DateFormatterRegistrar類。

例如,下面的Java配置將會註冊一個全局的「yyyyMMdd」格式。這個例子沒有使用Joda Time庫:

@Configuration
public class AppConfig {

    @Bean
    public FormattingConversionService conversionService() {

        // 使用DefaultFormattingConversionService可是不是用默認的註冊器
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);

        // 確保仍然支持@NumberFormat
        conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());

        // 使用特定的全局格式註冊數據轉換
        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>

Joda Time提供不一樣的類型表示日期、時間和日期時間值。JodaTimeFormatterRegistrar的dateFormatter、timeFormatter和dateTimeFormatter屬性被用於配置每種類型的不一樣格式。DateTimeFormatterFactoryBean提供一個便利的方式建立格式化器。

若是正在使用Spring MVC基於顯示的配置所使用的轉換服務。對於基於Java的@Configuration這意味着擴展WebMvcConfigurationSupport類並覆蓋mvcConversionService()方法。對於XML配置須要使用mvc:annotation-driven元素的"conversion-service"屬性。更多信息能夠查看Spring MVC的「轉換和格式化」一章。

8 Spring 驗證

Spring 3對器驗證支持引入了幾種加強。首先,JSR-303 Bean驗證API被徹底支持。其次,當使用編程方法,Spring的DataBinder能夠驗證對象同時綁定它們。最後,Spring MVC支持聲明式的驗證@Controller輸入。

8.1 JSR-303 Bean驗證API概覽

JSR-303標準化Java平臺的驗證約束聲明和元數據。使用這個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驗證器驗證,這些約束將會起做用。

查看Bean驗證的網站了解JSR-303/JSR-349更多信息。有關默認參考實現的具體功能信息,能夠查看Hibernate 驗證器文檔。瞭解如何創建一個Bean驗證提供者做爲Spring Bean,繼續向下閱讀。

8.2 配置一個Bean驗證提供者

Spring 提供了Bean驗證API的完整支持。這包括了啓動一個JSR-303/JSR-349Bean驗證提供這個做爲一個Spring bean的便利支持。在應用程序中任何須要驗證的地方容許javax.validation.ValidatorFactory或javax.validation.Validator被注入。

使用LocalValidatorFactoryBean配置一個默認的驗證器做爲Spring bean:

<bean id="validator"
    class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

上述基礎配置將觸發使用默認的啓動機制初始化Bean驗證。一個JSR-303/JSR-349提供者,例如Hibernate驗證器,須要在類路徑中被提供而且會被自動檢測。

注入一個驗證器

LocalValidatorFactoryBean實現了javax.validation.ValidatorFactory和javax.validation.Validator和Spring的org.springframework.validation.Validator接口。能夠將這些接口的引用注入到須要執行驗證邏輯的bean中。

若是偏好直接使用Bean驗證API,注入javax.validation.Validator引用:

import javax.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;

若是bean須要Spring驗證API,注入org.springframework.validation.Validator引用:

import org.springframework.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;

}

配置自定義約束

每一個Bean驗證約束有兩部分組成。首先,一個@Constraint註解聲明約束和它的配置屬性。第二,一個javax.validation.ConstraintValidator接口的實現,它實現了約束的行爲。爲了將聲明和實現關聯,每一個@Contraint註解引用一個相關的驗證約束實現類。在運行時,當域模型中遇到約束註釋時ConstraintValidatorFactory會實例化引用的實現。

默認的,LocalValidatorFactoryBean配置一個SpringConstraintValidatorFactory,它使用Spring建立約束驗證器實例。這容許自定義的約束驗證器像其餘Spring bean同樣使用依賴注入。

下面是一個自定義@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實現能夠有依賴@Autowired像其餘Spring bean同樣。

Spring驅動的方法驗證

Bean驗證1.1支持方法驗證特性,而且做爲Hibernate驗證器4.3的一個自定義擴展,能夠經過MethodValidationPostProcessor bean定義被集成進入Spring上下文:

<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>

爲了符合Spring驅動方法驗證的規範,全部目標類須要被Spring的@Validated註解註釋,可選的聲明使用的驗證組。參看MethodValidationPostProcessor的javadoc來獲取關於Hibernate驗證器和Bean驗證1.1提供者的配置信息。

額外的配置選項

默認的LocalValidatorFactoryBean配置對於大多數狀況都足夠了。但依然提供了一系列用於不一樣bean驗證構造的配置選項,從消息插值(message interpolation)到遍歷解析(traversal resolution)。查看LocalValidatorFactoryBean的javadoc獲取更多關於這些選項的信息。

8.3 配置數據綁定器

從Spring 3開始,一個DataBinder接口使用被配置爲驗證器。一旦被配置,驗證器能夠經過調用binder.validate()調用。任何驗證錯誤被自動的添加到綁定器的BindingResult。

當經過編程的方法使用DataBinder,它能夠用於在綁定到目標對象後調用驗證邏輯:

Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());

// 綁定到目標對象
binder.bind(propertyValues);

// 驗證目標對象
binder.validate();

// 獲取包含任何驗證錯誤的BindResult
BindingResult results = binder.getBindingResult();

一個DataBinder也能夠經過dataBinder.addValidators和dataBinder.replaceValidators配置多個Validator實例。當將全局配置的Bean驗證與在DataBinder實例上本地配置的Spring驗證器組合時,此功能很是有用。

8.3 Spring MVC 3驗證

查看Spring MVC一章中的「驗證」一節。

相關文章
相關標籤/搜索