Spring 5 中文解析核心篇-IoC容器之數據校驗、數據綁定和類型轉換

將驗證視爲業務邏輯有其優缺點,Spring提供的驗證(和數據綁定)設計不排除其中任何一種。具體來講,驗證不該與Web層綁定,而且應該易於本地化,而且應該能夠插入任何可用的驗證器。考慮到這些問題,Spring提供了一個Validator契約,該契約既基本又能夠在應用程序的每一個層中使用。html

數據綁定對於使用戶輸入動態綁定到應用程序的域模型(或用於處理用戶輸入的任何對象)很是有用。Spring提供了恰當地命名爲DataBinder的功能。ValidatorDataBindervalidation包組成,被主要的使用但不只限於web層。java

BeanWrapper在Spring框架中是一個基本的概念而且在許多地方被使用到。然而,你大概不須要直接地使用BeanWrapper。可是,因爲這是參考文檔,因此咱們認爲可能須要一些解釋。咱們將在本章中解釋BeanWrapper,由於若是你要使用它,那麼在嘗試將數據綁定到對象時最有可能使用它。react

Spring的DataBinder和低級別BeanWrapper二者使用PropertyEditorSupport實現去解析和格式化屬性值。PropertyEditorPropertyEditorSupport類型是JavaBeans規範的一部分而且在這個章節進行解釋。Spring 3開始引入了core.convert包,該包提供了常規的類型轉換工具,以及用於格式化UI字段值的高級「 format」包。你能夠將這些包用做PropertyEditorSupport實現的更簡單替代方案。這些也會在這個章節討論。git

Spring經過安裝基礎設計和適配Spring的Validator契約提供JavaBean校驗。應用程序能夠全局一次啓用Bean驗證,像在JavaBean校驗中描述同樣,而且僅將其用於全部驗證需求。在Web層中,應用程序能夠每一個DataBinder進一步註冊控制器本地的Spring Validator實例,如配置DataBinder中所述,這對於插入自定義驗證邏輯頗有用。github

3.1 經過使用Spring的校驗接口校驗

Spring提供一個Validator接口,你可使用它校驗對象。當校驗的時候,Validator接口經過使用Errors對象工做,所以校驗器能夠報告校驗失敗信息到Errors對象。web

考慮下面小數據對象例子:spring

public class Person {

    private String name;
    private int age;

    // the usual getters and setters...
}

下面例子經過實現下面org.springframework.validation.Validator接口的兩個方法爲Person類提供校驗行爲。express

  • supports(Class): Validator校驗接口是否支持Class
  • validate(Object, org.springframework.validation.Errors): 驗證給定的對象,並在發生驗證錯誤的狀況下,使用給定的Errors對象註冊這些對象。

實現Validator很是簡單,特別地當你知道Spring框架提供的ValidationUtils幫助類時。下面例子爲Person接口實現Validator編程

public class PersonValidator implements Validator {

