《Spring 5 官方文件》5. 驗證、資料繫結和型別轉換
原文連結 譯者:14shadow43
5 驗證、資料繫結和型別轉換
5.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
例項註冊額外的Spring Validator
考慮將驗證作為業務邏輯是有利有弊的,Spring提供了一種不排除利弊的用於驗證(和資料繫結)的設計。具體的驗證不應該捆綁在web層,應該容易本地化並且它應該能夠插入任何可用的驗證器。考慮到以上這些,Spring想出了一個Validator
介面,它在應用程式的每一層基本都是可用的。資料繫結對於將使用者輸入動態繫結到應用程式的領域模型上(或者任何你用於處理使用者輸入的物件)是非常有用的。Spring提供了所謂的DataBinder
來處理這個。Validator
和DataBinder
組成了validation
包,其主要用於但並不侷限於MVC框架。
BeanWrapper
BeanWrapper
。儘管這是參考文件,我們仍然覺得有一些說明需要一步步來。我們將會在本章中解釋BeanWrapper
,因為你極有可能會在嘗試將資料繫結到物件的時候使用它。
Spring的DataBinder和底層的BeanWrapper都使用PropertyEditor來解析和格式化屬性值。PropertyEditor
概念是JavaBeans規範的一部分,並會在本章進行說明。Spring 3不僅引入了”core.convert”包來提供一套通用型別轉換工具,還有一個高層次的”format”包用於格式化UI欄位值。可以將這些新包視作更簡單的PropertyEditor替代方式來使用,本章還會對此進行討論。
5.2 使用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
類提供驗證行為:
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 將程式碼解析成錯誤訊息
在之前我們已經談論了資料繫結和驗證,最後一件值得討論的事情是輸出對應於驗證錯誤的訊息。在我們上面展示的例子裡,我們拒絕了name
和age
欄位。如果我們要使用MessageSource
來輸出錯誤訊息,我們將會使用我們在拒絕該欄位(這個情況下是’姓名’和’年齡’)時給出的錯誤程式碼。當你呼叫(不管是直接呼叫還是間接通過使用ValidationUtils
類呼叫)來自Errors
介面的rejectValue
或者其他reject
方法時,其底層實現不僅會註冊你傳入的程式碼,還會註冊一些額外的錯誤程式碼。註冊怎樣的錯誤程式碼取決於它所使用的MessageCodesResolver
,預設情況下,會使用DefaultMessageCodesResolver
,其不僅會使用你提供的程式碼註冊訊息,還會註冊包含你傳遞給拒絕方法的欄位名稱的訊息。所以如果你使用rejectValue("age", "too.darn.old")
來拒絕一個欄位,除了too.darn.old
程式碼,Spring還會註冊too.darn.old.age
和too.darn.old.age.int
(第一個會包含欄位名稱且第二個會包含欄位型別)。這樣做是為了方便開發人員來定位錯誤訊息等。
有關MessageCodesResolver
和其預設策略的更多資訊可以分別在MessageCodesResolver
以及DefaultMessageCodesResolver
的線上java文件中找到。
5.4 Bean操作和BeanWrapper
org.springframework.beans
包遵循Oracle提供的JavaBeans標準。一個JavaBean只是一個包含預設無參構造器的類,它遵循一個命名約定(通過一個例子):一個名為bingoMadness
屬性將有一個設定方法setBingoMadness(..)
和一個獲取方法getBingoMadness(..)
。有關JavaBeans和其規範的更多資訊,請參考Oracle的網站(javabeans)。
beans包裡一個非常重要的類是BeanWrapper
介面和它的相應實現(BeanWrapperImpl
)。引用自java文件,BeanWrapper
提供了設定和獲取屬性值(單獨或批量)、獲取屬性描述符以及查詢屬性以確定它們是可讀還是可寫的功能。BeanWrapper
還提供對巢狀屬性的支援,能夠不受巢狀深度的限制啟用子屬性的屬性設定。然後,BeanWrapper
提供了無需目標類程式碼的支援就能夠新增標準JavaBeans的PropertyChangeListeners
和VetoableChangeListeners
的能力。最後然而並非最不重要的是,BeanWrapper
提供了對索引屬性設定的支援。BeanWrapper
通常不會被應用程式的程式碼直接使用,而是由DataBinder
和BeanFactory
使用。
BeanWrapper
的名字已經部分暗示了它的工作方式:它包裝一個bean以對其執行操作,比如設定和獲取屬性。
5.4.1 設定並獲取基本和巢狀屬性
使用setPropertyValue(s)
和getPropertyValue(s)
可以設定並獲取屬性,兩者都帶有幾個過載方法。在Spring自帶的java文件中對它們有更詳細的描述。重要的是要知道物件屬性指示的幾個約定。幾個例子:
表 5.1. 屬性示例
表示式 | 說明 |
---|---|
name |
表示屬性name 與方法getName() 或isName() 和setName() 相對應 |
account.name |
表示屬性account 的巢狀屬性name 與方法getAccount().setName() 或getAccount().getName() 相對應 |
account[2] |
表示索引屬性account 的第三個元素。索引屬性可以是array 、list 或其他自然排序的集合 |
account[COMPANYNAME] |
表示對映屬性account 被鍵COMPANYNAME索引到的對映項的值 |
下面你會發現一些使用BeanWrapper
來獲取和設定屬性的例子。
(如果你不打算直接使用BeanWrapper
,那麼下一部分對你來說並不重要。如果你僅使用DataBinder
和BeanFactory
以及它們開箱即用的實現,你應該跳到關於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;
}
}
以下的程式碼片段展示瞭如何檢索和操縱例項化的Companies
和Employees
的某些屬性:
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實現
Spring使用PropertyEditor
的概念來實現Object
和String
之間的轉換。如果你考慮到它,有時候換另一種方式表示屬性可能比物件本身更方便。舉個例子,一個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子類(比如Integer 、Long 、Float 、Double )的可定製的屬性編輯器。預設情況下由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
,這個包裡面包含如Font
、Color
型別以及其他大部分基本型別的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原始碼。它會將一個CustomNumberEditor
同Foo
類的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
當bean屬性設定成一個字串值時,Spring IoC容器最終會使用標準JavaBeans的PropertyEditor
將這些字串轉換成複雜型別的屬性。Spring預先註冊了一些自定義PropertyEditor
(例如將一個以字串表示的類名轉換成真正的Class
物件)。此外,Java的標準JavaBeans PropertyEditor
查詢機制允許一個PropertyEditor
只需要恰當的命名並同它支援的類位於相同的包,就能夠自動發現它。
如果需要註冊其他自定義的PropertyEditor
,還有幾種可用機制。假設你有一個BeanFactory
引用,最人工化的方式(但通常並不方便或者推薦)是直接使用ConfigurableBeanFactory
介面的registerCustomEditor()
方法。另一種略為方便的機制是使用一個被稱為CustomEditorConfigurer
的特殊的bean factory後處理器(post-processor)。雖然bean factory後處理器可以與BeanFactory
實現一起使用,但是因為CustomEditorConfigurer
有一個巢狀屬性設定過程,所以強烈推薦它與ApplicationContext
一起使用,這樣就可以採用與其他bean類似的方式來部署它,並自動檢測和應用。
請注意所有的bean工廠和應用上下文都會自動地使用一些內建屬性編輯器,這些編輯器通過一個被稱為BeanWrapper
的介面來處理屬性轉換。BeanWrapper
註冊的那些標準屬性編輯器已經列在上一部分。 此外,針對特定的應用程式上下文型別,ApplicationContext
會用適當的方法覆蓋或新增一些額外的編輯器來處理資源查詢。
標準的JavaBeans PropertyEditor
例項用於將字串表示的屬性值轉換成實際的複雜型別屬性。CustomEditorConfigurer
,一個bean factory後處理器,可以為新增額外的PropertyEditor
到ApplicationContext
提供便利支援。
考慮一個使用者類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
另一種將屬性編輯器註冊到Spring容器的機制是建立和使用一個PropertyEditorRegistrar
。當你需要在幾個不同場景裡使用同一組屬性編輯器,這個介面會特別有用:編寫一個相應的registrar並在每個用例裡重用。PropertyEditorRegistrar
與一個被稱為PropertyEditorRegistry
的介面配合工作,後者被Spring的BeanWrapper
(以及DataBinder
)實現。當與CustomEditorConfigurer
配合使用的時候,PropertyEditorRegistrar
特別方便(這裡有介紹),因為前者暴露了一個方法setPropertyEditorRegistrars(..)
:以這種方式新增到CustomEditorConfigurerd
的PropertyEditorRegistrar
可以很容易地在DataBinder
和Spring MVC Controllers
之間共享。另外,它避免了在自定義編輯器上的同步需求:一個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型別轉換
Spring 3引入了core.convert
包來提供一個一般型別的轉換系統。這個系統定義了實現型別轉換邏輯的服務提供介面(SPI)以及在執行時執行型別轉換的API。在Spring容器內,這個系統可以當作是PropertyEditor的替代選擇,用於將外部bean的屬性值字串轉換成所需的屬性型別。這個公共的API也可以在你的應用程式中任何需要型別轉換的地方使用。
5.5.1 Converter SPI
實現型別轉換邏輯的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
當你需要集中整個類層次結構的轉換邏輯時,例如,碰到將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
當你需要一個複雜的轉換器實現時,請考慮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
有時候你只想要在特定條件成立的情況下Converter
才執行,例如,你可能只想要在目標欄位存在特定註解的情況下才執行Converter
,或者你可能只想要在目標類中定義了特定方法,比如static
valueOf
方法,才執行Converter
。ConditionalGenericConverter
是GenericConverter
和ConditionalConveter
介面的聯合,允許你定義這樣的自定義匹配條件:
public interface ConditionalGenericConverter
extends GenericConverter, ConditionalConverter {
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}
ConditionalGenericConverter
的一個很好的例子是一個在持久化實體標識和實體引用之間進行轉換的實體轉換器。這個實體轉換器可能只匹配這樣的條件–目標實體類聲明瞭一個靜態的查詢方法,例如findAccount(Long)
,你將在matches(TypeDescriptor,TypeDescriptor)
方法實現裡執行這樣的查詢方法的檢測。
5.5.4 ConversionService API
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
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
要以程式設計方式使用ConversionService,你只需要像處理其他bean一樣注入一個引用即可:
@Service
public class MyService {
@Autowired
public MyService(ConversionService conversionService) {
this.conversionService = conversionService;
}
public void doIt() {
this.conversionService.convert(...)
}
}
對大多數用例來說,convert
方法指定了可以使用的目標型別,但是它不適用於更復雜的型別比如引數化元素的集合。例如,如果你想要以程式設計方式將一個Integer
的List
轉換成一個String
的List
,就需要為原型別和目標型別提供一個正式的定義。
幸運的是,TypeDescriptor
提供了多種選項使事情變得簡單:
DefaultConversionService cs = new DefaultConversionService();
List<Integer> input = ....
cs.convert(input,
TypeDescriptor.forObject(input), // List<Integer> type descriptor
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));
注意DefaultConversionService
會自動註冊對大部分環境都適用的轉換器,這其中包括了集合轉換器、標量轉換器還有基本的Object
到String
的轉換器。可以通過呼叫DefaultConversionService
類上的靜態方法addDefaultConverters
來向任意的ConverterRegistry
註冊相同的轉換器。
因為值型別的轉換器可以被陣列和集合重用,所以假設標準集合處理是恰當的,就沒有必要建立將一個S
的Collection
轉換成一個T
的Collection
的特定轉換器。
5.6 Spring欄位格式化
如上一節所述,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
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
包提供了NumberFormatter
、CurrencyFormatter
和PercentFormatter
,它們通過使用java.text.NumberFormat
來格式化java.lang.Number
物件 。datetime
包提供了DateFormatter
,其通過使用java.text.DateFormat
來格式化java.util.Date
。datetime.joda
包基於Joda Time library提供了全面的日期時間格式化支援。
考慮將DateFormatter
作為Formatter
實現的一個例子:
package org.springframework.format.datetime;
public final class DateFormatter implements Formatter<Date> {
private String pattern;
public DateFormatter(String pattern) {
this.pattern = pattern;
}
public String print(Date date, Locale locale) {
if (date == null) {
return "";
}
return getDateFormat(locale).format(date);
}
public Date parse(String formatted, Locale locale) throws ParseException {
if (formatted.length() == 0) {
return null;
}
return getDateFormat(locale).parse(formatted);
}
protected DateFormat getDateFormat(Locale locale) {
DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale);
dateFormat.setLenient(false);
return dateFormat;
}
}
Spring團隊歡迎社群驅動的Formatter
貢獻,可以登陸網站jira.spring.io瞭解如何參與貢獻。
5.6.2 註解驅動的格式化
如你所見,欄位格式化可以通過欄位型別或者註解進行配置,要將一個註解繫結到一個格式化器,可以實現AnnotationFormatterFactory:
package org.springframework.format;
public interface AnnotationFormatterFactory<A extends Annotation> {
Set<Class<?>> getFieldTypes();
Printer<?> getPrinter(A annotation, Class<?> fieldType);
Parser<?> getParser(A annotation, Class<?> fieldType);
}
泛型引數A代表你想要關聯格式化邏輯的欄位註解型別,例如org.springframework.format.annotation.DateTimeFormat
。讓getFieldTypes()
方法返回可能使用註解的欄位型別,讓getPrinter()
方法返回一個可以列印被註解欄位的值的印表機(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
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
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
FormatterRegistrar是一個通過FormatterRegistry註冊格式化器和轉換器的服務提供介面(SPI):
package org.springframework.format;
public interface FormatterRegistrar {
void registerFormatters(FormatterRegistry registry);
}
當要為一個給定的格式化類別(比如時間格式化)註冊多個關聯的轉換器和格式化器時,FormatterRegistrar會非常有用。
下一部分提供了更多關於轉換器和格式化器註冊的資訊。
5.6.5 在Spring MVC中配置格式化
5.7 配置一個全域性的日期&時間格式
預設情況下,未被@DateTimeFormat
註解的日期和時間欄位會使用DateFormat.SHORT
風格從字串轉換。如果你願意,你可以定義你自己的全域性格式來改變這種預設行為。
你將需要確保Spring不會註冊預設的格式化器,取而代之的是你應該手動註冊所有的格式化器。請根據你是否依賴Joda Time庫來確定是使用org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar
類還是org.springframework.format.datetime.DateFormatterRegistrar
類。
例如,下面的Java配置會註冊一個全域性的’yyyyMMdd’格式,這個例子不依賴於Joda Time庫:
@Configuration
public class AppConfig {
@Bean
public FormattingConversionService conversionService() {
// 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提供了不同的型別來表示
date
、time
和date-time
的值,JodaTimeFormatterRegistrar
中的dateFormatter
、timeFormatter
和dateTimeFormatter
屬性應該為每種型別配置不同的格式。DateTimeFormatterFactoryBean
提供了一種方便的方式來建立格式化器。
如果你在使用Spring MVC,請記住要明確配置所使用的轉換服務。針對基於@Configuration
的Java配置方式這意味著要繼承WebMvcConfigurationSupport
並且覆蓋mvcConversionService()
方法。針對XML的方式,你應該使用mvc:annotation-drive
元素的'conversion-service'
屬性。更多細節請看Section 18.16.3 “Conversion and Formatting”。
5.8 Spring驗證
Spring 3對驗證支援引入了幾個增強功能。首先,現在全面支援JSR-303 Bean Validation API;其次,當採用程式設計方式時,Spring的DataBinder現在不僅可以繫結物件還能夠驗證它們;最後,Spring MVC現在已經支援宣告式地驗證@Controller
的輸入。
5.8.1 JSR-303 Bean Validation API概述
JSR-303對Java平臺的驗證約束宣告和元資料進行了標準化定義。使用此API,你可以用宣告性的驗證約束對領域模型的屬性進行註解,並在執行時強制執行它們。現在已經有一些內建的約束供你使用,當然你也可以定義你自己的自定義約束。
為了說明這一點,考慮一個擁有兩個屬性的簡單的PersonForm模型:
public class PersonForm {
private String name;
private int age;
}
JSR-303允許你針對這些屬性定義宣告性的驗證約束:
public class PersonForm {
@NotNull
@Size(max=64)
private String name;
@Min(0)
private int age;
}
當此類的一個例項被實現JSR-303規範的驗證器進行校驗的時候,這些約束就會被強制執行。
有關JSR-303/JSR-349的一般資訊,可以訪問網站Bean Validation website去檢視。有關預設參考實現的具體功能的資訊,可以參考網站Hibernate Validator的文件。想要了解如何將Bean驗證器提供程式設定為Spring bean,請繼續保持閱讀。
5.8.2 配置Bean驗證器提供程式
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,可以存在於類路徑以使它能被自動檢測到。
注入驗證器
LocalValidatorFactoryBean
實現了javax.valida