分享、成長,拒絕淺藏輒止。關注公衆號【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類型轉換】系列:
【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的烏托邦
回覆專欄
二字即可全部獲取,分享、成長,拒絕淺藏輒止。
有些專欄已完結,有些正在連載中,期待你的關注、共同進步