    /**
     * This Validator validates only 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,看看它除了提供前面顯示的示例外還提供什麼功能。api

雖然能夠實現單個驗證器類來驗證對象中的每一個嵌套對象,但更好的作法是將每一個嵌套對象類的驗證邏輯封裝到本身的驗證器實現中。一個「豐富」對象的簡單示例是一個由兩個String屬性(第一個和第二個名字)和一個複雜的Address對象組成的CustomerAddress對象能夠獨立於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/>標籤去檢查錯誤信息,可是你也能夠本身檢查Errors對象。更多關於提供的信息在Javadoc中。

參考代碼: com.liyong.ioccontainer.service.validator.ValidatorTest
3.2 解析碼到錯誤信息

咱們介紹了數據綁定和校驗。本節介紹與驗證錯誤對應的輸出消息。在上一節顯示的例子中,咱們拒絕nameage字段。若是咱們想使用MessageSource去輸出錯誤信息,咱們可使用提供的錯誤碼,當拒絕字段時(在這個場景中nameage)。當你Errors接口調用(直接地或間接地,經過使用ValidationUtils類)rejectValue或其餘reject方法之一時,底層的實現不只註冊你傳遞的碼,並且還註冊一些附加的錯誤碼。MessageCodesResolver肯定哪個錯誤碼註冊到Errors接口。默認狀況下,使用DefaultMessageCodesResolver,它(例如)不只使用你提供的代碼註冊消息,並且還註冊包含傳遞給拒絕方法的字段名稱的消息。所以,若是你經過使用rejectValue(「age」,「too.darn.old」)拒絕字段,則除了too.darn.old代碼外,Spring還將註冊too.darn.old.agetoo.darn.old.age.int(第一個包含字段名稱,第二個包含字段類型)。這樣作是爲了方便開發人員在定位錯誤消息時提供幫助。

更多MessageCodesResolver上和默認策略信息能夠分別地在MessageCodesResolverDefaultMessageCodesResolver javadoc中找到。

3.3 bean操做和BeanWrapper

這個org.springframework.beans包遵循JavaBeans標準。JavaBean是具備默認無參數構造函數的類,而且遵循命名約定,在該命名約定下,例如:名爲bingoMadness的屬性將具備setter方法setBingoMadness(..)getter方法getBingoMadness()。更多關於JavaBean信息和規範,查看javaBeans

beans包中一個很是重要的類是BeanWrapper接口和它的對應實現(BeanWrapperImpl)。就像從Javadoc引言的那樣,BeanWrapper提供瞭如下功能:設置和獲取屬性值(單獨或批量),獲取屬性描述符以及查詢屬性以肯定它們是否可讀或可寫。此外,BeanWrapper還支持嵌套屬性,從而能夠將子屬性上的屬性設置爲無限深度。BeanWrapper還支持添加標準JavaBeans 的PropertyChangeListenersVetoableChangeListeners的功能,而無需在目標類中支持代碼。最後但並不是不重要的一點是,BeanWrapper支持設置索引屬性。BeanWrapper一般不直接由應用程序代碼使用,而是由DataBinderBeanFactory使用。

BeanWrapper的工做方式部分由其名稱表示:它包裝一個Bean,以對該Bean執行操做,例如設置和檢索屬性。

3.3.1 設置和獲取基本的和潛入的屬性

設置和獲取屬性是經過BeanWrapper的重載方法setPropertyValuegetPropertyValue的變體。查看它們的詳細文檔。下面的表格顯示這些約定:

Expression Explanation
name 表示屬性name對應的getName()isName()setName(..)方法。
account.name 表示嵌入account屬性的name屬性對應的getAccount().setName()getAccount().getName()方法
account[2] 表示2個索引元素屬性account。索引屬性能夠是類型arraylist或其餘天然順序集合。
account[COMPANYNAME] 表示map實體的值經過account Map屬性的key COMPANYNAME索引。

(若是你沒打算直接使用BeanWrapper,下面部分不是相當重要地。若是你僅僅使用DataBinderBeanFactory和他的默認實現,你能夠跳過PropertyEditors的部分)。

下面兩個例子類使用BeanWrapper去獲取和設置屬性:

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

如下代碼段顯示了一些有關如何檢索和操縱實例化的CompanyEmployee的某些屬性的示例:

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");
代碼示例: com.liyong.ioccontainer.service.beanwrapper.BeanWrapperTest
3.3.2 內建PropertyEditor實現

Spring使用PropertyEditor概念去影響一個對象和字符串之間的轉換。以不一樣於對象自己的方式表示屬性可能很方便。例如,日期能夠用人類可讀的方式表示(如字符串:'2007-14-09'),而咱們仍然能夠將人類可讀的形式轉換回原始日期(或者更好的是,轉換任何日期以人類可讀的形式輸入到Date對象)。經過註冊類型爲java.beans.PropertyEditor的自定義編輯器,能夠實現此行爲。在BeanWrapper上或在特定的IoC容器中註冊自定義編輯器(如上一章所述),使它具備如何將屬性轉換爲所需類型的能力。更多關於PropertyEditor請參閱Oracle的java.beans包的javadoc

在Spring中使用屬性編輯的兩個示例:

  • 經過使用PropertyEditor實如今bean上設置屬性。當使用String做爲在XML文件中聲明的某個bean的屬性的值時,Spring(若是相應屬性的setter具備Class參數)將使用ClassEditor嘗試將參數解析爲Class對象。
  • 在Spring的MVC框架中,經過使用各類PropertyEditor實現來解析HTTP請求參數,你能夠在CommandController的全部子類中手動綁定這些實現。

Spring有一個內建的PropertyEditor實現。它們都位於org.springframework.beans.propertyeditors包中。默認狀況下,大多數(但不是所有,以下表所示)由BeanWrapperImpl註冊。若是能夠經過某種方式配置屬性編輯器,則仍能夠註冊本身的變體以覆蓋默認變體。

下表描述了Spring提供的各類PropertyEditor實現:

Class Explanation
ByteArrayPropertyEditor 字節數組的編輯器。將字符串轉換爲其相應的字節表示形式。默認 BeanWrapperImpl註冊。
ClassEditor 將表明類的字符串解析爲實際類,反之亦然。當類沒有找到拋出IllegalArgumentException。默認 BeanWrapperImpl註冊。
CustomBooleanEditor Boolean屬性的可定製屬性編輯器。默認,經過BeanWrapperImpl註冊,可是能夠經過將其自定義實例註冊爲自定義編輯器來覆蓋它。
CustomCollectionEditor 集合屬性編輯器,轉換任何源Collection到給定Collection類型。
CustomDateEditor java.util.Date的可自定義屬性編輯器,支持一個自定義DateFormat。默認不會被註冊。必須根據須要以適當的格式進行用戶註冊。
CustomNumberEditor 任何Number子類可自定義屬性編輯器,例如IntegerLongFloatDouble。默認,經過BeanWrapperImpl註冊,可是能夠經過將其自定義實例註冊爲自定義編輯器來覆蓋它。
FileEditor 解析字符串爲java.io.File對象。默認,經過BeanWrapperImpl註冊。
InputStreamEditor 單向屬性編輯器,它能夠採用字符串並生成(經過中間的ResourceEditorResource)一個InputStream,以即可以將InputStream屬性直接設置爲字符串。請注意,默認用法不會爲你關閉InputStream。默認狀況下,由BeanWrapperImpl註冊
LocaleEditor 能夠將字符串解析爲Locale對象,反之亦然(字符串格式爲[country] [variant],相似Locale的toString()方法相同)。默認,經過BeanWrapperImpl註冊
PatternEditor 可以解析字符串爲java.util.regex.Pattern對象,反之亦然。
PropertiesEditor 能夠將字符串(格式設置爲java.util.Properties類的javadoc中定義的格式)轉換爲Properties對象
StringTrimmerEditor 修剪字符串的屬性編輯器。 (可選)容許將空字符串轉換爲空值。默認不被註冊-必須被用戶註冊。
URLEditor 可以轉換一個字符串表明的URL爲真實的URL對象。默認,經過BeanWrapperImpl註冊。

Spring使用java.beans.PropertyEditorManager去設置屬性編輯器可能須要的搜索路徑。搜索路徑也能夠包含sun.bean.editors,它包括例如FontColor和大多數原始類型的PropertyEditor實現。還要注意,若是標準JavaBeans基礎結構與它們處理的類在同一包中而且與該類具備相同的名稱,而且附加了Editor,則標準JavaBeans基礎結構會自動發現PropertyEditor類(無需顯式註冊它們)。例如,可使用如下類和包結構,這就足以識別SomethingEditor類並將其用做某種類型屬性的PropertyEditor

com
  chank
    pop
      Something
      SomethingEditor // SomethingEditor用做Something類

注意,你也能夠在此處使用標準的BeanInfo JavaBeans機制(這裏有所描述)。下面例子使用BeanInfo機制去明確地註冊一個或多個PropertyEditor實例到關聯類的屬性:

com
  chank
    pop
      Something
      SomethingBeanInfo // BeanInfo用做Something類

下面是引用的SomethingBeanInfo類的Java源代碼,它將CustomNumberEditorSomething類的age屬性關聯起來:

public class SomethingBeanInfo extends SimpleBeanInfo {

    public PropertyDescriptor[] getPropertyDescriptors() {
        try {
            final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
            PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) {
                public PropertyEditor createPropertyEditor(Object bean) {
                    return numberPE;
                };
            };
            return new PropertyDescriptor[] { ageDescriptor };
        }
        catch (IntrospectionException ex) {
            throw new Error(ex.toString());
        }
    }
}
參考代碼: com.liyong.ioccontainer.service.propertyeditor.PropertyEditorTest

註冊附加的自定義PropertyEditor實現

當設置bean屬性爲字符串值時,Spring IoC容器最終地使用標準JavaBean的PropertyEditor實現去轉換這些字符串爲屬性的複雜類型。Spring預註冊了很是多的自定義PropertyEditor實現(例如,將表示爲字符串的類名稱轉換爲Class對象)。此外,Java的標準JavaBeans PropertyEditor查找機制容許適當地命名類的PropertyEditor,並將其與提供支持的類放在同一包中,以即可以自動找到它。

若是須要註冊其餘自定義PropertyEditors,則可使用幾種機制。最手動的方法(一般不方便或不建議使用)是使用ConfigurableBeanFactory接口的registerCustomEditor()方法,假設你有BeanFactory引用。另外一種(稍微方便些)的機制是使用稱爲CustomEditorConfigurer的特殊bean工廠後處理器。儘管你能夠將Bean工廠後處理器與BeanFactory實現一塊兒使用,但CustomEditorConfigurer具備嵌套的屬性設置,所以咱們強烈建議你將其與ApplicationContext一塊兒使用,在這裏能夠將其以與其餘任何Bean類似的方式進行部署,而且能夠在任何位置進行部署。自動檢測並應用。

請注意,全部的bean工廠和應用程序上下文經過使用BeanWrapper來處理屬性轉換,都會自動使用許多內置的屬性編輯器。上一節列出了BeanWrapper註冊的標準屬性編輯器。此外,ApplicationContext還以適合特定應用程序上下文類型的方式重寫或添加其餘編輯器,以處理資源查找。

標準JavaBeans PropertyEditor實例用於將表示爲字符串的屬性值轉換爲屬性的實際複雜類型。你可使用bean工廠的後處理器CustomEditorConfigurer來方便地將對其餘PropertyEditor實例的支持添加到ApplicationContext中。

考慮如下示例,該示例定義了一個名爲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;
    }
}

正確設置以後,咱們但願可以將type屬性分配爲字符串,PropertyEditor會將其轉換爲實際的ExoticType實例。如下bean定義顯示瞭如何創建這種關係:

<bean id="sample" class="example.DependsOnExoticType">
    <property name="type" value="aNameForExoticType"/>
</bean>

PropertyEditor實現可能相似於如下內容:

// converts string representation to ExoticType object
package example;

public class ExoticTypeEditor extends PropertyEditorSupport {

    public void setAsText(String text) {
        setValue(new ExoticType(text.toUpperCase()));
    }
}

最後,下面的示例演示如何使用CustomEditorConfigurerApplicationContext註冊新的PropertyEditor,而後能夠根據須要使用它:

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="customEditors">
        <map>
            <entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
        </map>
    </property>
</bean>
參考代碼: com.liyong.ioccontainer.starter.PropertyEditorIocContainer

使用PropertyEditorRegistrar

在Spring容器中註冊屬性編輯器的其餘機制是建立和使用PropertyEditorRegistrar。當須要在幾種不一樣狀況下使用同一組屬性編輯器時,此接口特別有用。你能夠在每一種場景中寫對應的註冊和從新使用。PropertyEditorRegistrar實例與一個名爲PropertyEditorRegistry的接口一塊兒工做,該接口由Spring BeanWrapper(和DataBinder)實現。與CustomEditorConfigurer(在此描述)結合使用時,PropertyEditorRegistrar實例特別方便,該實例暴露了名爲setPropertyEditorRegistrars(..)的屬性。以這種方式添加到CustomEditorConfigurer中的PropertyEditorRegistrar實例能夠輕鬆地與DataBinder和Spring MVC控制器共享。此外,它避免了在自定義編輯器上進行同步的需求:但願PropertyEditorRegistrar爲每次建立bean的嘗試建立新的PropertyEditor實例。

如下示例說明如何建立本身的PropertyEditorRegistrar實現:

package com.foo.editors.spring;

public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {

    public void registerCustomEditors(PropertyEditorRegistry registry) {

        // 指望建立一個新的PropertyEditor示例
        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並將其注入咱們的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);
    }

    // other methods to do with registering a User
}

這種PropertyEditor註冊樣式可使代碼簡潔(initBinder(..)的實現只有一行長),而且能夠將通用的PropertyEditor註冊代碼封裝在一個類中,而後根據須要在許多Controller之間共享。

3.4 Spring類型轉換

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

3.4.1 轉換SPI

如如下接口定義所示,用於實現類型轉換邏輯的SPI很是簡單且具備強類型:

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

   T convert(S source);
}

要建立本身的轉換器,請實現Converter接口,並將S設置爲要被轉換的類型,並將T設置爲要轉換爲的類型。若是須要將S的集合或數組轉換爲T的集合,而且已經註冊了委託數組或集合轉換器(默認狀況下,DefaultConversionService會這樣作),那麼你還能夠透明地應用這樣的轉換器。

對於每次convert(S)的調用,方法參數必須保證不能爲null。若是轉換失敗,你的Converter可能拋出未檢查異常。特別地,它可能拋出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);
    }
}
3.4.2 使用ConverterFactory

當須要集中整個類層次結構的轉換邏輯時(例如,從String轉換爲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>),其中TR的子類。

考慮StringToEnumConverterFactory例子:

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());
        }
    }
}
3.4.3 使用GenericConverter

當你須要複雜的Converter實現時,請考慮使用GenericConverter接口。與Converter相比,GenericConverter具備比Converter更靈活但類型不強的簽名,支持多種源類型和目標類型之間進行轉換。此外,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接口,所以僅應在須要時使用它。支持 ConverterConverterFactory以知足基本的類型轉換需求。

參考代碼:com.liyong.ioccontainer.service.converter.GenericConverterTest

使用ConditionalGenericConverter

有時,你但願Converter僅在知足特定條件時才運行。例如,你可能只想在目標字段上存在特定註解時才運行Converter,或者可能在目標類上定義了特定方法(例如靜態valueOf方法)時才運行ConverterConditionalGenericConverterGenericConverterConditionalConverter接口的聯合,可以讓你定義如下自定義匹配條件:

public interface ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

ConditionalGenericConverter的一個很好的例子是EntityConverter,它在持久實體標識和實體引用之間轉換。僅當目標實體類型聲明靜態查找器方法(例如findAccount(Long))時,此類EntityConverter纔可能匹配。你能夠在matchs(TypeDescriptor,TypeDescriptor)的實現中執行這種finder方法檢查。

參考代碼: com.liyong.ioccontainer.service.converter.ConditionalConverterTest
3.4.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實現委派其註冊的轉換器執行類型轉換邏輯。

core.convert.support包中提供了一個強大的ConversionService實現。GenericConversionService是適用於大多數環境的通用實現。ConversionServiceFactory提供了一個方便的工廠來建立通用的ConversionService配置。

3.4.5 配置ConversionService

ConversionService是無狀態對象,旨在在應用程序啓動時實例化,而後在多個線程之間共享。在Spring應用程序中,一般爲每一個Spring容器(或ApplicationContext)配置一個ConversionService實例。當框架須要執行類型轉換時,Spring會使用該ConversionService並使用它。你還能夠將此ConversionService注入到任何bean中,而後直接調用它。

若是沒有向Spring註冊 ConversionService,則使用原始的基於 propertyeditor的特性。

要向Spring註冊默認的ConversionService,請添加如下bean定義,其id爲conversionService

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

默認的ConversionService能夠在字符串、數字、枚舉、集合、映射和其餘常見類型之間進行轉換。要用你本身的自定義轉換器補充或覆蓋默認轉換器,請設置converters屬性。屬性值能夠實現ConverterConverterFactoryGenericConverter接口中的任何一個。

<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一章中的轉換和格式化

在某些狀況下,你可能但願在轉換過程當中應用格式設置。有關使用FormattingConversionServiceFactoryBean的詳細信息,請參見FormatterRegistry SPI

3.4.6 編程式地使用ConversionService

要以編程方式使用ConversionService實例,能夠像對其餘任何bean同樣注入對該bean例的引用。如下示例顯示瞭如何執行此操做:

@Service
public class MyService {

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

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

對於大多數用例,可使用指定targetTypeconvert方法,但不適用於更復雜的類型,例如參數化元素的集合。例如,若是要以編程方式將整數列表轉換爲字符串列表,則須要提供源類型和目標類型的格式定義。

幸運的是,以下面的示例所示,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的集合轉換爲T的集合。

3.5 Spring字段格式

如上一節所述,core.convert是一種通用類型轉換系統。它提供了統一的ConversionService API和強類型的Converter SPI,用於實現從一種類型到另外一種類型的轉換邏輯。Spring容器使用此係統綁定bean屬性值。此外,Spring Expression Language(SpEL)和DataBinder都使用此係統綁定字段值。例如,當SpEL須要強制將Short轉換爲Long來完成expression.setValue(Object bean,Object value)嘗試時,core.convert系統將執行強制轉換。

考慮一個典型的客戶端環境轉換需求,例如web或桌面應用。在這種環境中,你一般將字符串轉換爲支持客戶端提交處理,以及將字符串轉換爲支持視圖呈現過程。以及,你一般須要本地化String值。更通用的core.convert Converter SPI不能直接知足此類格式化要求。爲了直接解決這些問題,Spring 3 引入了方便的Formatter SPI,它爲客戶端環境提供了PropertyEditor實現的簡單而強大的替代方案。

一般,當你須要實現通用類型轉換邏輯時,可使用Converter SPI,例如,在java.util.DateLong之間轉換。當你在客戶端環境中(例如,web應用)而且須要去解析和打印本地化字段值時,你可使用Formatter SPI。ConversionService爲這兩個SPI提供統一的類型轉換。

3.5.1 Formatter SPI

Formatter SPI去實現字段格式邏輯是簡單和強類型的。下面清單顯示Formatter接口信息:

package org.springframework.format;

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

FormatterPrinterParser構建塊接口拓展。下面清單顯示這兩個接口定義:

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應該拋一個ParseExceptionIllegalArgumentException異常。注意確保你的Formatter實現是線程安全的。

爲了方便format子包提供一些Formatter實現。number包提供NumberStyleFormatterCurrencyStyleFormatterPercentStyleFormatter去格式化Number對象,它使用java.text.NumberFormatdatetime包提供DateFormatter去格式化java.util.Datejava.text.DateFormat對象。datetime.joda包基於Joda-Time庫提供了全面的日期時間格式支持。

下面DateFormatterFormatter實現例子:

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歡迎社區驅動Formatter貢獻。查看GitHub Issues去貢獻。

3.5.2 註解驅動格式

能夠經過字段類型或註解配置字段格式。要將註解綁定到Formatter,請實現AnnotationFormatterFactory。下面清單顯示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參數化爲要與格式邏輯關聯的字段annotationType,例如,org.springframework.format.annotation.DateTimeFormatgetFieldTypes()返回可在其上使用註解的字段類型。讓getPrinter()返回Printer以打印帶註解的字段的值。讓getParser()返回Parser去爲註解字段解析clientValue

下面的示例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 NumberStyleFormatter(annotation.pattern());
        } else {
            Style style = annotation.style();
            if (style == Style.PERCENT) {
                return new PercentStyleFormatter();
            } else if (style == Style.CURRENCY) {
                return new CurrencyStyleFormatter();
            } else {
                return new NumberStyleFormatter();
            }
        }
    }
}

觸發格式,可使用@NumberFormat註解字段,如如下示例所示:

public class MyModel {
    @NumberFormat(style=Style.CURRENCY)
    private BigDecimal decimal;
}

格式註解API

org.springframework.format.annotation包中存在一個可移植的格式註解API。你可使用@NumberFormat格式化Number字段(例如DoubleLong),並使用@DateTimeFormat格式化java.util.Datejava.util.CalendarLong(用於毫秒時間戳)以及JSR-310 java.timeJoda-Time值類型。

下面例子使用@DateTimeFormat去格式java.util.Date爲ISO日期(yyyy-MM-dd);

public class MyModel {

    @DateTimeFormat(iso=ISO.DATE)
    private Date date;
}
3.5.3 FormatterRegistry SPI

FormatterRegistry是一個SPI用於註冊格式化器和轉換器。FormattingConversionServiceFormatterRegistry實現適用於絕大環境。經過使用FormattingConversionServiceFactoryBean,你能夠編程式地或聲明式配置這些變體做爲Spring bean。因爲此實現還實現了ConversionService,所以你能夠直接將其配置爲與Spring的DataBinder和Spring表達式語言(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);
}

像在前面清單顯示,你經過字段類型或經過註解註冊格式化器。

FormatterRegistry SPI使你能夠集中配置格式設置規則,而沒必要在控制器之間重複此類配置。例如,你可能要強制全部日期字段以某種方式設置格式或帶有特定註解的字段以某種方式設置格式。使用共享的FormatterRegistry,你能夠一次定義這些規則,並在須要格式化時應用它們。

3.5.4 FormatterRegistrar SPI

FormatterRegistrar是一個SPI,用於經過FormatterRegistry註冊格式器和轉換器。如下清單顯示了其接口定義:

package org.springframework.format;

public interface FormatterRegistrar {

    void registerFormatters(FormatterRegistry registry);
}

爲給定的格式類別(例如日期格式)註冊多個相關的轉換器和格式器時,FormatterRegistrar頗有用。在聲明式註冊不充分的狀況下它也頗有用。例如,當格式化程序須要在不一樣於其自身<T>的特定字段類型下進行索引時,或者在註冊Printer/Parser對時。下一節將提供有關轉換器和格式化註冊的更多信息。

3.5.5 在Spring MVC中配置格式化

在Spring MVC章節中,查看 Conversion 和 Formatting

3.6 配置全局DateTime格式

默認狀況下,未使用@DateTimeFormat註解日期和時間字段是使用DateFormat.SHORT格式從字符串轉換的。若是願意,能夠經過定義本身的全局格式來更改此設置。

爲此,請確保Spring不註冊默認格式器。相反,能夠藉助如下方法手動註冊格式化器:

  • org.springframework.format.datetime.standard.DateTimeFormatterRegistrar
  • org.springframework.format.datetime.DateFormatterRegistrar或爲Joda-Timeorg.springframework.format.datetime.joda.JodaTimeFormatterRegistrar

例如,下面Java配置註冊一個全局的yyyyMMdd格式:

@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 JSR-310 date conversion with a specific global format
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd"));
        registrar.registerFormatters(conversionService);

        // 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
        https://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>

注意:當在web應用中配置日期和時間格式時須要額外考慮。請查看 WebMVC Conversion 和 Formatting or WebFlux Conversion 和 Formatting.

3.7 Java Bean校驗

Spring框架提供對Java Bean校驗API。

3.7.1 Bean校驗概要

Bean驗證爲Java應用程序提供了經過約束聲明和元數據進行驗證的通用方法。要使用它,你須要使用聲明性驗證約束對域模型屬性進行註解,而後由經過運行時強制實施約束。有內置的約束,你也能夠定義本身的自定義約束。

考慮如下示例,該示例顯示了具備兩個屬性的簡單PersonForm模型:

public class PersonForm {
    private String name;
    private int age;
}

Bean驗證使你能夠聲明約束,如如下示例所示:

public class PersonForm {

    @NotNull
    @Size(max=64)
    private String name;

    @Min(0)
    private int age;
}

而後,Bean驗證器根據聲明的約束來驗證此類的實例。有關該API的通常信息,請參見Bean Validation。有關特定限制,請參見Hibernate Validator文檔。要學習如何將bean驗證提供程序設置爲Spring bean,請繼續閱讀。

3.7.2 配置Bean Validation提供者

Spring提供了對Bean驗證API的全面支持,包括將Bean驗證提供程序做爲Spring Bean執行引導。這使你能夠在應用程序中須要驗證的任何地方注入javax.validation.ValidatorFactoryjavax.validation.Validator

你可使用LocalValidatorFactoryBean將默認的Validator配置爲Spring Bean,如如下示例所示:

import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class AppConfig {

    @Bean
    public LocalValidatorFactoryBean validator() {
        return new LocalValidatorFactoryBean;
    }
}

前面示例中的基本配置觸發Bean驗證以使用其默認引導機制進行初始化。Bean驗證提供程序,例如Hibernate Validator,應該存在於類路徑中並被自動檢測到。

注入校驗器

LocalValidatorFactoryBean同時實現javax.validation.ValidatorFactoryjavax.validation.Validator以及Spring的org.springframework.validation.Validator。你能夠將對這些接口之一的引用注入須要調用驗證邏輯的bean中。

若是你但願直接使用Bean Validation API,則能夠注入對javax.validation.Validator的引用,如如下示例所示:

import javax.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;
}

配置自定義約束

每一個bean校驗約束由兩部分組成:

  • @Constraint註解,用於聲明約束及其可配置屬性。
  • javax.validation.ConstraintValidator接口的實現,用於實現約束的行爲。

要將聲明與實現相關聯,每一個@Constraint註解都引用一個對應的ConstraintValidator實現類。在運行時,當在域模型中遇到約束註解時,ConstraintValidatorFactory實例化引用的實現。

默認狀況下,LocalValidatorFactoryBean配置一個SpringConstraintValidatorFactory,該工廠使用Spring建立ConstraintValidator實例。這使你的自定義ConstraintValidators像其餘任何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實現能夠像其餘任何Spring bean同樣具備@Autowired依賴項。

參考代碼: com.liyong.ioccontainer.service.validator.ConstraintTest

Spring驅動方法驗證

你能夠經過MethodValidationPostProcessor bean定義將Bean Validation 1.1(以及做爲自定義擴展,還包括Hibernate Validator 4.3)支持的方法驗證功能集成到Spring上下文中:

import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;

@Configuration

public class AppConfig {

    @Bean
    public MethodValidationPostProcessor validationPostProcessor() {
        return new MethodValidationPostProcessor;
    }
}

爲了有資格進行Spring驅動的方法驗證,全部目標類都必須使用Spring的@Validated註解進行註釋,該註解也能夠選擇聲明要使用的驗證組。有關使用Hibernate ValidatorBean Validation 1.1提供程序的設置詳細信息,請參見MethodValidationPostProcessor

方法驗證依賴於目標類周圍的AOP代理,即接口上方法的JDK動態代理或CGLIB代理。代理的使用存在某些限制,在 理解 AOP 代理中介紹了其中的一些限制。另外,請記住在代理類上使用方法和訪問器;直接訪問將不起做用。

參考代碼:com.liyong.ioccontainer.starter.MethodvalidationIocContainer

其餘配置選項

在大多數狀況下,默認LocalValidatorFactoryBean配置就足夠了。從消息插值到遍歷解析,有多種用於各類Bean驗證構造的配置選項。有關這些選項的更多信息,請參見LocalValidatorFactoryBean Javadoc。

3.7.3 配置DataBinder

從Spring 3 開始,你可使用Validator配置DataBinder實例。配置完成後,你能夠經過調用binder.validate()來調用Validator。任何驗證錯誤都會自動添加到綁定的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.addValidatorsdataBinder.replaceValidators配置具備多個Validator實例的DataBinder。當將全局配置的bean驗證與在DataBinder實例上本地配置的Spring Validator結合使用時,這頗有用。查看Spring MVC 校驗配置

參考代碼: com.liyong.ioccontainer.service.validator.ValidatorTest
3.7.4 Spring MVC 3 校驗

在Sprint MVC 章節中,查看Validation

做者

我的從事金融行業,就任過易極付、思建科技、某網約車平臺等重慶一流技術團隊,目前就任於某銀行負責統一支付系統建設。自身對金融行業有強烈的愛好。同時也實踐大數據、數據存儲、自動化集成和部署、分佈式微服務、響應式編程、人工智能等領域。同時也熱衷於技術分享創立公衆號和博客站點對知識體系進行分享。關注公衆號: 青年IT男 獲取最新技術文章推送!

博客地址: http://youngitman.tech

CSDN: https://blog.csdn.net/liyong1...

微信公衆號:

技術交流羣:

相關文章
相關標籤/搜索