4. 上新了Spring,全新一代類型轉換機制

分享、成長,拒絕淺藏輒止。關注公衆號【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設計缺陷

前提說明:本文指出它的設計缺陷,只討論把它當做類型轉換器在轉換場景下存在的一些缺陷。

  1. 職責不單一:該接口有非常多的方法,但只用到2個而已
  2. 類型不安全:setValue()方法入參是Object,getValue()返回值是Object,依賴於約定好的類型強轉,不安全
  3. 線程不安全:依賴於setValue()後getValue(),實例是線程不安全的
  4. 語義不清晰:從語義上根本不能知道它是用於類型轉換的組件
  5. 只能用於String類型:它只能進行String <-> 其它類型的轉換,而非更靈活的Object <-> Object

PropertyEditor存在這五宗“罪”,讓Spring決定自己設計一套全新API用於專門服務於類型轉換,這就是本文標題所述:新一代類型轉換Converter、ConverterFactory、GenericConverter。

關於PropertyEditor在Spring中的詳情介紹,請參見文章:3. 搞定收工,PropertyEditor就到這

新一代類型轉換

爲了解決PropertyEditor作爲類型轉換方式的設計缺陷,Spring 3.0版本重新設計了一套類型轉換接口,有3個核心接口:

  1. Converter<S, T>:Source -> Target類型轉換接口,適用於1:1轉換
  2. ConverterFactory<S, R>:Source -> R類型轉換接口,適用於1:N轉換
  3. GenericConverter:更爲通用的類型轉換接口,適用於N:N轉換
    1. 注意:就它沒有泛型約束,因爲是通用

另外,還有一個條件接口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);
	}

該轉換步驟稍微有點複雜,我幫你屢清楚後有這幾個關鍵步驟:

  1. 快速返回:對於特殊情況,做快速返回處理
    1. 若目標元素類型是元素類型的子類型(或相同),就沒有轉換的必要了(copyRequired = false)
    2. 若源集合爲空,或者目標集合沒指定泛型,也不需要做轉換動作
      1. 源集合爲空,還轉換個啥
      2. 目標集合沒指定泛型,那就是Object,因此可以接納一切,還轉換個啥
  2. 若沒有觸發快速返回。給目標創建一個新集合,然後把source的元素一個一個的放進新集合裏去,這裏又分爲兩種處理case
    1. 若新集合(目標集合)沒有指定泛型類型(那就是Object),就直接putAll即可,並不需要做類型轉換
    2. 若新集合(目標集合指定了泛型類型),就遍歷源集合委託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起被新一代轉換接口所取代,主要有:

  1. Converter<S, T>:Source -> Target類型轉換接口,適用於1:1轉換
  2. ConverterFactory<S, R>:Source -> R類型轉換接口,適用於1:N轉換
  3. GenericConverter:更爲通用的類型轉換接口,適用於N:N轉換

下篇文章將針對於GenericConverter的幾個特殊實現撰專文爲你講解,你也知道做難事必有所得,做難事纔有可能破局、破圈,歡迎保持關注。


✔✔✔推薦閱讀✔✔✔

【Spring類型轉換】系列:

【Jackson】系列:

【數據校驗Bean Validation】系列:

【新特性】系列:

【程序人生】系列:

還有諸如【Spring配置類】【Spring-static】【Spring數據綁定】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】...更多原創專欄,關注BAT的烏托邦回覆專欄二字即可全部獲取,分享、成長,拒絕淺藏輒止。

有些專欄已完結,有些正在連載中,期待你的關注、共同進步

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章