將驗證視為業務邏輯有其優缺點,Spring提供的驗證(和數據綁定)設計不排除其中任何一種。具體來說,驗證不應與Web層綁定,並且應該易於本地化,並且應該可以插入任何可用的驗證器。考慮到這些問題,Spring提供了一個Validator
契約,該契約既基本又可以在應用程序的每個層中使用。
數據綁定對於使用戶輸入動態綁定到應用程序的域模型(或用於處理用戶輸入的任何對象)非常有用。Spring提供了恰當地命名為DataBinder
的功能。Validator
和DataBinder
由validation
包組成,被主要的使用但不僅限於web層。
BeanWrapper
在Spring框架中是一個基本的概念並且在許多地方被使用到。然而,你大概不需要直接地使用BeanWrapper
。但是,由於這是參考文檔,所以我們認為可能需要一些解釋。我們將在本章中解釋BeanWrapper
,因為如果你要使用它,那麼在嘗試將數據綁定到對象時最有可能使用它。
Spring的DataBinder
和低級別BeanWrapper
兩者使用PropertyEditorSupport
實現去解析和格式化屬性值。PropertyEditor
和PropertyEditorSupport
類型是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
對象組成的Customer
。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/>
標籤去檢查錯誤信息,但是你也可以自己檢查Errors
對象。更多關於提供的信息在Javadoc中。
參考代碼:
com.liyong.ioccontainer.service.validator.ValidatorTest
3.2 解析碼到錯誤信息
我們介紹了數據綁定和校驗。本節介紹與驗證錯誤對應的輸出消息。在上一節顯示的例子中,我們拒絕name
和age
字段。如果我們想使用MessageSource
去輸出錯誤信息,我們可以使用提供的錯誤碼,當拒絕字段時(在這個場景中name
和age
)。當你Errors
接口調用(直接地或間接地,通過使用ValidationUtils
類)rejectValue
或其他reject
方法之一時,底層的實現不僅註冊你傳遞的碼,而且還註冊一些附加的錯誤碼。MessageCodesResolver
確定哪一個錯誤碼註冊到Errors
接口。默認情況下,使用DefaultMessageCodesResolver
,它(例如)不僅使用你提供的代碼註冊消息,而且還註冊包含傳遞給拒絕方法的字段名稱的消息。因此,如果你通過使用rejectValue(“age”,“too.darn.old”)
拒絕字段,則除了too.darn.old
代碼外,Spring還將註冊too.darn.old.age
和too.darn.old.age.int
(第一個包含字段名稱,第二個包含字段類型)。這樣做是為了方便開發人員在定位錯誤消息時提供幫助。
更多MessageCodesResolver
上和默認策略信息可以分別地在MessageCodesResolver
和DefaultMessageCodesResolver
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 的PropertyChangeListeners
和VetoableChangeListeners
的功能,而無需在目標類中支持代碼。最後但並非不重要的一點是,BeanWrapper
支持設置索引屬性。BeanWrapper
通常不直接由應用程序代碼使用,而是由DataBinder
和BeanFactory
使用。
BeanWrapper
的工作方式部分由其名稱表示:它包裝一個Bean,以對該Bean執行操作,例如設置和檢索屬性。
3.3.1 設置和獲取基本的和潛入的屬性
設置和獲取屬性是通過BeanWrapper
的重載方法setPropertyValue
和getPropertyValue
的變體。查看它們的詳細文檔。下面的表格顯示這些約定:
Expression | Explanation |
---|---|
name |
表示屬性name 對應的getName() 或 isName() 和 setName(..) 方法。 |
account.name |
表示嵌入account 屬性的name 屬性對應的getAccount().setName() 或getAccount().getName() 方法 |
account[2] |
表示2個索引元素屬性account 。索引屬性可以是類型array 、list 或其他自然順序集合。 |
account[COMPANYNAME] |
表示map 實體的值通過account Map屬性的key COMPANYNAME 索引。 |
(如果你沒打算直接使用BeanWrapper
,下面部分不是至關重要地。如果你僅僅使用DataBinder
、BeanFactory
和他的默認實現,你可以跳過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;
}
}
以下代碼段顯示了一些有關如何檢索和操縱實例化的Company
和Employee
的某些屬性的示例:
BeanWrapper company = new BeanWrapperImpl(new Company());
// setting the company name..
company.setPropertyValue("name", "Some Company Inc.");
// ... can also be done like this:
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);
// ok, let's create the director and tie it to the company:
BeanWrapper jim = new BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());
// retrieving the salary of the managingDirector through the company
Float salary = (Float) company.getPropertyValue("managingDirector.salary");
代碼示例:
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 子類可自定義屬性編輯器,例如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 類的javadoc中定義的格式)轉換為Properties 對象 |
StringTrimmerEditor |
修剪字符串的屬性編輯器。 (可選)允許將空字符串轉換為空值。默認不被註冊-必須被用戶註冊。 |
URLEditor |
能夠轉換一個字符串代表的URL為真實的URL對象。默認,通過BeanWrapperImpl 註冊。 |
Spring使用java.beans.PropertyEditorManager
去設置屬性編輯器可能需要的搜索路徑。搜索路徑也可以包含sun.bean.editors
,它包括例如Font
、Color
和大多數原始類型的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源代碼,它將CustomNumberEditor
與Something
類的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()));
}
}
最後,下面的示例演示如何使用CustomEditorConfigurer
向ApplicationContext
註冊新的PropertyEditor
,然後可以根據需要使用它:
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
</map>
</property>
</bean>
參考代碼:
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>)
,其中T
是R
的子類。
考慮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接口,因此僅應在需要時使用它。支持Converter
或ConverterFactory
以滿足基本的類型轉換需求。參考代碼:
com.liyong.ioccontainer.service.converter.GenericConverterTest
使用ConditionalGenericConverter
有時,你希望Converter
僅在滿足特定條件時才運行。例如,你可能只想在目標字段上存在特定註解時才運行Converter
,或者可能在目標類上定義了特定方法(例如靜態valueOf
方法)時才運行Converter
。ConditionalGenericConverter
是GenericConverter
和ConditionalConverter
接口的聯合,可讓你定義以下自定義匹配條件:
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
屬性。屬性值可以實現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一章中的轉換和格式化。
在某些情況下,你可能希望在轉換過程中應用格式設置。有關使用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(...)
}
}
對於大多數用例,可以使用指定targetType
的convert
方法,但不適用於更復雜的類型,例如參數化元素的集合。例如,如果要以編程方式將整數列表轉換為字符串列表,則需要提供源類型和目標類型的格式定義。
幸運的是,如下面的示例所示,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.Date
和Long
之間轉換。當你在客戶端環境中(例如,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> {
}
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
,實現前面展示的Formatter
接口。將T
參數化為你希望格式化的對象類型-例如,java.util.Date
。實現print()
操作以打印T
的實例以在客戶端語言環境中顯示。實現parse()
操作,以從客戶端本地返回的格式化表示形式解析T
的實例。如果嘗試解析失敗,你的Formatter
應該拋一個ParseException
或IllegalArgumentException
異常。注意確保你的Formatter
實現是線程安全的。
為了方便format
子包提供一些Formatter
實現。number
包提供NumberStyleFormatter
、CurrencyStyleFormatter
和PercentStyleFormatter
去格式化Number
對象,它使用java.text.NumberFormat
。datetime
包提供DateFormatter
去格式化java.util.Date
與java.text.DateFormat
對象。datetime.joda
包基於Joda-Time庫提供了全面的日期時間格式支持。
下面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
貢獻。查看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.DateTimeFormat
讓getFieldTypes()
返回可在其上使用註解的字段類型。讓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
字段(例如Double
和Long
),並使用@DateTimeFormat
格式化java.util.Date
、java.util.Calendar
、Long
(用於毫秒時間戳)以及JSR-310 java.time
和Joda-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用於註冊格式化器和轉換器。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
,你可以一次定義這些規則,並在需要格式化時應用它們。
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 配置全局Date
和Time
格式
默認情況下,未使用@DateTimeFormat
註解日期和時間字段是使用DateFormat.SHORT
格式從字符串轉換的。如果願意,可以通過定義自己的全局格式來更改此設置。
為此,請確保Spring不註冊默認格式器。相反,可以藉助以下方法手動註冊格式化器:
org.springframework.format.datetime.standard.DateTimeFormatterRegistrar
-
org.springframework.format.datetime.DateFormatterRegistrar
或為Joda-Time
的org.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.ValidatorFactory
或javax.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.ValidatorFactory
和javax.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 Validator
和Bean 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.addValidators
和dataBinder.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
微信公眾號:
技術交流群: