1. 程式人生 > >spring5-驗證、資料繫結和型別轉換

spring5-驗證、資料繫結和型別轉換

5.1 介紹 {#toc_1}

JSR-303/JSR-349 Bean Validation

在設定支援方面,Spring Framework 4.0支援Bean Validation 1.0(JSR-303)和Bean Validation 1.1(JSR-349),也將其改寫成了Spring的Validator介面。

正如5.8 Spring驗證所述,應用程式可以選擇一次性全域性啟用Bean驗證,並使其專門用於所有的驗證需求。

正如5.8.3 配置DataBinder所述,應用程式也可以為每個DataBinder例項註冊額外的SpringValidator例項,這可能有助於不通過使用註解而插入驗證邏輯。

考慮將驗證作為業務邏輯是有利有弊的,Spring提供了一種不排除利弊的用於驗證(和資料繫結)的設計。具體的驗證不應該捆綁在web層,應該容易本地化並且它應該能夠插入任何可用的驗證器。考慮到以上這些,Spring想出了一個Validator介面,它在應用程式的每一層基本都是可用的。資料繫結對於將使用者輸入動態繫結到應用程式的領域模型上(或者任何你用於處理使用者輸入的物件)是非常有用的。Spring提供了所謂的DataBinder來處理這個。ValidatorDataBinder組成了validation包,其主要用於但並不侷限於MVC框架。

BeanWrapper是Spring框架中的一個基本概念且在很多地方使用。然而,你可能並不需要直接使用BeanWrapper

。儘管這是參考文件,我們仍然覺得有一些說明需要一步步來。我們將會在本章中解釋BeanWrapper,因為你極有可能會在嘗試將資料繫結到物件的時候使用它。

Spring的DataBinder和底層的BeanWrapper都使用PropertyEditor來解析和格式化屬性值。PropertyEditor概念是JavaBeans規範的一部分,並會在本章進行說明。Spring 3不僅引入了”core.convert”包來提供一套通用型別轉換工具,還有一個高層次的”format”包用於格式化UI欄位值。可以將這些新包視作更簡單的PropertyEditor替代方式來使用,本章還會對此進行討論。

5.2 使用Spring的驗證器介面進行驗證 {#toc_2}

Spring具有一個Validator介面可以讓你用於驗證物件。Validator介面在工作時需要使用一個Errors物件,以便於在驗證過程中,驗證器可以將驗證失敗的資訊報告給這個Errors物件。

讓我們考慮一個小的資料物件:

public class Person {

    private String name;
    private int age;

    // the usual getters and setters...
}

通過實現org.springframework.validation.Validator的下列兩個介面,我們打算為Person類提供驗證行為:

  • support(Class)– 這個Validator是否可以驗證給定Class的例項validate(Object,org.springframework.validation.Errors)
  • – 驗證給定的物件並且萬一驗證錯誤,可以將這些錯誤註冊到給定的Errors物件

實現一個Validator是相當簡單的,特別是當你知道Spring框架還提供了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(..)被用於拒絕那些值為null或者空字串的'name'屬性。除了上面展示的例子之外,去看一看ValidationUtils的java文件有助於瞭解它提供的功能。

通過實現單個的Validator類來逐個驗證富物件中的巢狀物件當然是有可能的,然而將驗證邏輯封裝在每個巢狀類物件自身的Validator實現中可能是一種更好的選擇。Customer就是一個‘富’物件的簡單示例,它由兩個字串屬性(姓和名)以及一個複雜物件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/>標籤來檢查錯誤資訊,不過當然你也可以自己檢查錯誤物件。有關它提供的方法的更多資訊可以在java文件中找到。

5.3 將程式碼解析成錯誤訊息 {#toc_3}

在之前我們已經談論了資料繫結和驗證,最後一件值得討論的事情是輸出對應於驗證錯誤的訊息。在我們上面展示的例子裡,我們拒絕了nameage欄位。如果我們要使用MessageSource來輸出錯誤訊息,我們將會使用我們在拒絕該欄位(這個情況下是’姓名’和’年齡’)時給出的錯誤程式碼。當你呼叫(不管是直接呼叫還是間接通過使用ValidationUtils類呼叫)來自Errors介面的rejectValue或者其他reject方法時,其底層實現不僅會註冊你傳入的程式碼,還會註冊一些額外的錯誤程式碼。註冊怎樣的錯誤程式碼取決於它所使用的MessageCodesResolver,預設情況下,會使用DefaultMessageCodesResolver,其不僅會使用你提供的程式碼註冊訊息,還會註冊包含你傳遞給拒絕方法的欄位名稱的訊息。所以如果你使用rejectValue("age", "too.darn.old")來拒絕一個欄位,除了too.darn.old程式碼,Spring還會註冊too.darn.old.agetoo.darn.old.age.int(第一個會包含欄位名稱且第二個會包含欄位型別)。這樣做是為了方便開發人員來定位錯誤訊息等。

有關MessageCodesResolver和其預設策略的更多資訊可以分別在MessageCodesResolver以及DefaultMessageCodesResolver的線上java文件中找到

5.4 Bean操作和BeanWrapper {#toc_4}

org.springframework.beans包遵循Oracle提供的JavaBeans標準。一個JavaBean只是一個包含預設無參構造器的類,它遵循一個命名約定(通過一個例子):一個名為bingoMadness屬性將有一個設定方法setBingoMadness(..)和一個獲取方法getBingoMadness(..)。有關JavaBeans和其規範的更多資訊,請參考Oracle的網站(javabeans)。

beans包裡一個非常重要的類是BeanWrapper介面和它的相應實現(BeanWrapperImpl)。引用自java文件,BeanWrapper提供了設定和獲取屬性值(單獨或批量)、獲取屬性描述符以及查詢屬性以確定它們是可讀還是可寫的功能。BeanWrapper還提供對巢狀屬性的支援,能夠不受巢狀深度的限制啟用子屬性的屬性設定。然後,BeanWrapper提供了無需目標類程式碼的支援就能夠新增標準JavaBeans的PropertyChangeListenersVetoableChangeListeners的能力。最後然而並非最不重要的是,BeanWrapper提供了對索引屬性設定的支援。BeanWrapper通常不會被應用程式的程式碼直接使用,而是由DataBinderBeanFactory使用。

BeanWrapper的名字已經部分暗示了它的工作方式:它包裝一個bean以對其執行操作,比如設定和獲取屬性。

5.4.1 設定並獲取基本和巢狀屬性 {#toc_5}

使用setPropertyValue(s)getPropertyValue(s)可以設定並獲取屬性,兩者都帶有幾個過載方法。在Spring自帶的java文件中對它們有更詳細的描述。重要的是要知道物件屬性指示的幾個約定。幾個例子:

表 5.1. 屬性示例

表示式 說明
name 表示屬性name與方法getName()isName()setName()相對應
account.name 表示屬性account的巢狀屬性name與方法getAccount().setName()getAccount().getName()相對應
account[2] 表示索引屬性account的第三個元素。索引屬性可以是arraylist或其他自然排序的集合
account[COMPANYNAME] 表示對映屬性account被鍵COMPANYNAME索引到的對映項的值

下面你會發現一些使用BeanWrapper來獲取和設定屬性的例子。

(如果你不打算直接使用BeanWrapper,那麼下一部分對你來說並不重要。如果你僅使用DataBinderBeanFactory以及它們開箱即用的實現,你應該跳到關於PropertyEditor部分的開頭)。

考慮下面兩個類:

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

以下的程式碼片段展示瞭如何檢索和操縱例項化的CompaniesEmployees的某些屬性:

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");

5.4.2 內建PropertyEditor實現 {#toc_6}

Spring使用PropertyEditor的概念來實現ObjectString之間的轉換。如果你考慮到它,有時候換另一種方式表示屬性可能比物件本身更方便。舉個例子,一個Date可以以人類可讀的方式表示(如String'2007-14-09'),同時我們依然能把人類可讀的形式轉換回原始的時間(甚至可能更好:將任何以人類可讀形式輸入的時間轉換回Date物件)。這種行為可以通過註冊型別為PropertyEditor的自定義編輯器來實現。在BeanWrapper或上一章提到的特定IoC容器中註冊自定義編輯器,可以使其瞭解如何將屬性轉換為期望的型別。請閱讀Oracle為java.beans包提供的java文件來獲取更多關於PropertyEditor的資訊。

這是Spring使用屬性編輯的兩個例子:

  • 使用PropertyEditor來完成bean的屬性設定。當提到將java.lang.String作為你在XML檔案中宣告的某些bean的屬性值時,Spring將會(如果相應的屬性的設定方法具有一個Class引數)使用ClassEditor嘗試將引數解析成Class物件。
  • 在Spring的MVC框架中解析HTTP請求的引數是由各種PropertyEditor完成的,你可以把它們手動繫結到CommandController
    的所有子類。

Spring有一些內建的PropertyEditor使生活變得輕鬆。它們中的每一個都已列在下面,並且它們都被放在org.springframework.beans.propertyeditors包中。大部分但並不是全部(如下所示),預設情況下會由BeanWrapperImpl註冊。在某種方式下屬性編輯器是可配置的,那麼理所當然,你可以註冊你自己的變種來覆蓋預設編輯器:

Table 5.2. 內建PropertyEditor

說明
ByteArrayPropertyEditor 針對位元組陣列的編輯器。字串會簡單地轉換成相應的位元組表示。預設情況下由BeanWrapperImpl註冊。
ClassEditor 將類的字串表示形式解析成實際的類形式並且也能返回實際類的字串表示形式。如果找不到類,會丟擲一個IllegalArgumentException。預設情況下由BeanWrapperImpl註冊。
CustomBooleanEditor 針對Boolean屬性的可定製的屬性編輯器。預設情況下由BeanWrapperImpl註冊,但是可以作為一種自定義編輯器通過註冊其自定義例項來進行覆蓋。
CustomCollectionEditor 針對集合的屬性編輯器,可以將原始的Collection轉換成給定的目標Collection型別。
CustomDateEditor 針對java.util.Date的可定製的屬性編輯器,支援自定義的時間格式。不會被預設註冊,使用者必須使用適當格式進行註冊。
CustomNumberEditor 針對任何Number子類(比如IntegerLongFloatDouble)的可定製的屬性編輯器。預設情況下由BeanWrapperImpl註冊,但是可以作為一種自定義編輯器通過註冊其自定義例項來進行覆蓋。
FileEditor 能夠將字串解析成java.io.File物件。預設情況下由BeanWrapperImpl註冊。
InputStreamEditor 一次性的屬性編輯器,能夠讀取文字字串並生成(通過中間的ResourceEditor以及Resource)一個InputStream物件,因此InputStream型別的屬性可以直接以字串設定。請注意預設的使用方式不會為你關閉InputStream!預設情況下由BeanWrapperImpl註冊。
LocaleEditor 能夠將字串解析成Locale物件,反之亦然(字串格式是[country][variant],這與Locale提供的toString()方法是一樣的)。預設情況下由BeanWrapperImpl註冊。
PatternEditor 能夠將字串解析成java.util.regex.Pattern物件,反之亦然。
PropertiesEditor 能夠將字串(按照java.util.Properties類的java文件定義的格式進行格式化)解析成Properties物件。預設情況下由BeanWrapperImpl註冊。
StringTrimmerEditor 用於縮減字串的屬性編輯器。有選擇性允許將一個空字串轉變成null值。不會進行預設註冊,需要在使用者有需要的時候註冊。
URLEditor 能夠將一個URL的字串表示解析成實際的URL物件。預設情況下由BeanWrapperImpl註冊。

Spring使用java.beans.PropertyEditorManager來設定可能需要的屬性編輯器的搜尋路徑。搜尋路徑中還包括了sun.bean.editors

,這個包裡面包含如FontColor型別以及其他大部分基本型別的PropertyEditor實現。還要注意的是,如果PropertyEditor類與它們所處理的類位於同一個包並且除了’Editor’字尾之外擁有相同的名字,那麼標準的JavaBeans基礎設施會自動發現這些它們(不需要你顯式的註冊它們)。例如,有人可能會有以下的類和包結構,這已經足夠識別出FooEditor類並將其作為Foo型別屬性的PropertyEditor

com
  chank
    pop
      Foo
      FooEditor // the PropertyEditor for the Foo class

要注意的是在這裡你也可以使用標準JavaBeans機制的BeanInfo(在in not-amazing-detail here有描述)。在下面的示例中,你可以看使用BeanInfo機制為一個關聯類的屬性顯式註冊一個或多個PropertyEditor例項。

com
  chank
    pop
      Foo
      FooBeanInfo // the BeanInfo for the Foo class

這是被引用到的FooBeanInfo類的Java原始碼。它會將一個CustomNumberEditorFoo類的age屬性關聯。

public class FooBeanInfo extends SimpleBeanInfo {

    public PropertyDescriptor[] getPropertyDescriptors() {
        try {
            final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
            PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Foo.class) {
                public PropertyEditor createPropertyEditor(Object bean) {
                    return numberPE;
                };
            };
            return new PropertyDescriptor[] { ageDescriptor };
        }
        catch (IntrospectionException ex) {
            throw new Error(ex.toString());
        }
    }
}

註冊額外的自定義PropertyEditor {#toc_7}

當bean屬性設定成一個字串值時,Spring IoC容器最終會使用標準JavaBeans的PropertyEditor將這些字串轉換成複雜型別的屬性。Spring預先註冊了一些自定義PropertyEditor(例如將一個以字串表示的類名轉換成真正的Class物件)。此外,Java的標準JavaBeansPropertyEditor查詢機制允許一個PropertyEditor只需要恰當的命名並同它支援的類位於相同的包,就能夠自動發現它。

如果需要註冊其他自定義的PropertyEditor,還有幾種可用機制。假設你有一個BeanFactory引用,最人工化的方式(但通常並不方便或者推薦)是直接使用ConfigurableBeanFactory介面的registerCustomEditor()方法。另一種略為方便的機制是使用一個被稱為CustomEditorConfigurer的特殊的bean factory後處理器(post-processor)。雖然bean factory後處理器可以與BeanFactory實現一起使用,但是因為CustomEditorConfigurer有一個巢狀屬性設定過程,所以強烈推薦它與ApplicationContext一起使用,這樣就可以採用與其他bean類似的方式來部署它,並自動檢測和應用。

請注意所有的bean工廠和應用上下文都會自動地使用一些內建屬性編輯器,這些編輯器通過一個被稱為BeanWrapper的介面來處理屬性轉換。BeanWrapper註冊的那些標準屬性編輯器已經列在上一部分。 此外,針對特定的應用程式上下文型別,ApplicationContext會用適當的方法覆蓋或新增一些額外的編輯器來處理資源查詢。

標準的JavaBeansPropertyEditor例項用於將字串表示的屬性值轉換成實際的複雜型別屬性。CustomEditorConfigurer,一個bean factory後處理器,可以為新增額外的PropertyEditorApplicationContext提供便利支援。

考慮一個使用者類ExoticType和另外一個需要將ExoticType設為屬性的類DependsOnExoticType

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

最後,我們使用CustomEditorConfigurer將一個新的PropertyEditor註冊到ApplicationContext,那麼在需要的時候就能夠使用它:

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="customEditors">
        <map>
            <entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
        </map>
    </property>
</bean>

使用PropertyEditorRegistrar {#toc_8}

另一種將屬性編輯器註冊到Spring容器的機制是建立和使用一個PropertyEditorRegistrar。當你需要在幾個不同場景裡使用同一組屬性編輯器,這個介面會特別有用:編寫一個相應的registrar並在每個用例裡重用。PropertyEditorRegistrar與一個被稱為PropertyEditorRegistry的介面配合工作,後者被Spring的BeanWrapper(以及DataBinder)實現。當與CustomEditorConfigurer配合使用的時候,PropertyEditorRegistrar特別方便(這裡有介紹),因為前者暴露了一個方法setPropertyEditorRegistrars(..):以這種方式新增到CustomEditorConfigurerdPropertyEditorRegistrar可以很容易地在DataBinder和Spring MVCControllers之間共享。另外,它避免了在自定義編輯器上的同步需求:一個PropertyEditorRegistrar可以為每一次bean建立嘗試建立新的PropertyEditor例項。

使用PropertyEditorRegistrar可能最好還是以一個例子來說明。首先,你需要建立你自己的PropertyEditorRegistrar實現:

package com.foo.editors.spring;

public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {

    public void registerCustomEditors(PropertyEditorRegistry registry) {

        // it is expected that new PropertyEditor instances are created
        registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());

        // you could register as many custom property editors as are required here...
    }
}

也可以檢視org.springframework.beans.support.ResourceEditorRegistrar當作一個PropertyEditorRegistrar實現的示例。注意在它的registerCustomEditors(..)方法實現裡是如何為每個屬性編輯器建立新的例項的。

接著我們配置了一個CustomEditorConfigurerd並將我們的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’s MVC web framework的那些人,使用PropertyEditorRegistrar與資料繫結的Controller(比如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之間共享。

5.5 Spring型別轉換 {#toc_9}

Spring 3引入了core.convert包來提供一個一般型別的轉換系統。這個系統定義了實現型別轉換邏輯的服務提供介面(SPI)以及在執行時執行型別轉換的API。在Spring容器內,這個系統可以當作是PropertyEditor的替代選擇,用於將外部bean的屬性值字串轉換成所需的屬性型別。這個公共的API也可以在你的應用程式中任何需要型別轉換的地方使用。

5.5.1 Converter SPI {#toc_10}

實現型別轉換邏輯的SPI是簡單並且強型別的:

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

    T convert(S source);

}

要建立屬於你自己的轉換器,只需要簡單的實現以上介面即可。泛型引數S表示你想要進行轉換的源型別,而泛型引數T表示你想要轉換的目標型別。如果一個包含S型別元素的集合或陣列需要轉換為一個包含T型別的陣列或集合,那麼這個轉換器也可以被透明地應用,前提是已經註冊了一個委託陣列或集合的轉換器(預設情況下會是DefaultConversionService處理)。

對每次方法convert(S)的呼叫,source引數值必須確保不為空。如果轉換失敗,你的轉換器可以丟擲任何非受檢異常(unchecked exception);具體來說,為了報告一個非法的source引數值,應該丟擲一個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.5.2 ConverterFactory {#toc_11}

當你需要集中整個類層次結構的轉換邏輯時,例如,碰到將String轉換到java.lang.Enum物件的時候,請實現ConverterFactory

package org.springframework.core.convert.converter;

public interface ConverterFactory<S, R> {

    <T extends R> Converter<S, T> getConverter(Class<T> targetType);

}

泛型引數S表示你想要轉換的源型別,泛型引數R表示你可以轉換的那些範圍內的型別的基類。然後實現getConverter(Class),其中T就是R的一個子類。

考慮將StringToEnum作為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.5.3 GenericConverter {#toc_12}

當你需要一個複雜的轉換器實現時,請考慮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 {#toc_13}

有時候你只想要在特定條件成立的情況下Converter才執行,例如,你可能只想要在目標欄位存在特定註解的情況下才執行Converter,或者你可能只想要在目標類中定義了特定方法,比如staticvalueOf方法,才執行ConverterConditionalGenericConverterGenericConverterConditionalConveter介面的聯合,允許你定義這樣的自定義匹配條件:

public interface ConditionalGenericConverter
        extends GenericConverter, ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);

}

ConditionalGenericConverter的一個很好的例子是一個在持久化實體標識和實體引用之間進行轉換的實體轉換器。這個實體轉換器可能只匹配這樣的條件–目標實體類聲明瞭一個靜態的查詢方法,例如findAccount(Long),你將在matches(TypeDescriptor,TypeDescriptor)方法實現裡執行這樣的查詢方法的檢測。

5.5.4 ConversionService API {#toc_14}

ConversionService介面定義了執行時執行型別轉換的統一API,轉換器往往是在這個門面(facade)介面背後執行:

package org.springframework.core.convert;

public interface ConversionService {

    boolean canConvert(Class<?> sourceType, Class<?> targetType);

    <T> T convert(Object source, Class<T> targetType);

    boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);

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

}

大多數ConversionService實現也會實現ConverterRegistry介面,這個介面提供一個用於註冊轉換器的服務提供介面(SPI)。在內部,一個ConversionService實現會以委託給註冊其中的轉換器的方式來執行型別轉換邏輯。

core.convert.support包已經提供了一個強大的ConversionService實現,GenericConversionService是適用於大多數環境的通用實現,ConversionServiceFactory以工廠的方式為建立常見的ConversionService配置提供了便利。

5.5.5 配置ConversionService {#toc_15}

ConversionService是一個被設計成在應用程式啟動時會進行例項化的無狀態物件,隨後可以在多個執行緒之間共享。在一個Spring應用程式中,你通常會為每一個Spring容器(或者應用程式上下文ApplicationContext)配置一個ConversionService例項,它會被Spring接收並在框架需要執行一個型別轉換時使用。你也可以將這個ConversionService直接注入到你任何的Bean中並直接呼叫。

如果Spring沒有註冊ConversionService,則會使用原始的基於PropertyEditor的系統。

要向Spring註冊預設的ConversionService,可以用conversionService作為id來新增如下的bean定義:

<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章節的Section 18.16.3 “Conversion and Formatting”

在某些情況下,你可能希望在轉換期間應用格式化,可以看5.6.3 “FormatterRegistry SPI”獲取使用FormattingConversionServiceFactoryBean的細節。

5.5.6 程式設計方式使用ConversionService {#toc_16}

要以程式設計方式使用ConversionService,你只需要像處理其他bean一樣注入一個引用即可:

@Service
public class MyService {

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

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

對大多數用例來說,convert方法指定了可以使用的目標型別,但是它不適用於更復雜的型別比如引數化元素的集合。例如,如果你想要以程式設計方式將一個IntegerList轉換成一個StringList,就需要為原型別和目標型別提供一個正式的定義。

幸運的是,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會自動註冊對大部分環境都適用的轉換器,這其中包括了集合轉換器、標量轉換器還有基本的ObjectString的轉換器。可以通過呼叫DefaultConversionService類上的靜態方法addDefaultConverters來向任意的ConverterRegistry註冊相同的轉換器。

因為值型別的轉換器可以被陣列和集合重用,所以假設標準集合處理是恰當的,就沒有必要建立將一個SCollection轉換成一個TCollection的特定轉換器

.6 Spring欄位格式化 {#toc_17}

如上一節所述,core.convert包是一個通用型別轉換系統,它提供了統一的ConversionService API以及強型別的Converter SPI用於實現將一種型別轉換成另外一種的轉換邏輯。Spring容器使用這個系統來繫結bean屬性值,此外,Spring表示式語言(SpEL)和DataBinder也都使用這個系統來繫結欄位值。舉個例子,當SpEL需要將Short強制轉換成Long來完成一次expression.setValue(Object bean, Object value)嘗試時,core.convert系統就會執行這個強制轉換。

現在讓我們考慮一個典型的客戶端環境如web或桌面應用程式的型別轉換要求,在這樣的環境裡,你通常會經歷將字串進行轉換以支援客戶端回傳的過程以及轉換回字串以支援檢視渲染的過程。此外,你經常需要對字串值進行本地化。更通用的core.convert包中的Converter SPI不直接解決這種格式化要求。Spring 3為此引入了一個方便的Formatter SPI來直接解決這些問題,這個介面為客戶端環境提供一種簡單強大並且替代PropertyEditor的方案。

一般來說,當你需要實現通用的型別轉換邏輯時請使用Converter SPI,例如,在java.util.Date和java.lang.Long之間進行轉換。當你在一個客戶端環境(比如web應用程式)工作並且需要解析和列印本地化的欄位值時,請使用Formatter SPI。ConversionService介面為這兩者提供了一套統一的型別轉換API。

5.6.1 Formatter SPI {#toc_18}

Formatter 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包提供了NumberFormatterCurrencyFormatterPercentFormatter,它們通過使用java.text.NumberFormat來格式化java.lang.Number物件 。datetime包提供了DateFormatter,其通過使用java.text.DateFormat來格式化java.util.Datedatetime.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團隊歡迎社群驅動的Formatter貢獻,可以登陸網站jira.spring.io瞭解如何參與貢獻。

5.6.2 註解驅動的格式化 {#toc_19}

如你所見,欄位格式化可以通過欄位型別或者註解進行配置,要將一個註解繫結到一個格式化器,可以實現AnnotationFormatterFactory:

package org.springframework.format;

public interface AnnotationFormatterFactory<A extends Annotation> {

    Set<Class<?>> getFieldTypes();

    Printer<?> getPrinter(A annotation, Class<?> fieldType);

    Parser<?> getParser(A annotation, Class<?> fieldType);

}

泛型引數A代表你想要關聯格式化邏輯的欄位註解型別,例如org.springframework.format.annotation.DateTimeFormat。讓getFieldTypes()方法返回可能使用註解的欄位型別,讓getPrinter()方法返回一個可以列印被註解欄位的值的印表機(Printer),讓getParser()方法返回一個可以解析被註解欄位的客戶端值的解析器(Parser)。

下面這個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;

}

Format Annotation API {#toc_20}

org.springframework.format.annotation包中存在一套可移植(portable)的格式化註解API。請使用@NumberFormat格式化java.lang.Number欄位,使用@DateTimeFormat格式化java.util.Date、java.util.Calendar、java.util.Long(注:此處可能是原文錯誤,應為java.lang.Long)或者Joda Time欄位。

下面這個例子使用@DateTimeFormat將java.util.Date格式化為ISO時間(yyyy-MM-dd)

public class MyModel {

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

}

5.6.3 FormatterRegistry SPI {#toc_21}

FormatterRegistry是一個用於註冊格式化器和轉換器的服務提供介面(SPI)。FormattingConversionService是一個適用於大多數環境的FormatterRegistry實現,可以以程式設計方式或利用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,你可以只定義這些規則一次,而在需要格式化的時候應用它們。

5.6.4 FormatterRegistrar SPI {#toc_22}

FormatterRegistrar是一個通過FormatterRegistry註冊格式化器和轉換器的服務提供介面(SPI):

package org.springframework.format;

public interface FormatterRegistrar {

    void registerFormatters(FormatterRegistry registry);

}

當要為一個給定的格式化類別(比如時間格式化)註冊多個關聯的轉換器和格式化器時,FormatterRegistrar會非常有用。

下一部分提供了更多關於轉換器和格式化器註冊的資訊。

5.6.5 在Spring MVC中配置格式化 {#toc_23}

請檢視Spring MVC章節的Section 18.16.3 “Conversion and Formatting”

.7 配置一個全域性的日期&時間格式 {#toc_24}

預設情況下,未被@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() {

        // Use the DefaultFormattingConversionService but do not register defaults
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);

        // Ensure @NumberFormat is still supported
        conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());

        // Register date conversion with a specific global format
        DateFormatterRegistrar registrar = new DateFormatterRegistrar();
        registrar.setFormatter(new DateFormatter("yyyyMMdd"));
        registrar.registerFormatters(conversionService);

        return conversionService;
    }
}

如果你更喜歡基於XML的配置,你可以使用一個FormattingConversionServiceFactoryBean,這是同一個例子,但這次使用了Joda Time:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd>

    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="registerDefaultFormatters" value="false" />
        <property name="formatters">
            <set>
                <bean class="org.springframework.format.number.NumberFormatAnnotationFormatterFactory" />
            </set>
        </property>
        <property name="formatterRegistrars">
            <set>
                <bean class="org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar">
                    <property name="dateFormatter">
                        <bean class="org.springframework.format.datetime.joda.DateTimeFormatterFactoryBean">
                            <property name="pattern" value="yyyyMMdd"/>
                        </bean>
                    </property>
                </bean>
            </set>
        </property>
    </bean>
</beans>

Joda Time提供了不同的型別來表示datetimedate-time的值,JodaTimeFormatterRegistrar中的dateFormattertimeFormatterdateTimeFormatter屬性應該為每種型別配置不同的格式。DateTimeFormatterFactoryBean提供了一種方便的方式來建立格式化器。在

如果你在使用Spring MVC,請記住要明確配置所使用的轉換服務。針對基於@Configuration的Java配置方式這意味著要繼承WebMvcConfigurationSupport並且覆蓋mvcConversionService()方法。針對XML的方式,你應該使用mvc:annotation-drive元素的'conversion-service'屬性。更多細節請看Section 18.16.3 “Conversion and Formatting”

{#toc_25}

5.8 Spring驗證 {#toc_25}

Spring 3對驗證支援引入了幾個增強功能。首先,現在全面支援JSR-303 Bean Validation API;其次,當採用程式設計方式時,Spring的DataBinder現在不僅可以繫結物件還能夠驗證它們;最後,Spring MVC現在已經支援宣告式地驗證@Controller的輸入。

5.8.1 JSR-303 Bean Validation API概述 {#toc_26}

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規範的驗證器進行校驗的時候,這些約束就會被強制執行。

有關JSR-303/JSR-349的一般資訊,可以訪問網站Bean Validation website去檢視。有關預設參考實現的具體功能的資訊,可以參考網站Hibernate Validator的文件。想要了解如何將Bean驗證器提供程式設定為Spring bean,請繼續保持閱讀。

5.8.2 配置Bean驗證器提供程式 {#toc_27}

Spring提供了對Bean Validation API的全面支援,這包括將實現JSR-303/JSR-349規範的Bean驗證提供程式引導為Spring Bean的方便支援。這樣就允許在應用程式任何需要驗證的地方注入javax.validation.ValidatorFactory或者javax.validation.Validator

LocalValidatorFactoryBean當作Spring bean來配置成預設的驗證器:

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

以上的基本配置會觸發Bean Validation使用它預設的引導機制來進行初始化。作為實現JSR-303/JSR-349規範的提供程式,如Hibernate Validator,可以存在於類路徑以使它能被自動檢測到。

注入驗證器 {#toc_28}

LocalValidatorFactoryBean實現了javax.validation.ValidatorFactoryjavax.validation.Validator這兩個介面,以及Spring的org.springframework.validation.Validator介面,你可以將這些介面當中的任意一個注入到需要呼叫驗證邏輯的Bean裡。

如果你喜歡直接使用Bean Validtion API,那麼就注入javax.validation.Validator的引用:

import javax.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;

如果你的Bean需要Spring Validation API,那麼就注入org.springframework.validation.Validator的引用:

import org.springframework.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;

}

配置自定義約束 {#toc_29}

每一個Bean驗證約束由兩部分組成,第一部分是聲明瞭約束和其可配置屬性的@Constraint註解,第二部分是實現約束行為的javax.validation.ConstraintValidator介面實現。為了將宣告與實現關聯起來,每個@Constraint註解會引用一個相應的驗證約束的實現類。在執行期間,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;

    ...
}

如你所見,一個約束驗證器實現可以像其他Spring bean一樣使用@Autowired註解來自動裝配它的依賴。

Spring驅動的方法驗證 {#toc_30}

被Bean Validation 1.1以及作為Hibernate Validator 4.3中的自定義擴充套件所支援的方法驗證功能可以通過配置MethodValidationPostProcessor的bean定義整合到Spring的上下文中:

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

為了符合Spring驅動的方法驗證,需要對所有目標類用Spring的@Validated註解進行註解,且有選擇地對其宣告驗證組,這樣才可以使用。請查閱MethodValidationPostProcessor的java文件來了解針對Hibernate Validator和Bean Validation 1.1提供程式的設定細節。

附加配置選項 {#toc_31}

對於大多數情況,預設的LocalValidatorFactoryBean配置應該足夠。有許多配置選項來處理從訊息插補到遍歷解析的各種Bean驗證結構。請檢視LocalValidatorFactoryBean的java文件來獲取關於這些選項的更多資訊。

5.8.3 配置DataBinder {#toc_32}

從Spring 3開始,DataBinder的例項可以配置一個驗證器。一旦配置完成,那麼可以通過呼叫binder.validate()來呼叫驗證器,任何的驗證錯誤都會自動新增到DataBinder的繫結結果(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,一個DataBinder也可以配置多個Validator例項。當需要將全域性配置的Bean驗證與一個DataBinder例項上區域性配置的SpringValidator結合時,這一點是非常有用的。

5.8.4 Spring MVC 3 驗證 {#toc_33}

請檢視Spring MVC章節的Section 18.16.4 “Validation”