開發與維運

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

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

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

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

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

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

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

Spring提供一個Validator接口,你可以使用它校驗對象。當校驗的時候,Validator接口通過使用Errors對象工作,因此校驗器可以報告校驗失敗信息到Errors對象。

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

public class Person {

    private String name;
    private int age;

    // the usual getters and setters...
}

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

  • 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,看看它除了提供前面顯示的示例外還提供什麼功能。

雖然可以實現單個驗證器類來驗證對象中的每個嵌套對象,但更好的做法是將每個嵌套對象類的驗證邏輯封裝到自己的驗證器實現中。一個“豐富”對象的簡單示例是一個由兩個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很有用。在聲明式註冊不充分的情況下它也很有用。例如,當格式化程序需要在不同於其自身的特定字段類型下進行索引時,或者在註冊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/liyong1028826685

微信公眾號:

技術交流群:

Leave a Reply

Your email address will not be published. Required fields are marked *