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