目錄
- ?前言
- 版本約定
- ?正文
- PropertyEditor設計缺陷
- 新一代型別轉換
- Converter
- 代碼示例
- 不足
- ConverterFactory
- 代碼示例
- 不足
- GenericConverter
- 代碼示例
- 不足
- ConditionalConverter
- ?總結
- ???推薦閱讀???
- ?關注A哥?

分享、成長,拒絕淺藏輒止,關注公眾號【BAT的烏托邦】,回復關鍵字
專欄有Spring技術堆疊、中間件等小而美的原創專欄供以免費學習,本文已被 https://www.yourbatman.cn 收錄,
?前言
你好,我是YourBatman,
上篇文章 介紹完了Spring型別轉換早期使用的PropertyEditor詳細介紹,關于PropertyEditor現存的資料其實還蠻少的,希望這幾篇文章能彌補這塊空白,貢獻一份微薄之力,
如果你也吐槽過PropertyEditor不好用,那么本文將對會有幫助,Spring自3.0版本開始自建了一套全新型別轉換介面,這就是本文的主要內容,接下來逐步展開,
說明:Spring自3.0后笑傲群雄,進入大一統,Java從此步入Spring的時代
版本約定
- Spring Framework:5.3.1
- Spring Boot:2.4.0

?正文
在了解新一代的轉換介面之前,先思考一個問題:Spring為何要自己造一套輪子呢? 一向秉承不重復造輪子原則的Spring,不是迫不得已的話是不會去動他人奶酪的,畢竟互利共生才能長久,型別轉換,作為Spring框架的基石,扮演著例外重要的角色,因此對其可擴展性、可維護性、高效性均有很高要求,
基于此,我們先來了解下PropertyEditor設計上到底有哪些缺陷/不足(不能滿足現代化需求),讓Spring“被迫”走上了自建道路,
PropertyEditor設計缺陷
前提說明:本文指出它的設計缺陷,只討論把它當做型別轉換器在轉換場景下存在的一些缺陷,
- 職責不單一:該介面有非常多的方法,但只用到2個而已
- 型別不安全:setValue()方法入參是Object,getValue()回傳值是Object,依賴于約定好的型別強轉,不安全
- 執行緒不安全:依賴于setValue()后getValue(),實體是執行緒不安全的
- 語意不清晰:從語意上根本不能知道它是用于型別轉換的組件
- 只能用于String型別:它只能進行String <-> 其它型別的轉換,而非更靈活的Object <-> Object
PropertyEditor存在這五宗“罪”,讓Spring決定自己設計一套全新API用于專門服務于型別轉換,這就是本文標題所述:新一代型別轉換Converter、ConverterFactory、GenericConverter,
關于PropertyEditor在Spring中的詳情介紹,請參見文章:3. 搞定收工,PropertyEditor就到這
新一代型別轉換
為了解決PropertyEditor作為型別轉換方式的設計缺陷,Spring 3.0版本重新設計了一套型別轉換介面,有3個核心介面:
Converter<S, T>:Source -> Target型別轉換介面,適用于1:1轉換ConverterFactory<S, R>:Source -> R型別轉換介面,適用于1:N轉換GenericConverter:更為通用的型別轉換介面,適用于N:N轉換- 注意:就它沒有泛型約束,因為是通用
另外,還有一個條件介面ConditionalConverter,可跟上面3個介面搭配組合使用,提供前置條件判斷驗證,
這套介面,解決了PropertyEditor做型別轉換存在的所有缺陷,且具有非常高的靈活性和可擴展性,下面進入詳細了解,
Converter
將源型別S轉換為目標型別T,
@FunctionalInterface
public interface Converter<S, T> {
T convert(S source);
}
它是個函式式介面,介面定義非常簡單,適合1:1轉換場景:可以將任意型別 轉換為 任意型別,它的實作類非常多,部分截圖如下:

值得注意的是:幾乎所有實作類的訪問權限都是default/private,只有少數幾個是public公開的,下面我用代碼示例來“近距離”感受一下,
代碼示例
/**
* Converter:1:1
*/
@Test
public void test() {
System.out.println("----------------StringToBooleanConverter---------------");
Converter<String, Boolean> converter = new StringToBooleanConverter();
// trueValues.add("true");
// trueValues.add("on");
// trueValues.add("yes");
// trueValues.add("1");
System.out.println(converter.convert("true"));
System.out.println(converter.convert("1"));
// falseValues.add("false");
// falseValues.add("off");
// falseValues.add("no");
// falseValues.add("0");
System.out.println(converter.convert("FalSe"));
System.out.println(converter.convert("off"));
// 注意:空串回傳的是null
System.out.println(converter.convert(""));
System.out.println("----------------StringToCharsetConverter---------------");
Converter<String, Charset> converter2 = new StringToCharsetConverter();
// 中間橫杠非必須,但強烈建議寫上 不區分大小寫
System.out.println(converter2.convert("uTf-8"));
System.out.println(converter2.convert("utF8"));
}
運行程式,正常輸出:
----------------StringToBooleanConverter---------------
true
true
false
false
null
----------------StringToCharsetConverter---------------
UTF-8
UTF-8
說明:StringToBooleanConverter/StringToCharsetConverter訪問權限都是default,外部不可直接使用,此處為了做示例用到一個小技巧 -> 將Demo的報名調整為和轉換器的一樣,這樣就可以直接訪問,
關注點:true/on/yes/1都能被正確轉換為true的,且對于英文字母來說一般都不區分大小寫,增加了容錯性(包括Charset的轉換),
不足
Converter用于解決1:1的任意型別轉換,因此它必然存在一個不足:解決1:N轉換問題需要寫N遍,造成重復冗余代碼,
譬如:輸入是字串,它可以轉為任意數字型別,包括byte、short、int、long、double等等,如果用Converter來轉換的話每個型別都得寫個轉換器,想想都麻煩有木有,
Spring早早就考慮到了該場景,提供了相應的介面來處理,它就是ConverterFactory<S, R>,
ConverterFactory
從名稱上看它代表一個轉換工廠:可以將物件S轉換為R的所有子型別,從而形成1:N的關系,
該介面描述為xxxFactory是非常合適的,很好的表達了1:N的關系
public interface ConverterFactory<S, R> {
<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}
它同樣也是個函式式介面,該介面的實作類并不多,Spring Framework共提供了5個內建實作(訪問權限全部為default):

以StringToNumberConverterFactory為例看看實作的套路:
final class StringToNumberConverterFactory implements ConverterFactory<String, Number> {
@Override
public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) {
return new StringToNumber<T>(targetType);
}
// 私有內部類:實作Converter介面,用泛型邊界約束一型別別
private static final class StringToNumber<T extends Number> implements Converter<String, T> {
private final Class<T> targetType;
public StringToNumber(Class<T> targetType) {
this.targetType = targetType;
}
@Override
public T convert(String source) {
if (source.isEmpty()) {
return null;
}
return NumberUtils.parseNumber(source, this.targetType);
}
}
}
由點知面,ConverterFactory作為Converter的工廠,對Converter進行包裝,從而達到屏蔽內部實作的目的,對使用者友好,這不正是工廠模式的優點么,符合xxxFactory的語意,但你需要清除的是,工廠內部實作其實也是通過眾多if else之類的去完成的,本質上并無差異,
代碼示例
/**
* ConverterFactory:1:N
*/
@Test
public void test2() {
System.out.println("----------------StringToNumberConverterFactory---------------");
ConverterFactory<String, Number> converterFactory = new StringToNumberConverterFactory();
// 注意:這里不能寫基本資料型別,如int.class將拋錯
System.out.println(converterFactory.getConverter(Integer.class).convert("1").getClass());
System.out.println(converterFactory.getConverter(Double.class).convert("1.1").getClass());
System.out.println(converterFactory.getConverter(Byte.class).convert("0x11").getClass());
}
運行程式,正常輸出:
----------------StringToNumberConverterFactory---------------
class java.lang.Integer
class java.lang.Double
class java.lang.Byte
關注點:數字型別的字串,是可以被轉換為任意Java中的數字型別的,String(1) -> Number(N),這便就是ConverterFactory的功勞,它能處理這一類轉換問題,
不足
既然有了1:1、1:N,自然就有N:N,比如集合轉換、陣列轉換、Map到Map的轉換等等,這些N:N的場景,就需要借助下一個介面GenericConverter來實作,
GenericConverter
它是一個通用的轉換介面,用于在兩個或多個型別之間進行轉換,相較于前兩個,這是最靈活的SPI轉換器介面,但也是最復雜的,
public interface GenericConverter {
Set<ConvertiblePair> getConvertibleTypes();
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
// 普通POJO
final class ConvertiblePair {
private final Class<?> sourceType;
private final Class<?> targetType;
}
}
該介面并非函式式介面,雖然方法不多但稍顯復雜,現對出現的幾個型別做簡單介紹:
ConvertiblePair:維護sourceType和targetType的POJO- getConvertibleTypes()方法回傳此Pair的Set集合,由此也能看出該轉換器是可以支持N:N的(大多數情況下只寫一對值而已,也有寫多對的)
TypeDescriptor:型別描述,該類專用于Spring的型別轉換場景,用于描述from or to的型別- 比單獨的Type型別強大,內部借助了ResolvableType來解決泛型議題
GenericConverter的內置實作也比較多,部分截圖如下:

ConditionalGenericConverter是GenericConverter和條件介面ConditionalConverter的組合,作用是在執行GenericConverter轉換時增加一個前置條件判斷方法,
| 轉換器 | 描述 | 示例 |
|---|---|---|
| ArrayToArrayConverter | 陣列轉陣列Object[] -> Object[] | [“1”,“2”] -> [1,2] |
| ArrayToCollectionConverter | 陣列轉集合 Object[] -> Collection | 同上 |
| CollectionToCollectionConverter | 陣列轉集合 Collection -> Collection | 同上 |
| StringToCollectionConverter | 字串轉集合String -> Collection | 1,2 -> [1,2] |
| StringToArrayConverter | 字串轉陣列String -> Array | 同上 |
| MapToMapConverter | Map -> Map(需特別注意:key和value都支持轉換才行) | 略 |
| CollectionToStringConverter | 集合轉字串Collection -> String | [1,2] -> 1,2 |
| ArrayToStringConverter | 委托給CollectionToStringConverter完成 | 同上 |
-- | -- | -- |
| StreamConverter | 集合/陣列 <-> Stream互轉 | 集合/陣列型別 -> Stream型別 |
| IdToEntityConverter | ID->Entity的轉換 | 傳入任意型別ID -> 一個Entity實體 |
| ObjectToObjectConverter | 很復雜的物件轉換,任意物件之間 | obj -> obj |
| FallbackObjectToStringConverter | 上個轉換器的兜底,呼叫Obj.toString()轉換 | obj -> String |
說明:分割線下面的4個轉換器比較特殊,字面上不好理解其實際作用,比較“高級”,它們如果能被運用在日常作業中可以事半功弎,因此放在在下篇文章專門給你介紹
下面以CollectionToCollectionConverter為例分析此轉換器的“復雜”之處:
final class CollectionToCollectionConverter implements ConditionalGenericConverter {
private final ConversionService conversionService;
public CollectionToCollectionConverter(ConversionService conversionService) {
this.conversionService = conversionService;
}
// 集合轉集合:如String集合轉為Integer集合
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Collection.class, Collection.class));
}
}
這是唯一構造器,必須傳入ConversionService:元素與元素之間的轉換是依賴于conversionService轉換服務去完成的,最終完成集合到集合的轉換,
CollectionToCollectionConverter:
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return ConversionUtils.canConvertElements(sourceType.getElementTypeDescriptor(), targetType.getElementTypeDescriptor(), this.conversionService);
}
判斷能否轉換的依據:集合里的元素與元素之間是否能夠轉換,底層依賴于ConversionService#canConvert()這個API去完成判斷,
接下來再看最復雜的轉換方法:
CollectionToCollectionConverter:
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}
Collection<?> sourceCollection = (Collection<?>) source;
// 判斷:這些情況下,將不用執行后續轉換動作了,直接回傳即可
boolean copyRequired = !targetType.getType().isInstance(source);
if (!copyRequired && sourceCollection.isEmpty()) {
return source;
}
TypeDescriptor elementDesc = targetType.getElementTypeDescriptor();
if (elementDesc == null && !copyRequired) {
return source;
}
Collection<Object> target = CollectionFactory.createCollection(targetType.getType(),
(elementDesc != null ? elementDesc.getType() : null), sourceCollection.size());
// 若目標型別沒有指定泛型(沒指定就是Object),不用遍歷直接添加全部即可
if (elementDesc == null) {
target.addAll(sourceCollection);
} else {
// 遍歷:一個一個元素的轉,時間復雜度還是蠻高的
// 元素轉元素委托給conversionService去完成
for (Object sourceElement : sourceCollection) {
Object targetElement = this.conversionService.convert(sourceElement,
sourceType.elementTypeDescriptor(sourceElement), elementDesc);
target.add(targetElement);
if (sourceElement != targetElement) {
copyRequired = true;
}
}
}
return (copyRequired ? target : source);
}
該轉換步驟稍微有點復雜,我幫你屢清楚后有這幾個關鍵步驟:
- 快速回傳:對于特殊情況,做快速回傳處理
- 若目標元素型別是源元素型別的子型別(或相同),就沒有轉換的必要了(copyRequired = false)
- 若源集合為空,或者目標集合沒指定泛型,也不需要做轉換動作
- 源集合為空,還轉換個啥
- 目標集合沒指定泛型,那就是Object,因此可以接納一切,還轉換個啥
- 若沒有觸發快速回傳,給目標創建一個新集合,然后把source的元素一個一個的放進新集合里去,這里又分為兩種處理case
- 若新集合(目標集合)沒有指定泛型型別(那就是Object),就直接putAll即可,并不需要做型別轉換
- 若新集合(目標集合指定了泛型型別),就遍歷源集合委托
conversionService.convert()對元素一個一個的轉
代碼示例
以CollectionToCollectionConverter做示范:List<String> -> Set<Integer>
@Test
public void test3() {
System.out.println("----------------CollectionToCollectionConverter---------------");
ConditionalGenericConverter conditionalGenericConverter = new CollectionToCollectionConverter(new DefaultConversionService());
// 將Collection轉為Collection(注意:沒有指定泛型型別哦)
System.out.println(conditionalGenericConverter.getConvertibleTypes());
List<String> sourceList = Arrays.asList("1", "2", "2", "3", "4");
TypeDescriptor sourceTypeDesp = TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class));
TypeDescriptor targetTypeDesp = TypeDescriptor.collection(Set.class, TypeDescriptor.valueOf(Integer.class));
System.out.println(conditionalGenericConverter.matches(sourceTypeDesp, targetTypeDesp));
Object convert = conditionalGenericConverter.convert(sourceList, sourceTypeDesp, targetTypeDesp);
System.out.println(convert.getClass());
System.out.println(convert);
}
運行程式,正常輸出:
[java.util.Collection -> java.util.Collection]
true
class java.util.LinkedHashSet
[1, 2, 3, 4]
關注點:target最終使用的是LinkedHashSet來存盤,這結果和CollectionFactory#createCollection該API的實作邏輯是相關(Set型別默認創建的是LinkedHashSet實體),
不足
如果說它的優點是功能強大,能夠處理復雜型別的轉換(PropertyEditor和前2個介面都只能轉換單元素型別),那么缺點就是使用、自定義實作起來比較復雜,這不官方也給出了使用指導意見:在Converter/ConverterFactory介面能夠滿足條件的情況下,可不使用此介面就不使用,
ConditionalConverter
條件介面,@since 3.2,它可以為Converter、GenericConverter、ConverterFactory轉換增加一個前置判斷條件,
public interface ConditionalConverter {
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}
該介面的實作,截圖如下:

可以看到,只有通用轉換器GenericConverter和它進行了合體,這也很容易理解,作為通用的轉換器,加個前置判斷將更加嚴謹和更安全,對于專用的轉換器如Converter,它已明確規定了轉換的型別,自然就不需要做前置判斷嘍,
?總結
本文詳細介紹了Spring新一代的型別轉換介面,型別轉換作為Spring的基石,其重要性可見一斑,
PropertyEditor作為Spring早期使用“轉換器”,因存在眾多設計缺陷自Spring 3.0起被新一代轉換介面所取代,主要有:
Converter<S, T>:Source -> Target型別轉換介面,適用于1:1轉換ConverterFactory<S, R>:Source -> R型別轉換介面,適用于1:N轉換GenericConverter:更為通用的型別轉換介面,適用于N:N轉換
下篇文章將針對于GenericConverter的幾個特殊實作撰專文為你講解,你也知道做難事必有所得,做難事才有可能破局、破圈,歡迎保持關注,
???推薦閱讀???
【Spring型別轉換】系列:
- 1. 揭秘Spring型別轉換 - 框架設計的基石
- 2. Spring早期型別轉換,基于PropertyEditor實作
- 3. 搞定收工,PropertyEditor就到這
【Jackson】系列:
- 1. 初識Jackson – 世界上最好的JSON庫
- 2. 媽呀,Jackson原來是這樣寫JSON的
- 3. 懂了這些,方敢在簡歷上說會用Jackson寫JSON
- 4. JSON字串是如何被決議的?JsonParser了解一下
- 5. JsonFactory工廠而已,還蠻有料,這是我沒想到的
- 6. 二十不惑,ObjectMapper使用也不再迷惑
- 7. Jackson用樹模型處理JSON是必備技能,不信你看
【資料校驗Bean Validation】系列:
- 1. 不吹不擂,第一篇就能提升你對Bean Validation資料校驗的認知
- 2. Bean Validation宣告式校驗方法的引數、回傳值
- 3. 站在使用層面,Bean Validation這些標準介面你需要爛熟于胸
- 4. Validator校驗器的五大核心組件,一個都不能少
- 5. Bean Validation宣告式驗證四大級別:欄位、屬性、容器元素、類
- 6. 自定義容器型別元素驗證,類級別驗證(多欄位聯合驗證)
【新特性】系列:
- IntelliJ IDEA 2020.3正式發布,年度最后一個版本很講武德
- IntelliJ IDEA 2020.2正式發布,諸多亮點總有幾款能助你提效
- IntelliJ IDEA 2020.1正式發布,你要的Almost都在這!
- Spring Framework 5.3.0正式發布,在云原生路上繼續發力
- Spring Boot 2.4.0正式發布,全新的組態檔加載機制(不向下兼容)
- Spring改變版本號命名規則:此舉對非英語國家很友好
- JDK15正式發布,劃時代的ZGC同時宣布轉正
【程式人生】系列:
- 螞蟻金服上市了,我不想努力了
- 如果程式員和產品經理都用凡爾賽文學對話…
- 程式人生 | 春風得意馬蹄疾,一日看盡長安花
還有諸如【Spring配置類】【Spring-static】【Spring資料系結】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】…更多原創專欄,關注BAT的烏托邦回復專欄二字即可全部獲取,分享、成長,拒絕淺藏輒止,
有些專欄已完結,有些正在連載中,期待你的關注、共同進步
?關注A哥?
| Author | A哥(YourBatman) |
|---|---|
| 個人站點 | www.yourbatman.cn |
| yourbatman@qq.com | |
| 微 信 | fsx1056342982 |
活躍平臺 | ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
| 公眾號 | BAT的烏托邦(ID:BAT-utopia) |
| 知識星球 | BAT的烏托邦 |
| 每日文章推薦 | 每日文章推薦 |

CSDN認證博客專家
博客專家
專欄創作者
BAT的烏托邦
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/239198.html
標籤:java







