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

YourBatman發表於2020-12-21

分享、成長,拒絕淺藏輒止。關注公眾號【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的烏托邦回覆專欄二字即可全部獲取,分享、成長,拒絕淺藏輒止。

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

相關文章