基于最新Spring 5.x,詳細介紹了Spring的類型轉換機制,包括三種最常見的資料類型轉換器PropertyEditor、Formatter、Converter、HttpMessageConverter、ConversionService等核心類。
在使用Spring以及使用Spring MVC的時候,Spring會通過一系列的類型轉換機制将參數轉換為我們指定的類型,這種轉換對于使用者來說通常是無感的,我們隻需要使用指定的類型接收即可!
下面我們來詳細的了解Spring的類型轉換機制,包括三種最常見的資料類型轉換器PropertyEditor、Formatter、Converter,以及ConversionService等核心類。
Spring MVC學習 系列文章
Spring MVC學習(1)—MVC的介紹以及Spring MVC的入門案例
Spring MVC學習(2)—Spring MVC中容器的層次結構以及父子容器的概念
Spring MVC學習(3)—Spring MVC中的核心元件以及請求的執行流程
Spring MVC學習(4)—ViewSolvsolver視圖解析器的詳細介紹與使用案例
Spring MVC學習(5)—基于注解的Controller控制器的配置全解【一萬字】
Spring MVC學習(6)—Spring資料類型轉換機制全解【一萬字】
Spring MVC學習(7)—Validation基于注解的聲明式資料校驗機制全解【一萬字】
Spring MVC學習(8)—HandlerInterceptor處理器攔截器機制全解
Spring MVC學習(9)—項目統一異常處理機制詳解與使用案例
Spring MVC學習(10)—檔案上傳配置、DispatcherServlet的路徑配置、請求和響應内容編碼
Spring MVC學習(11)—跨域的介紹以及使用CORS解決跨域問題
文章目錄
- Spring MVC學習 系列文章
- 1 Spring類型轉換機制概述
- 2 PropertyEditor
-
- 2.1 PropertyEditor的概述
- 2.2 内置的PropertyEditor
- 2.3 PropertyEditorManager
- 2.4 注冊自定義PropertyEditor
-
- 2.4.1 使用PropertyEditorRegistrar
-
- 2.4.1.1 共享配置
- 3 Converter
-
- 3.1 Converter SPI接口
- 3.2 使用ConverterFactory
- 3.3 使用GenericConverter
-
- 3.3.1 使用ConditionalGenericConverter
- 3.4 ConversionService API接口
- 3.5 配置ConversionService
-
- 3.5.1 配置BeanWarpper的ConversionService
-
- 3.5.1.1 直接使用ConversionService
- 3.5.2 配置DataBinder的ConversionService
- 4 Formarter
-
- 4.1 Formatter SPI
- 4.2 注解驅動格式化
- 4.3 FormatterRegistry SPI
- 4.4 FormatterRegistrar SPI
- 4.5 配置全局轉換器
- 5 HttpMessageConverter
-
- 5.1 配置MessageConverter
- 6 Spring MVC的DataBinder
1 Spring類型轉換機制概述
BeanWrapper
是一個Spring的内部體系,主要用于在建立bean執行個體之後填充bean的依賴,我們在此前
Spring IOC的源碼
的部分已經講過了,
BeanWrapper
對于大部分開者這來說都是無感覺的,被Spring内部使用,屬于一個底層的對象。
Spring中的類型轉換主要發生在兩個地方:
-
,在底層的BeanWrapper中注入Bean的屬性依賴的時候,如果對于找到的依賴類型(給定的常量值或者找到依賴對象)如果不符合屬性的具體類型,那麼需要轉換為對應的屬性類型;Spring建立bean執行個體時
- Spring MVC中,在執行處理器方法之前,可能需要把HTTP請求的資料通過DataBinder綁定到控制器
上,然而HTTP參數到後端時都是String類型,而方法參數可能是各種類型,這就可能涉及到從String到給定類型的轉換。方法的給定參數
Spring提供了三種最常見的資料類型轉換器PropertyEditor、Formatter、Converter,無論是Spring MVC的 DataBinder和底層的BeanWrapper都支援使用這些轉換器進行資料轉換:
-
是JDK自帶的類型轉換接口。主要用于實作String到其他類型的轉換。Spring已經提供了用于常見類型轉換的PropertyEditor實作。PropertyEditor
-
是Spring 3.0時提供的接口,隻能轉換String到其他類型,支援SPI機制。通常對于Spring MVC的參數綁定時的類型轉換使用Formatter就可以了。Spring已經提供了用于常見類型轉換的Formatter實作。Formatter
-
是Spring 3.0時提供的接口,可以提供從一個對象類型到另一個對象類型的轉換,支援SPI機制,當需要實作通用類型轉換邏輯時應該使用Converter。Spring已經提供了用于常見類型轉換的Converter實作。Converter
2 PropertyEditor
2.1 PropertyEditor的概述
在最開始,Spring 使用PropertyEditor(屬性編輯器)的概念來實作對象和字元串之間的轉換,PropertyEditor接口并非來自于Spring,而是來自Java的rt.jar核心依賴包,是JDK自帶的。屬性編輯器的作用我們在此前就說過了:
- 在Spring建立bean時将資料轉換為bean的屬性對應的類型。
- 在 Spring MVC 架構中分析、轉換HTTP 請求參數。
PropertyEditor接口的常見方法如下:
public interface PropertyEditor {
/**
* 設定(或更改)要編輯的對象。原始類型(如"int")必須包裝為相應的對象類型,如"java.lang.integer"
*
* @param value 要編輯的新目标對象。屬性編輯器不應修改此對象,而屬性編輯器應建立一個新對象來儲存任何修改的值
*/
void setValue(Object value);
/**
* 擷取屬性值。
*
* @return 屬性的值。原始類型(如"int")将包裝為相應的對象類型,如"java.lang.integer"
*/
Object getValue();
/**
* 為屬性提供一個表示初始值的字元串,屬性編輯器以此值作為屬性的預設值
*/
String getJavaInitializationString();
/**
* 擷取屬性值的可編輯的字元串表示形式
*
* @return 如果值不能表示為可編輯字元串,則傳回 null。如果傳回非 null 值,則屬性編輯器應準備好在 setAsText()中分析該字元串
*/
String getAsText();
/**
* 通過分析給定字元串來設定屬性值。如果字元串格式錯誤或此類屬性不能以文本形式表示,可能會引發 java.lang.IllegalArgumentException
*
* @param text 要解析的字元串。
*/
void setAsText(String text) throws java.lang.IllegalArgumentException;
}
雖然經常看見有文章說PropertyEditor僅用于支援從String到對象的轉換。但是實際上在目前的Spring版本中,早已支援通過PropertyEditor實作從對象到對象的轉換,典型的實作就是來自于spring-data-redis的各種PropertyEditor實作,比如ValueOperationsEditor,我們可以直接依賴ValueOperations,并且對其注入redisTemplate,Spring 在檢查到類型不一緻時,最終會在ValueOperationsEditor中通過注入的redisTemplate擷取ValueOperations并傳回。
支援從對象轉換為對象的核心方法就是PropertyEditor#setValue方法。
2.2 内置的PropertyEditor
盡管現在如果在需要自定義轉換器時,PropertyEditor被推薦使用Converter替代,但是我們仍然能夠配置并且正常使用自定義的PropertyEditor,并且Spring内部就是用了很多預設PropertyEditor。
Spring 擁有許多内置的
PropertyEditor
實作。它們都位于
org.springframework.beans.propertyeditors包中
。預設情況下,大多數(但不包括全部)由
BeanWrapperImpl
來注冊(位于
AbstractBeanFactory#initBeanWrapper
方法中,注冊之後被BeanWrapper用于建立和填充Bean執行個體的類型轉換)。很多預設屬性編輯器實作也都是可配置的,比如
CustomDateEditor,就可以指定日期模式
。下表列出了Spring提供的常見PropertyEditor:
類型 | 描述 |
---|---|
ByteArrayPropertyEditor | 位元組數組的編輯器。将String轉換為相應的byte[]表示形式。預設情況下由 BeanWrapperImpl 注冊。 |
ClassEditor | 支援表示類的字元串String解析與實際Class的互相轉換。當找不到類時,将抛出IllegalArgumentException。預設情況下,由BeanWrapperImpl注冊。 |
CustomBooleanEditor | boolean的可自定義屬性編輯器,将指定的String解析為boolean值。預設情況下,由 BeanWrapperImpl 注冊,但可以通過将其自定義執行個體注冊為自定義編輯器來覆寫。 |
CustomCollectionEditor | 集合的可自定義屬性編輯器,将任何源字元串或者集合轉換為給定的目标集合類型。預設情況下,由 BeanWrapperImpl 注冊,但可以通過将其自定義執行個體注冊為自定義編輯器來覆寫。 |
CustomDateEditor | java.util.Date 的可自定義屬性編輯器,支援自定義 DateFormat。預設情況下未注冊。必須由使用者根據需要使用适當的格式進行手動注冊。 |
CustomNumberEditor | 任何Number的子類(如Integer、 Long、 Float或 Double)的可自定義屬性編輯器。預設情況下,由BeanWrapperImpl注冊,但可以通過将其自定義執行個體注冊為自定義編輯器來覆寫。 |
FileEditor | 将字元串解析為 java.io.File 對象。預設情況下,由 BeanWrapperImpl 注冊。 |
InputStreamEditor | 将一個String通過中間的ResourceEditor和Resource生成一個InputStream。預設用法不會關閉inputstream。預設情況下,由BeanWrapperImpl注冊。 |
LocaleEditor | 可以實作String和Locale對象的互相轉換,字元串格式為[國家/地區],與Locale的 toString()方法相同)。預設情況下,由 BeanWrapperImpl 注冊。 |
PatternEditor | 可以實作String和java.util.regex.Pattern對象的互相轉換 |
PropertiesEditor | 可以将字元串轉換為Properties對象(使用java.util.Properties類的javadoc中定義的格式格式化)。預設情況下,由BeanWrapperImpl注冊。 |
StringTrimmerEditor | 修剪字元串的屬性編輯器,還可以(可選)将空字元串轉換為null值。預設情況下未注冊,必須是使用者手動注冊的。 |
URLEditor | 可以将URL字元串解析為實際 URL 對象。預設情況下,由 BeanWrapperImpl 注冊。 |
BeanWrapperImpl自動注冊的PropertyEditor位于PropertyEditorRegistrySupport#createDefaultEditors方法中,并且是在需要轉類型但是其他自定義轉換器中無法找到合适的轉換器的時候才會注冊的(convertIfNecessary方法内的findDefaultEditor方法),屬于延遲初始化!
2.3 PropertyEditorManager
Spring使用
java.beans.PropertyEditorManager
來注冊、搜尋可能的需要的PropertyEditor。搜尋路徑還包括rt.jar包中的sun.bean.editors,其中包括用于Font、Color和大多數基本類型的PropertyEditor實作。
另外,如果某個類型的類型轉換器與該類型的Class位于同一個包路徑,并且命名為ClassName+Editor,那麼當需要轉換為該類型時,将會自動發現該轉換器,而無需手動注冊,如下執行個體:
com
chank
pop
Something // Something類
SomethingEditor // 用于Something類的類型轉換,将會自動發現
一個管理預設屬性編輯器的管理器:PropertyEditorManager,該管理器内儲存着一些常見類型的屬性編輯器, 如果某個JavaBean的常見類型屬性沒有通過BeanInfo顯式指定屬性編輯器,IDE将自動使用PropertyEditorManager中注冊的對應預設屬性編輯器。
實際上我們前面說的spring-data-redis的各種PropertyEditor實作就是采用的這個機制取發現的,它們并沒有手動注冊:
當然,我們也可以使用标準的BeanInfo JavaBeans機制顯式的指定某個類與某個屬性編輯器的關系,如下執行個體:
com
chank
pop
Something
SomethingBeanInfo
2.4 注冊自定義PropertyEditor
Spring 預注冊了許多自定義屬性編輯器實作(例如,将表示為字元串的類名轉換為Class對象)。此外,Java 的标準 JavaBeans PropertyEditor查找機制允許對類的PropertyEditor進行适當命名,并放置在與它所支援的類相同的包中,以便可以自動找到它(上面講過了)。
Spring提供了PropertyEditor的一個核心實作類
PropertyEditorSupport
,如果我們要編寫自定義的屬性編輯器,隻需要繼承這個類即可,PropertyEditorSupport實作了
PropertyEditor
接口的所有方法,我們繼承PropertyEditorSupport之後隻需要重寫自己需要的方法即可,更加友善!
如果需要注冊其他自定義PropertyEditors,可以使用幾種機制:
- 最不建議、不友善是使用
,因為這需要我們擷取BeanFactory的引用。該方法将自定義的PropertyEditor直接注冊到ConfigurableBeanFactory接口的registerCustomEditor()方法
緩存中,等待後續BeanWrapper的擷取。AbstractBeanFactory的customEditors
- 另一種(稍微友善一點)的機制是使用
,它是一個特殊的CustomEditorConfigurer
,可以将自定義的PropertyEditor或者PropertyEditorRegistrar實作存入其内部的BeanFactoryPostProcessor
屬性中,啟動項目之後,它的customEditors和propertyEditorRegistrars
方法會在所有普通bean執行個體化和初始化之前(建立BeanWrapper之前)調用beanFactory來将這些postProcessBeanFactory
注冊到PropertyEditor和propertyEditorRegistrars
緩存。AbstractBeanFactory的customEditors和propertyEditorRegistrars
基于以上的配置,在Spring bean對應的BeanWrapper初始化時,會自動從
AbstractBeanFactory的customEditors和propertyEditorRegistrars
緩存中将自定義的PropertyEditor注冊到自己内部(位于
AbstractBeanFactory#initBeanWrapper
方法中),之後被
BeanWrapper用于建立和填充 Bean 執行個體的類型轉換。
但是請注意,這種配置不适用于Spring MVC的資料綁定,因為DataBinder預設不會查找這裡注冊到AbstractBeanFactory中的customEditors和propertyEditorRegistrars緩存,資料綁定時需要的自定義Editor必須在org.springframework.validation.DataBinder中手動注冊(通過Spring MVC的@InitBinder方法)。
如下案例,自定義了一個PropertyEditor,日期格式為“yyyy-MM-dd”:
/**
* @author lx
*/
public class DateEditor extends PropertyEditorSupport {
private String formatter = "yyyy-MM-dd";
@Override
public void setAsText(String text) throws IllegalArgumentException {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(formatter);
try {
Date date = simpleDateFormat.parse(text);
System.out.println("-----DateEditor-----");
//轉換後的值設定給PropertyEditorSupport内部的value屬性
setValue(date);
} catch (ParseException e) {
throw new IllegalArgumentException(e);
}
}
public DateEditor() {
}
public DateEditor(String formatter) {
this.formatter = formatter;
}
}
一個實體,需要将“2020-12-12”的字元串轉換為Date類型的屬性:
@Component
public class TestDate {
@Value("2020-12-12")
private Date date;
@PostConstruct
public void test() {
System.out.println(date);
}
}
将自定義的PropertyEditor注冊到CustomEditorConfigurer的customEditors屬性中,該屬性是Map<Class<?>, Class<? extends PropertyEditor>類型,即都是Class類型:
<bean
class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="java.util.Date" value="com.spring.mvc.config.DateEditor"/>
</map>
</property>
</bean>
啟動項目,即可看到輸出:
-----DateEditor-----
Sat Dec 12 00:00:00 CST 2020
通過在
CustomEditorConfigurer的customEditors屬性
中直接添加自定義編輯器的類型确實可以注冊,但是這種方法的缺點是無法為
自定義的PropertyEditor
指定初始化參數。
實際上在早期的Spring版本中,Map中的value類型是一個執行個體,是以它是支援自定義初始化參數的,但是因為PropertyEditor是有狀态的,如果多個BeanWrapper共用同一個PropertyEditor執行個體,那麼可能造成難以察覺的問題。是以,在新版本中customEditors屬性的Map的Value是Class類型,并且每個BeanWrapper在設定轉換器是都會建立屬于自己的PropertyEditor執行個體。如果想要需要控制PropertyEditor的執行個體化過程,比如設定初始化參數,那麼我們需要使用PropertyEditorRegistrar去注冊它們。
還有一個缺點是,
基于customEditors屬性配置的PropertyEditor無法與Spring MVC的資料綁定共享同樣的配置方式,即使它們都需要配置某個同樣的PropertyEditor
。
2.4.1 使用PropertyEditorRegistrar
PropertyEditorRegistrar
,顧名思義,它是一個PropertyEditor的“系統資料庫”,Spring中還有很多Registrar結尾的類,這種類通用作用就是用于注冊類名去除“Registrar”之後的資料,比如PropertyEditorRegistrar就是用于注冊PropertyEditor,它還有一個可選的特性就是,可以在一次方法調用中注冊多個執行個體并且更加靈活!
另外,PropertyEditorRegistrar執行個體與名為PropertyEditorRegistry的接口配合使用,而該接口又被Spring的BeanWrapper和 DataBinder都實作了,是以PropertyEditorRegistrar中的PropertyEditor配置很容易的被BeanWrapper和 DataBinder共享!
Spring為我們提供了一個PropertyEditorRegistrar的實作ResourceEditorRegistrar,如果我們要實作自己的PropertyEditorRegistrar,那麼可以參數該類,特别是它的registerCustomEditors方法。實際上ResourceEditorRegistrar将會被Spring自動預設注冊到容器中(位于prepareBeanFactory方法中),是以該類中的PropertyEditor通常會被所有的beanWarpper使用!
下面是一個自己的PropertyEditorRegistrar:
/**
* @author lx
*/
public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
private String formatter;
/**
* 傳遞一個PropertyEditorRegistry的實作,使用給定的PropertyEditorRegistry注冊自定義PropertyEditor
* BeanWrapperImpl和DataBinder都實作了PropertyEditorRegistry接口,傳遞的通常是 BeanWrapper 或 DataBinder。
* <p>
* 該方法僅僅是定義了注冊的流程,隻有當某個BeanWrapper 或 DataBinder實際調用時才會真正的注冊
*
* @param registry 将要注冊自定義PropertyEditor的PropertyEditorRegistry
*/
@Override
public void registerCustomEditors(PropertyEditorRegistry registry) {
// 預期将建立新的屬性編輯器執行個體,可以自己控制建立流程
registry.registerCustomEditor(Date.class, new DateEditor(formatter));
// 可以在此處注冊盡可能多的自定義屬性編輯器...
}
public String getFormatter() {
return formatter;
}
public void setFormatter(String formatter) {
this.formatter = formatter;
}
}
下面是如何配置CustomEditorConfigurer并注入CustomPropertyEditorRegistrar執行個體:
<bean
class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<!--propertyEditorRegistrars是一個數組,可以傳遞多個自定義的PropertyEditorRegistrar-->
<property name="propertyEditorRegistrars">
<array>
<ref bean="customPropertyEditorRegistrar"/>
</array>
</property>
</bean>
<!--自定義的CustomPropertyEditorRegistrar-->
<bean id="customPropertyEditorRegistrar" class="com.spring.mvc.config.CustomPropertyEditorRegistrar">
<property name="formatter" value="yyyy-MM-dd"/>
</bean>
啟動項目,同樣成功轉換:
-----DateEditor-----
Sat Dec 12 00:00:00 CST 2020
注意,Spring的目的就是讓每個BeanWarpper和DataBinder都初始化自己的PropertyEditor執行個體,這是為了防止多個執行個體共用一個有狀态的PropertyEditor導緻資料異常,如果你确定沒問題的話,也可以在PropertyEditorRegistrar中配置同一個PropertyEditor執行個體。
2.4.1.1 共享配置
配置PropertyEditorRegistrar之後,想要将這些PropertyEditor配置應用在Spring MVC的DataBinder中非常簡單,如下案例:
@Controller
public class RegistrarController {
@Resource
private CustomPropertyEditorRegistrar customPropertyEditorRegistrar;
@InitBinder
public void init(WebDataBinder binder) {
//調用registerCustomEditors方法向目前DateBinder注冊PropertyEditor
customPropertyEditorRegistrar.registerCustomEditors(binder);
}
//其他控制器方法
}
隻需要在控制器中引入
customPropertyEditorRegistrar
執行個體,然後在
@ initBinder方法
中調用
registerCustomEditors
方法并傳入
DataBinder
,即可将内部配置的PropertyEditor注冊到目前DataBinder中。
這種類型的PropertyEditor注冊方式可以産生簡潔的代碼(注冊多個PropertyEditor的實作隻有一行代碼),并允許将公共的PropertyEditor注冊代碼封裝在一個類中,然後根據需要在多個Controllers之間共享。
3 Converter
Spring 3.0引入了
core.convert
包,它提供了一般類型的轉換系統,作為
JavaBeans PropertyEditors
屬性編輯器的替代服務。
3.1 Converter SPI接口
相比于複雜的PropertyEditor接口,
org.springframework.core.convert.converter.Converter
是一個用于類型轉換的非常簡單且強大的SPI接口,翻譯成中文就是“轉換器”。
Converter
提供了核心的轉換行為!
@FunctionalInterface
public interface Converter<S, T> {
/**
* 将 S 類型的源對象轉換為目标類型 T 對象
*
* @param source 要轉換的源對象,它必須是 S 類型的執行個體(從不為null)
* @return 轉換後的對象,它必須是 T 類型的執行個體(可能為null)
* @throws IllegalArgumentException 如果源對象無法轉換為所需的目标類型
*/
@Nullable
T convert(S source);
}
要想建立自己的
Converter
,隻需要實作Converter接口,其中S表示要轉換的類型,T表示要轉換到的類型。
convert(S)
方法的每次調用,都應該保證輸入參數不為null。如果轉換失敗,轉換器可能會引發任何非受檢異常。在抛出異常時應該包裹在一個IllegalArgumentException中,同時我們必須保證Converter是
線程安全
的!
和PropertyEditor類似,為了友善起見,
Spring在core.convert.support
包中已經提供了非常多的
Converter轉換器實作
,其中包括從字元串到數字的轉換器和其它常見類型。
下面是一個典型的Converter轉換器實作:
public final class StringToInteger implements Converter<String, Integer> {
@Override
public Integer convert(String source) {
return Integer.valueOf(source);
}
}
3.2 使用ConverterFactory
Converter的類型轉換是明确的,倘若對有同一父類或接口的多個子類型需要進行類型轉化,為每個類型都寫一個Converter顯然是十分不理智的,當需要集中管理整個類層次結構的轉換邏輯時,可以實作
ConverterFactory
接口:
/**
* @param <S> 源類型
* @param <R> 目标類型的超類型
*/
public interface ConverterFactory<S, R> {
/**
* 擷取轉換器從 S 轉換為目标類型 T,其中 T 也是 R 的子類型。
*
* @param <T> 目标類型
* @param targetType 要轉換為的目标類型的Class
* @return 從 S 到 T 的轉換器
*/
<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}
參數S是需要轉換的類型,R是要轉換到的類的基類。然後實作
getConverter(Class)
方法,其中T是R的子類型。
ConverterFactory
用于實作一種類型到N種類型的轉換。
Spring已經提供了基本的
ConverterFactory的實作
:
3.3 使用GenericConverter
當需要定義
複雜的Converter實作
時,可以使用
GenericConverter
接口,GenericConverter并不是Converter的子接口,而是一個
獨立的頂級接口
,翻譯成中文就是“通用轉換器”!
與Converter相比,GenericConverter更靈活并且沒有強類型的簽名,它支援在多個源類型和目标類型之間進行轉換,用于實作N種類型到N種類型的轉換。此外,GenericConverter提供了可用的源和目标字段上下文(
TypeDescriptor
),你可以在實作轉換邏輯時使用它們。這樣的上下文允許類型轉換由字段注解或字段簽名上聲明的泛型資訊驅動類型轉換。
下面是GenericConverter的接口定義:
/**
* 用于在兩種或多種類型之間轉換的通用轉換器接口。
* <p>
* 這是最靈活的轉換器SPI接口,也是最複雜的。它的靈活性在于GenericConverter可能支援在多個源/目标類型對之間轉換
* 此外,GenericConverter的實作在類型轉換過程中可以通路源/目标字段上下文。
* 這允許解析源和目标字段中繼資料,如注解和泛型資訊,這些中繼資料可用于影響轉換邏輯。
*/
public interface GenericConverter {
/**
* 傳回所有此轉換器可以轉換的源類型和目标類型的ConvertiblePair
* 每個ConvertiblePair都表示一組可轉換的源類型以及目标類型。
* <p>
* 對于ConditionalConverter,此方法可能會傳回 null 以訓示應考慮所有ConvertiblePair
*/
@Nullable
Set<ConvertiblePair> getConvertibleTypes();
/**
* 将源對象轉換為TypeDescriptor(類型描述符)描述的目标類型。
*
* @param source 要轉換的源對象(可能是null)
* @param sourceType 正在轉換的字段的類型描述符
* @param targetType 要轉換為的字段的類型描述符
* @return 轉換的對象
*/
@Nullable
Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
/**
* 源類型到目标類型對的持有者
*/
final class ConvertiblePair {
private final Class<?> sourceType;
private final Class<?> targetType;
/**
* 建立一個新的ConvertiblePair
*
* @param sourceType 源類型
* @param targetType 目标類型
*/
public ConvertiblePair(Class<?> sourceType, Class<?> targetType) {
Assert.notNull(sourceType, "Source type must not be null");
Assert.notNull(targetType, "Target type must not be null");
this.sourceType = sourceType;
this.targetType = targetType;
}
public Class<?> getSourceType() {
return this.sourceType;
}
public Class<?> getTargetType() {
return this.targetType;
}
/*用于判斷是否已存在某個源類型到目标類型的組*/
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (other == null || other.getClass() != ConvertiblePair.class) {
return false;
}
ConvertiblePair otherPair = (ConvertiblePair) other;
return (this.sourceType == otherPair.sourceType && this.targetType == otherPair.targetType);
}
@Override
public int hashCode() {
return (this.sourceType.hashCode() * 31 + this.targetType.hashCode());
}
@Override
public String toString() {
return (this.sourceType.getName() + " -> " + this.targetType.getName());
}
}
}
GenericConverter
中擁有一個内部類
ConvertiblePair
,這個内部類用于封裝一種可轉換的源類型與目标類型對,一個GenericConverter可以擁有多個ConvertiblePair。
要實作GenericConverter,需要重寫
getConvertibleTypes()方法
以傳回受轉換支援的源類型到目标類型對,也就是ConvertiblePair。然後實作
convert(Object, TypeDescriptor, TypeDescriptor)方法
,該方法包含轉換的邏輯。源 TypeDescriptor(字段描述符)提供對儲存了要轉換值的源字段的通路。目标 TypeDescriptor提供對要設定轉換值的目标字段的通路。
TypeDescriptor
作為類型描述符,儲存了對應參數、字段的中繼資料,可以從其中擷取對應參數、字段的名字、類型、注解、泛型的資訊!
Spring已經提供了基本的GenericConverter的實作,一個很好的例子就是在Java數組和集合之間轉換的轉換器,比如
ArrayToCollectionConverter
,首先線它會建立對應的集合類型,然後在将數組元素存入集合中時,如有必要,會嘗試将數組元素類型轉換為集合元素的泛型類型!
3.3.1 使用ConditionalGenericConverter
如果覺得隻通過源類型和目标類型是否比對來判斷能夠支援轉換的方式太過簡單了,還需要在特定的條件成立時才支援轉換,比如可能希望在目标字段上存在指定的注解時才表示可以轉換,或者可能希望僅在目标字段的類型上定義了特定方法(如靜态的valueOf方法)時才表示可以轉換,此時我們可以使用
ConditionalGenericConverter
接口。
ConditionalGenericConverter是GenericConverter 和ConditionalConverter的結合
,允許自定義比對條件來确定是否能執行轉換!
/**
* 條件轉換器,允許有條件的執行轉換
* <p>
* 通常用于根據字段或類級别的特征(如注解或方法)的存在有選擇地比對自定義轉換邏輯。
* 例如,從 String 字段轉換為 Date 字段時,如果目标字段已使用@DateTimeFormat注解,則matches可能會傳回true
*/
public interface ConditionalConverter {
/**
* 是否可以應用從源類型到目前正在考慮的目标類型的轉換?
*
* @param sourceType 正在轉換的字段的類型描述符
* @param targetType 要轉換為的字段的類型描述符
* @return 如果應執行轉換,則為 true,否則為 false
*/
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}
/**
* ConditionalGenericConverter同時繼承了GenericConverter和ConditionalConverter接口,支援更複雜的判斷
*/
public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}
Spring已經提供了基本的ConditionalGenericConverter的實作
,大部分的實作都是用來處理有集合或數組參與的轉換,一個很好的例子同樣是
ArrayToCollectionConverter
,它會在
getConvertibleTypes
方法判斷源類型和目标類型滿足條件之後,繼續調用matches方法判斷源數組元素類型到底能否轉換為目标集合的元素類型,如果可以轉換,那麼才會真正的執行轉換的邏輯!
3.4 ConversionService API接口
由于整個Conversion機制的複雜性,Spring提供了
ConversionService
接口,該接口定義了一系列統一的API方法供外部調用,用于在運作時執行類型轉換,屏蔽了具體的内部調用邏輯。這是基于
門面(facade)設計模式
!
/**
* 用于類型轉換的服務接口。
* 這是進入轉換系統的入口,通過調用convert(Object, Class) 來使用這個系統執行線程安全的類型轉換。
*/
public interface ConversionService {
/**
* 如果源類型的對象可以轉換為目标類型,則傳回 true
* <p>
* 如果此方法傳回 true,則意味着convert(Object, Class) 方法能夠将源型的執行個體轉換為目标類型。
* <p>
* 對于集合、數組和map類型之間的轉換,此方法将傳回 true,即使轉換調用仍可能抛出ConversionException(如果基礎元素不可轉換)。
* 調用者在使用集合和map時應處理此特殊情況。
*
* @param sourceType 要轉換的源類型(如果源對象為 null,則可能為 null)
* @param targetType 要轉換為的目标類型(必需存在)
* @return 如果可以執行轉換,則為 true,如果不執行,則為 false
* @throws IllegalArgumentException 如果targetType目标類型為null
*/
boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
/**
* 如果源類型的對象可以轉換為目标類型,則傳回 true
* <p>
* 如果此方法傳回 true,則意味着convert(Object, TypeDescriptor, TypeDescriptor)方法能夠将源類型執行個體轉換為目标類型。
* <p>
* 對于集合、數組和map類型之間的轉換,此方法将傳回 true,即使轉換調用仍可能抛出ConversionException(如果基礎元素不可轉換)。
* 調用者在使用集合和map時應處理此特殊情況。
*
* @param sourceType 有關要轉換的源類型的上下文,也就是TypeDescriptor(如果源對象為 null,則可能為 null)
* @param targetType 要轉換為的目标類型的上下文,也就是TypeDescriptor(必需存在)
* @return 如果可以在源類型和目标類型之間執行轉換,則為 true,如果不執行,則為 false
* @throws IllegalArgumentException 如果targetType目标類型為null
*/
boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
/**
* 将給定的源對象轉換為指定的目标類型。
*
* @param source 要轉換的源對象(可能是 null )
* @param targetType 要轉換為的目标類型(必需存在)
* @return 轉換的對象,目标類型的執行個體
* @throws ConversionException 如果發生轉換異常
* @throws IllegalArgumentException 如果targetType目标類型為null
*/
@Nullable
<T> T convert(@Nullable Object source, Class<T> targetType);
/**
* 将給定的源對象轉換為指定的目标類型。
* TypeDescriptor提供有關發生轉換的源和目标位置(通常是對象字段或屬性位置)的其他上下文,從中可以擷取到字段名、類型、注解、泛型等資訊
*
* @param source 要轉換的源對象(可能是 null )
* @param sourceType 有關要轉換的源類型的上下文,也就是TypeDescriptor(如果源對象為 null,則可能為 null)
* @param targetType 要轉換為的目标類型的上下文,也就是TypeDescriptor(必需存在)
* @return 轉換的對象,目标類型的執行個體
* @throws ConversionException 如果發生轉換異常
* @throws IllegalArgumentException 如果目标類型為null,或源類型為null,但源對象不是null
*/
@Nullable
Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
}
ConversionService僅僅是一個用于類型轉換的服務接口,大多數ConversionService的實作還實作了ConverterRegistry接口(一個提供了注冊Converter方法的接口),它為注冊Converter轉換器提供了SPI機制。是以,
ConversionService的實作通常具有支援注冊多個Converter轉換器的方法。
/**
* 用于使用類型轉換系統注冊轉換器。
*/
public interface ConverterRegistry {
/**
* 向此系統資料庫添加一個普通轉換器,可轉換的可轉換源/目标類型對派生自轉換器的泛型參數類型。
*/
void addConverter(Converter<?, ?> converter);
/**
* 向此系統資料庫添加一個普通轉換器,已顯示指定可轉換的可轉換源/目标類型對
*/
<S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter);
/**
* 向此系統資料庫添加通用轉換器。
*/
void addConverter(GenericConverter converter);
/**
* 将範圍轉換器工廠添加到此系統資料庫。可轉換源/目标類型對派生自轉換器工廠的泛型參數類型。
*/
void addConverterFactory(ConverterFactory<?, ?> factory);
/**
* 删除對應 源/目标類型對 的所有Converter
*/
void removeConvertible(Class<?> sourceType, Class<?> targetType);
}
通常,需要進行類型轉換的時候,我們直接調用ConversionService的方法即可,而實際上的具體的轉換邏輯将會被委托給其内部注冊的轉換器執行個體!
Spring在
core.convert.support
包中提供了一系列健壯的conversionService實作和相關配置類。比如
GenericConversionService
是适用于大多數環境的通用實作,提供了配置Converter的功能。比如
ConversionServiceFactory
則是一個提供了為ConversionService注冊Converter的服務的通用工廠。
3.5 配置ConversionService
ConversionService是一個無狀态對象,被設計為在應用程式啟動時執行個體化,然後可以在多個線程之間共享。在Spring應用程式中,通常為每個Spring容器(或ApplicationContext)配置一個ConversionService執行個體。Spring接收轉換服務,并在架構需要執行類型轉換時使用它。我們還可以将此ConversionService注入到任何bean中,并直接調用它的轉換方法。
3.5.1 配置BeanWarpper的ConversionService
如果想要手動注冊全局生效的預設ConversionService,那麼需要将id命名為“
conversionService
”。在容器的
finishBeanFactoryInitialization
方法初始化全部普通bean執行個體之前,Spring容器會首先初始化這個
名為“conversionService”的ConversionService
,并且設定到
AbstractBeanFactory的conversionService屬性
中。
在後續的
BeanWarpper
的初始化方法中(
AbstractBeanFactory#initBeanWrapper方法
中),會擷取這個注冊的
conversionService
并儲存在自己的内部,用于後續的屬性填充的類型轉換服務:
對于的
普通spring項目
來說,不會注冊預設的ConversionService,是以底層BeanWarpper的ConversionService為
null
,對于
boot項目
則會預設注冊一個
ApplicationConversionService
服務。是以說,如果未在Spring中注冊ConversionService,則BeanWarpper會使用基于
PropertyEditor
屬性編輯器的原始轉換系統。
下面是注冊一個預設ConversionService的示例:
<bean id="conversionService" class="org.springframework.context.support.Con
versionServiceFactoryBean"/>
這裡我們并沒有直接配置ConversionService,而是配置的是一個
ConversionServiceFactoryBean
對象,它是一個
FactoryBean
類型的對象, 通過工廠模式來生産
ConversionService
,這也符合Spring的一貫作風,比較重且複雜的類構造起來一般采用工廠模式,它生産的對象實際類型就是
DefaultConversionService
。
在建立DefaultConversionService時,其預設會注冊一些常用的Converter,如果要用自己的自定義轉換器補充或重寫預設轉換器,那麼可以設定
ConversionServiceFactoryBean的converters屬性
,該屬性值可以配置為
任何Converter、ConverterFactory或GenericConverter的實作
。
如下是一個自定義的Converter實作:
/**
* 把字元串轉換Date
*
* @author lx
*/
@Component
public class StringToDateConverter implements Converter<String, Date> {
/**
* String source 傳入進來字元串
*
* @param source 傳入的要被轉換的字元串
* @return 轉換後的格式類型
*/
@Override
public Date convert(String source) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date parse;
try {
parse = dateFormat.parse(source);
System.out.println("--------StringToDateConverter---------");
} catch (ParseException e) {
throw new IllegalArgumentException(e);
}
return parse;
}
}
然後将其配置到ConversionServiceFactoryBean中即可:
<!--配置類型轉換服務工廠,它會預設建立DefaultConversionService,并且支援注入自定義
類型轉換器之外-->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<!--注入自定義轉換器執行個體-->
<ref bean="stringToDateConverter"/>
</set>
</property>
</bean>
3.5.1.1 直接使用ConversionService
我們可以直接在程式中以程式設計式的使用DefaultConversionService并且不需要建立DefaultConversionService的執行個體,因為DefaultConversionService實作了
懶加載的單例模式
,我們可以通過
getSharedInstance()
方法直接擷取共享執行個體。
public class DefaultConversionService extends GenericConversionService {
@Nullable
private static volatile DefaultConversionService sharedInstance;
public static ConversionService getSharedInstance() {
DefaultConversionService cs = sharedInstance;
if (cs == null) {
synchronized (DefaultConversionService.class) {
cs = sharedInstance;
if (cs == null) {
cs = new DefaultConversionService();
sharedInstance = cs;
}
}
}
return cs;
}
//…………
}
3.5.2 配置DataBinder的ConversionService
上面注冊的預設全局ConversionService僅适用于
BeanWarpper
,對于
Spring MVC的DataBinder
無效,因為DataBinder初始化并在綁定ConversionService時不會使用
AbstractBeanFactory的conversionService屬性
,DataBinder的ConversionService有另外的配置方法:
- 對于
來說,配置XML配置
标簽即表示會預設使用一個<mvc:annotation-driven>
執行個體,可以通過DefaultFormattingConversionService
屬性指定某個conversion-service
執行個體。ConversionService
- 對于
來說,通過加入JavaConfig配置
也能注冊一個預設的@EnableWebMvc注解
,而注冊一個DefaultFormattingConversionService
即表示替代這個預設的id為mvcConversionService的ConversionService
。DefaultFormattingConversionService
- 如果沒有這兩種配置,那麼DataBinder同樣沒有ConversionService。
如下配置,使得BeanWarpper和DataBinder都是用同一個conversionService:
<!--conversion-service屬性指定在字段綁定期間用于類型轉換的轉換服務的bean名稱。 -->
<!--如果不指定,則表示注冊預設DefaultFormattingConversionService-->
<mvc:annotation-driven conversion-service="conversionService"/>
<!--配置類型轉換服務工廠,它會預設建立DefaultConversionService,并且支援注入自定義類型轉換器之外-->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<!--注入自定義轉換器執行個體-->
<ref bean="stringToDateConverter"/>
</set>
</property>
</bean>
關于
DefaultFormattingConversionService
,我們下面會介紹!
4 Formarter
如上節所述,
core.convert
是一種通用類型轉換系統。它提供了一個統一的ConversionService API 以及一個強類型的Converter SPI,用于實作從一種類型到另一種類型的轉換邏輯,甚至提供了更多擴充功能的GenericConverter以及ConditionalGenericConverter,Spring通常建議使用此系統為beanWarpper綁定bean屬性值。此外,Spring表達式語言(SPEL)和DataBinder都可以使用此系統綁定字段值。例如,當SPEL需要将Short強制轉換為Long以完成expression.setValue(Object bean, Object value)操作時,core.convert系統将執行強制轉換。
在
Spring MVC
中,HTTP中的源資料都是
String
類型,資料綁定需要将String轉換為其他類型,同時也可能需要将資料轉換為具有本地格式的字元串樣式進行展示,core.convert中更通用的Converter SPI不能直接滿足這種格式要求。為了解決這些問題,Spring 3 引入了一個友善的
Formatter SPI
,用于在web環境下替代PropertyEditor。
通常,當需要實作通用類型轉換邏輯時,可以使用Converter SPI,例如,在java.util.Date和Long之間轉換。當在用戶端環境(如Web應用程式)中工作并且需要解析和輸出本地化字段值時,可以使用
Formatter SPI
。ConversionService為兩個SPI提供統一的類型轉換API。
4.1 Formatter SPI
org.springframework.format.Formatter
是一個用于實作字段格式化邏輯的非常簡單并且是強類型的SPI接口。
Formatter繼承了Printer和Parser接口,下面是這兩個接口的定義:
@FunctionalInterface
public interface Printer<T> {
/**
* 列印類型 T 的對象以進行顯示
*
* @param object 要列印的執行個體
* @param locale 目前使用者的區域(本地化)設定
* @return 列印的文本字元串
*/
String print(T object, Locale locale);
}
@FunctionalInterface
public interface Parser<T> {
/**
* 分析文本字元串以生成 T
*
* @param text 文本字元串
* @param locale 目前使用者區域設定
* @return T 的執行個體
* @throws ParseException 當 java.text 解析庫中發生解析異常時
* @throws IllegalArgumentException 當發生解析異常時
*/
T parse(String text, Locale locale) throws ParseException;
}
如果要建立自己的Formatter, 需要實作上面提到的
Formatter
接口以完成T類型對象的格式化和解析功能。
實作
print()
方法根據用戶端的區域設定以列印T執行個體,實作
parse()
方法根據用戶端的區域設定以及文本字元串中分析 T 的執行個體。如果解析嘗試失敗,則 Formatter 應引發ParseException或IllegalArgumentException異常。注意確定 Formatter 實作是線程安全的。
org.springframework.format.support子包
提供了常見友善使用的Formatter實作。
org.springframework.format.number子包
提供了NumberStyleFormatter、 CurrencyStyleFormatter和PercentStyleFormatter來格式化Number對象,其内部使用java.text.NumberFormat。
org.springframework.format.number.money
提供了與JSR-354內建的針對貨币的格式化器,比如CurrencyUnitFormatter、MonetaryAmountFormatter。
org.springframework.format.datetime子包
提供了DateFormatter,内部使用java.text.DateFormat來格式化java.util.Date對象。org.springframework.format.datetime.joda子包基于joda時間庫提供全面的datetime格式支援。
4.2 注解驅動格式化
字段格式化可以按字段類型或注解進行配置,要綁定一個注解到Formatter,可以實作
AnnotationFormatterFactory
接口。
BeanWarpper和DataBinder
都支援通過自定義注解驅動類型轉換,但是請注意Spring MVC請求資料綁定時隻能對某個完整變量(URI路徑變量、請求參數、請求體資料)進行注解驅動類型轉換,如果是
@RequestBody
之類的請求或者使用一個變量表示一個實體時,變量内部的資料不支援注解驅動類型轉換。
/**
* 用于建立formatter以格式化使用特定注解的字段值的工廠。
* <p>
* 例如,DateTimeFormatAnnotationForMatterFactory 可能會建立一個formatter,該formatter對使用@DateTimeForma注解的字段格式化為Date類型
*
* @param <A> 應觸發格式化的注解的類型
*/
public interface AnnotationFormatterFactory<A extends Annotation> {
/**
* 可使用A類型注解的字段類型。
*/
Set<Class<?>> getFieldTypes();
/**
* 擷取 Printer 以 print 具有指定注解的fieldType類型的字段值
*
* @param annotation 注解執行個體
* @param fieldType 注解的字段類型
* @return the printer
*/
Printer<?> getPrinter(A annotation, Class<?> fieldType);
/**
* 擷取 Parser 以 parse 有指定注解的fieldType類型的字段值
*
* @param annotation 注解執行個體
* @param fieldType 注解的字段類型
* @return the parser
*/
Parser<?> getParser(A annotation, Class<?> fieldType);
}
泛型A表示與格式化邏輯關聯的注解,例如
org.springframework.format.annotation.DateTimeFormat
,
getFieldTypes()
方法傳回可在其上使用注解的字段類型。
getprinter()
傳回Printer以列印注解字段的值。
getParser()
傳回一個Parser來解析注解字段的clientValue。
org.springframework.format.annotation
包中提供了
AnnotationFormatterFactory
關聯的注解的實作:
-
用格式化Number類型的字段(比如Double、Long),對應NumberFormatAnnotationFormatterFactory、Jsr354NumberFormatAnnotationFormatterFactory。@NumberFormat
-
用于格式化java.util.Date、java.util.Calendar、Long(時間戳毫秒)以及JSR-310 java.time 和 Joda-Time 值類型。對應DateTimeFormatAnnotationFormatterFactory、JodaDateTimeFormatAnnotationFormatterFactory、Jsr310DateTimeFormatAnnotationFormatterFactory。@DateTimeFormat
使用時也很簡單,開啟Spring mvc配置之後,Spring MVC預設會注冊這些AnnotationFormatterFactory,我們可直接使用上面的注解。
下面的示例使用@DateTimeFormat将前端傳遞的yyyy-MM-dd類型的日期字元串參數格式化為Date!
public class MyDate {
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date date;
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
}
一個控制器方法:
@RequestMapping("/dateTimeFormat/{date}")
@ResponseBody
public MyDate annotationFormatterFactory(MyDate date) {
System.out.println(DateFormat.getDateTimeInstance().format(date.getDate()));
return date;
}
通路/dateTimeFormat/2021-01-29,可以看到如下輸出:
說明格式化成功,此時頁面展示的JSON字元串樣式如下:
4.3 FormatterRegistry SPI
org.springframework.format.FormatterRegistry
是可用于注冊formatters的SPI接口,它還繼承了
ConverterRegistry
,是以同樣可以注冊converters
public interface FormatterRegistry extends ConverterRegistry {
void addPrinter(Printer<?> printer);
void addParser(Parser<?> parser);
void addFormatter(Formatter<?> formatter);
void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);
}
實際上,上面看到的FormatterRegistry提供的注冊Formatter、Parser、Printer、AnnotationFormatterFactory的方法在内部會被轉換為注冊對應的Converter,因為它們的功能的本質都是相同的,并且Converter的功能涵蓋了Formatter所有功能!是以Formatter和Converter可以看作是同一個底層類的不同表層展示:
FormatterRegistry SPI 允許我們集中配置格式規則,而不是在控制器之間複制這樣的配置。例如,可能希望強制所有日期字段以某種方式格式化,或者具有特定注解的字段以某種方式格式化。使用共享的FormatterRegistry,我們隻需定義一次這些規則,這些規則在需要格式化時會自動使用。
FormattingConversionService是一個适用于大多數環境的FormatTerregistry的實作。并且因為FormattingConversionService還繼承了GenericConversionService,是以可以直接使用FormattingConversionService将Converter和Formatter都配置在一起。
Spring MVC預設使用的
DefaultFormattingConversionService
就實作了FormattingConversionService。
4.4 FormatterRegistrar SPI
org.springframework.format.FormatterRegistrar
是一個用于為FormatterRegistry注冊formatters和converters的通用SPI接口,它類似于前面學習的PropertyEditorRegistrar,可
一次性注冊多個formatter和converter
,在為給定格式(如日期格式)注冊多個相關converters和formatters時,FormatterRegistrar非常有用。
public interface FormatterRegistrar {
/**
* 為FormatterRegistry注冊formatters和converters
*
* @param registry 要使用的 FormatterRegistry 執行個體
*/
void registerFormatters(FormatterRegistry registry);
}
4.5 配置全局轉換器
我們在“配置DataBinder的ConversionService”的部分就說過了,在開啟MVC配置之後,DataBinder會預設使用一個DefaultFormattingConversionService作為conversionService,當然我們也可以配置自定義的conversionService。
在學習Fromatter之後,我們可以使用
FormattingConversionServiceFactoryBean
工廠而不是ConversionServiceFactoryBean來作為一個真正的BeanWrapper和DataBinder 都共用的conversionService,因為它支援更多特性,比如同時支援注冊formatters和converters!
下面我們提供一個簡單的自定義的全局conversionService配置,其内部通過FormatterRegistrar注冊兩個AnnotationFormatterFactory執行個體,以期待實作基于注解的格式化轉換!
自定義兩個注解:
/**
* @author lx
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface PersonFormat {
}
/**
* @author lx
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface SexFormat {
String value() default "";
}
Person實體,其内部的sex字段标注了@SexFormat注解,該類用來測試Spring MVC的DataBinder的資料綁定:
/**
* @author lx
*/
public class Person {
private Long id;
private String tel;
private Integer age;
@SexFormat
private String sex;
public Person(Long id, String tel, Integer age, String sex) {
this.id = id;
this.tel = tel;
this.age = age;
this.sex = sex;
}
public Person() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTel() {
return tel;
}
public void setTel(String tel) {
this.tel = tel;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
PersonFormatter的實作,用于将前端傳遞的person字元串轉換為Person,我們約定前段傳遞的person字元串使用“|”分隔:
/**
* @author lx
*/
public class PersonFormatter implements Formatter<Person> {
@Override
public Person parse(String text, Locale locale) throws ParseException {
String[] split = text.split("\\|");
if (split.length == 4) {
return new Person(Long.valueOf(split[0]), split[1], Integer.valueOf(split[2]), split[3]);
}
throw new ParseException("參數格式不正确:" + text, 0);
}
@Override
public String print(Person object, Locale locale) {
return object.toString();
}
}
SexFormatter的實作,用于将性别的數字轉換為性别字元串:
public class SexFormatter implements Formatter<String> {
private static final String MAN = "男";
private static final String WOMAN = "女";
private static final String OTHER = "未知";
@Override
public String parse(String text, Locale locale) {
if ("0".equals(text)) {
return MAN;
}
if ("1".equals(text)) {
return WOMAN;
}
return OTHER;
}
@Override
public String print(String object, Locale locale) {
return object;
}
public static class WomanFormatter extends SexFormatter {
@Override
public String parse(String text, Locale locale) {
return WOMAN;
}
}
public static class ManFormatter extends SexFormatter {
@Override
public String parse(String text, Locale locale) {
return MAN;
}
}
}
PersonAnnotationFormatterFactory的實作:
/**
* @author lx
*/
@Component
public class PersonAnnotationFormatterFactory implements AnnotationFormatterFactory<PersonFormat> {
Set<Class<?>> classSet = Collections.singleton(Person.class);
@Override
public Set<Class<?>> getFieldTypes() {
return classSet;
}
@Override
public Parser<?> getParser(PersonFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation);
}
@Override
public Printer<?> getPrinter(PersonFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation);
}
private Formatter<Person> configureFormatterFrom(PersonFormat annotation) {
return new PersonFormatter();
}
}
SexAnnotationFormatterFactory的實作:
/**
* @author lx
*/
@Component
public class SexAnnotationFormatterFactory implements AnnotationFormatterFactory<SexFormat> {
Set<Class<?>> classSet = Collections.singleton(String.class);
@Override
public Set<Class<?>> getFieldTypes() {
return classSet;
}
@Override
public Parser<?> getParser(SexFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation);
}
@Override
public Printer<?> getPrinter(SexFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation);
}
private Formatter<String> configureFormatterFrom(SexFormat annotation) {
String value = annotation.value();
if ("0".equals(value)) {
return new SexFormatter.ManFormatter();
}
if ("1".equals(value)) {
return new SexFormatter.WomanFormatter();
}
return new SexFormatter();
}
}
一個Controller控制器,用于測試Spring MVC的DataBinder,同時其内部有一個sex屬性标注了@SexFormat注解,該類用來測試Spring 的BeanWrapper:
/**
* @author lx
*/
@RestController
public class AnnotationFormatterFactoryController {
/*用于測試DataBinder的資料轉換*/
@RequestMapping("/annotationFormatterFactory/{person}")
@ResponseBody
public Person annotationFormatterFactory(@PersonFormat Person person, @SexFormat String sex) {
System.out.println(sex);
return person;
}
/*用于測試BeanWrapper的資料轉換*/
@SexFormat("2")
@Value("1")
private String sex;
@PostConstruct
public void test() {
System.out.println(sex);
}
}
一個自定義的FormatterRegistrar,将兩個自定義的AnnotationFormatterFactory注冊到FormatterRegistry中:
/**
* @author lx
*/
@Component
public class CustomFormatterRegistrar implements FormatterRegistrar {
@Resource
private PersonAnnotationFormatterFactory personAnnotationFormatterFactory;
@Resource
private SexAnnotationFormatterFactory sexAnnotationFormatterFactory;
@Override
public void registerFormatters(FormatterRegistry registry) {
registry.addFormatterForFieldAnnotation(personAnnotationFormatterFactory);
registry.addFormatterForFieldAnnotation(sexAnnotationFormatterFactory);
}
}
下面是配置檔案,Spring的BeanWrapper和Spring MVC的DataBinder都支援該conversionService:
<!--conversion-service屬性指定在字段綁定期間用于類型轉換的轉換服務的bean名稱。 -->
<!--如果不指定,則表示注冊預設DefaultFormattingConversionService-->
<mvc:annotation-driven conversion-service="conversionService"/>
<!--配置工廠,它會預設建立DefaultFormattingConversionService,并且支援注入自定義converters和formatters-->
<!--如果将它命名為conversionService,那麼BeanWrapper和DataBinder 都共用此conversionService-->
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<!--注入自定義formatters,支援Formatter 和 AnnotationFormatterFactory的執行個體-->
<property name="formatters">
<set/>
</property>
<!--注入自定義converters,支援Converter、ConverterFactory、GenericConverter的執行個體-->
<property name="converters">
<set/>
</property>
<!--注入自定義的formatterRegistrars-->
<property name="formatterRegistrars">
<set>
<ref bean="customFormatterRegistrar"/>
</set>
</property>
</bean>
啟動項目,我們即發現了輸出了“女”字元,說我們配置的conversionService對于Spring BeanWrapper的屬性資料轉換成功生效!
通路/annotationFormatterFactory/1234134|123456|11|1,結果如下:
成功的從指定格式的字元串轉換Person參數執行個體,Spring MVC的DataBinder測試成功,但是我們發現其内部的sex屬性卻還是1,并沒有轉換為性别字元串,為什麼呢?因為Spring MVC請求資料綁定時隻能對某個完整變量(URI路徑變量、請求參數、請求體資料)進行整體的注解驅動類型轉換,比如上面的person路徑變量就可以整體轉換為Person對象成功,但是變量内部的部分資料不支援注解驅動類型轉換,比如上面的性别資料辨別符“1”位于person變量字元串内部,這樣就不能應用注解驅動類型轉換!
如果想要處理,那麼可以添加一個單獨的請求參數“sex”。通路/annotationFormatterFactory/1234134|123456|11|1?sex=1,結果如下:
成功的進行了内部屬性的轉換!
5 HttpMessageConverter
HttpMessageConverter同樣Spring 3.0加入的一個Converter,但是它不屬于org.springframework.core.convert體系,而是位于org.springframework.http.converter包,它不會參與BeanWrapper、DataBinder、SPEL中的類型轉換操作,它常被用在HTTP用戶端(比如RestTemplate)和服務端(比如Spring MVC REST風格的controllers)的資料轉換中,較長的描述如下:
- 在Spring MVC的控制器方法中,如果使用
等設定請求參數時,将會采用@RequestBody、HttpEntity<B>、@RequestPart
完成請求正文(請求體)到方法參數的類型轉換,并且這裡的轉換操作。HttpMessageConverter
- 在Spring MVC的控制器方法中,如果使用
等設定響應時,将會采用@ResponseBody、HttpEntity<B>, ResponseBodyEmitter、SseEmitterResponseEntity<B>
對傳回的實體對象執行轉換操作并寫入響應正文(響應體)。HttpMessageConverter
- 在通過
進行遠端HTTP調用的時候,将會通過RestTemplate
将調用傳回的響應正文(響應體)轉換為指定類型的對象,開發者可以直接擷取轉換後的對象。HttpMessageConverter
對于請求體的轉換操作發生在DataBinder建立之前。某個請求體或者響應體使用什麼哪種HttpMessageConverter來進行轉換,與請求或者響應中的media type(MIME,媒體類型)有關,Spring MVC已經提供了主要的媒體類型的HttpMessageConverter實作,預設情況下,HttpMessageConverter在用戶端的 RestTemplate和伺服器端的 RequestMappingHandlerAdapter中注冊。
所有轉換器會支援各自預設的媒體類型,但可以通過設定supportedMediaTypes屬性來覆寫它。
下面是HttpMessageConverter的常見實作以及簡介:
MessageConverter | 描述 |
---|---|
StringHttpMessageConverter | 可以從 HTTP 請求和響應讀取和寫入字元串資料。預設情況下,此轉換器支援所有媒體類型(/),并使用Content-Type為text/plain的内容類型進行寫入。 |
FormHttpMessageConverter | 可以從 HTTP 請求和響應讀取和寫入表單資料。預設情況下,此轉換器支援讀取和寫入application/x-www-form-urlencoded媒體類型MultiValueMap<String, String>。預設還支援寫“multipart/form-data”和"multipart/mixed"媒體類型的資料MultiValueMap<String, Object>,但是無法讀取這兩個請求的資料,也就是說無法支援檔案上傳。如果想要支援multipart/form-data媒體類型的請求,請配置MultipartResolver元件 |
ByteArrayHttpMessageConverter | 可以從 HTTP 請求和響應讀取和寫入byte[]位元組數組。預設情況下,此轉換器支援所有媒體類型 (/),并使用Content-Type為application/octet-stream的内容類型進行寫入。這可以通過設定受支援媒體類型屬性來覆寫。 |
MarshallingHttpMessageConverter | 可以從 HTTP 請求和響應通過org.springframework.oxm包中的Marshaller和Unmarshaller來讀取和寫入XML資料。預設情況下,此轉換器支援text/xml 和application/xml。 |
MappingJackson2HttpMessageConverter | 最常見的Converter。可以從 HTTP 請求和響應通過Jackson 2.x 的ObjectMapper讀取和寫入Json資料。可以使用Jackson提供的注解根據需要自定義 JSON 映射規則(比如@JsonView)。當需要進一步控制JSON序列化和反序列化規則時,可以自定義ObjectMapper的實作并通過該Converter的objectMapper屬性注入。預設情況下,此轉換器支援application/json。 |
MappingJackson2XmlHttpMessageConverter | 可以從 HTTP 請求和響應通過通過Jackson XML 擴充的 XmlMapper 讀取和寫入 XML資料。可以使用JAXB或Jackson提供的注解根據需要自定義 XML 映射規則。當需要進一步控制XML序列化和反序列化規則時,可以自定義XmlMapper的實作并通過該Converter的objectMapper屬性注入。預設情況下,此轉換器支援application/xml。 |
SourceHttpMessageConverter | 可以從 HTTP 請求和響應讀取和寫入javax.xml.transform.Source資料,僅支援 DOMSource、SAXSource 和 StreamSource類型。預設情況下,此轉換器支援text/xml和application/xml。 |
BufferedImageHttpMessageConverter | 可以從 HTTP 請求和響應讀取和寫入java.awt.image.BufferedImage資料。預設情況下,此轉換器可以讀取ImageIO#getReaderMIMETypes()方法傳回的所有媒體類型,并且使用ImageIO#getWriterMIMETypes()方法傳回的第一個可用的媒體類型進行寫入。 |
5.1 配置MessageConverter
在通過注解和JavaConfig開啟MVC配置之後,我們可以通過重寫
configureMessageConverters方法
來替換Spring MVC建立的預設轉換器,比如配置FastJson轉換器,或者重寫
extendMessageConverters方法
來擴充或者修改轉換器!
圖中的
addDefaultHttpMessageConverters方法
用于注冊預設的轉換器,從該方法的源碼中能夠得知,如果存在jackson的依賴,那麼會自動注冊
MappingJackson2HttpMessageConverter
:
如下JavaConfig配置:
/**
* @author lx
*/
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
.indentOutput(true)
.dateFormat(new SimpleDateFormat("yyyy-MM-dd"));
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
}
}
這表示我們自定義配置的
MappingJackson2HttpMessageConverter
在序列化和反序列化Date時僅支援“
yyyy-MM-dd
”格式。
以下XML配置,可達到和JavaConfg配置同樣的效果:
<mvc:annotation-driven>
<!--配置自定義的消息轉換器-->
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<!--配置objectMapper-->
<property name="objectMapper">
<bean class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"
p:indentOutput="true"
p:simpleDateFormat="yyyy-MM-dd"/>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
推薦使用JavaConfg的配置!
6 Spring MVC的DataBinder
和此前介紹的
@ModelAttribute
方法類似,Spring MVC的
@InitBinder
方法同樣在控制器方法之前執行,并且同樣支援與@RequestMapping控制器方法相同的許多參數。但是,它們通常使用
WebDataBinder
參數(用于注冊類型轉換器)和并且傳回值為void。
和此前介紹的@ModelAttribute方法類似,@Controller類和@ControllerAdvice類中都可以定義@InitBinder方法,@Controller類中的@InitBinder方法預設支援範圍是目前類的請求,@ControllerAdvice類中方法預設支援範圍是所有的請求!
@InitBinder方法有如下作用:
- 将請求參數(即表單或查詢資料)綁定到model對象,這和@ModelAttribute差不多,但不是它的主要功能。
- 通過注冊類型轉換器,用于将基于字元串的請求值(例如請求參數,路徑變量,請求頭,Cookie等)轉換為控制器方法參數的目标類型。
每次的請求,在HandlerAdapter的handler方法内都會建立一個WebDataBinder對象:
建立WebDataBinder之後,會嘗試添加預配置的
conversionService
,如果
開啟了Spring mvc的配置(通過@EnableWebMvc注解或者<mvc:annotation-driven/>标簽
,這個conversionService的配置我們在上面就講過了)并且沒有更改,那麼預設就是一個D
efaultFormattingConversionService
,其内部封裝了常見的全局可用的Converter和Formarter(Formatter會被轉換為Converter),這個conversionService中的轉換器是
全局可用的
!
随後回調所有符合規則的@InitBinder方法,然後我們可以在該方法中對WebDataBinder對象中注冊适用于目前請求的自定義的PropertyEditor和Formatter轉換器(Formatter會被轉換為PropertyEditor),但是這裡注冊的轉換器将值綁定到該Binder對象本身,也就是說每一次請求中通過WebDataBinder的@InitBinder方法注冊的轉換器是不可複用的。
在解析時,首先使用注冊的局部PropertyEditor來解析,然後在使用全局的conversionService來解析!
如下案例,有一個Controller:
@RestController
public class InitBinderController {
@GetMapping("/initBinder")
public void handle(Date date) {
System.out.println(date);
}
}
如果我們通路/initBinder?date=2012/12/12,結果如下:
可以發現,可以實作字元串到時間的轉換,這是因為預設的conversionService提供了這種格式的字元串到Date類型的Converter,如果我們嘗試換一種格式呢?
如果我們通路/initBinder?date=2012-12-12,結果如下:
此時直接抛出異常了!因為字元串格式不比對,此時我們可以自定義類型轉換器,如上面所講,我們可以定義三種轉換器,如果在@InitBinder方法中注冊,那麼我們可以注冊PropertyEditor和Formatter這兩種類型的轉換器!
對于這種時間字元串轉換為Date的轉換器,PropertyEditor和Formatter都已經提供了各自的實作,我們隻需要傳入給定的字元串模式即可,當然也可以自己實作!
如果我們注冊PropertyEditor,那麼如下聲明:
@RestController
public class InitBinderController {
@InitBinder
public void initBinder(WebDataBinder binder) {
System.out.println("----initBinder------");
//注冊一個自定義的PropertyEditor
//第一個參數表示轉換後屬性的類型,第二個參數是自定義的PropertyEditor的執行個體
//這個CustomDateEditor是Spring内置的專門用于格式化時間的PropertyEditor,我們隻需要設定時間字元串的模式即可
binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
}
@GetMapping("/initBinder")
public void handle(Date date) {
System.out.println(date);
}
}
再次通路/initBinder?date=2012-12-12,結果如下:
----initBinder------
Wed Dec 12 00:00:00 CST 2012
成功的進行了轉換!
如果我們注冊Formatter,那麼如下聲明:
@InitBinder
public void initBinder(WebDataBinder binder) {
System.out.println("----initBinder------");
//注冊一個自定義的Formatter
//這個DateFormatter是Spring内置的專門用于格式化時間的Formatter
//隻需要在構造器參數中設定時間字元串的模式即可,這裡并沒有設定,因為DateFormatter内置了本地模式支援解析yyyy-MM-dd
binder.addCustomFormatter(new DateFormatter());
}
再次通路/initBinder?date=2012-12-12,結果如下:
----initBinder------
Wed Dec 12 00:00:00 CST 2012
同樣成功的進行了轉換!
相關文章:
https://spring.io/
Spring Framework 5.x 學習
Spring Framework 5.x 源碼
如有需要交流,或者文章有誤,請直接留言。另外希望點贊、收藏、關注,我将不間斷更新各種Java學習部落格!