6. 抹平差異,統一型別轉換服務ConversionService

YourBatman發表於2020-12-28

分享、成長,拒絕淺藏輒止。關注公眾號【BAT的烏托邦】,回覆關鍵字專欄有Spring技術棧、中介軟體等小而美的原創專欄供以免費學習。本文已被 https://www.yourbatman.cn 收錄。

✍前言

你好,我是YourBatman。

通過前兩篇文章的介紹已經非常熟悉Spirng 3.0全新一代的型別轉換機制了,它提供的三種型別轉換器(Converter、ConverterFactory、GenericConverter),分別可處理1:1、1:N、N:N的型別轉換。按照Spring的設計習慣,必有一個註冊中心來統一管理,負責它們的註冊、刪除等,它就是ConverterRegistry

對於ConverterRegistry在文首多說一句:我翻閱了很多部落格文章介紹它時幾乎無一例外的提到有查詢的功能,但實際上是沒有的。Spring設計此API介面並沒有暴露其查詢功能,選擇把最為複雜的查詢匹配邏輯私有化,目的是讓開發者使可無需關心,細節之處充分體現了Spring團隊API設計的卓越能力。

另外,內建的絕大多數轉換器訪問許可權都是default/private,那麼如何使用它們,以及遮蔽各種轉換器的差異化呢?為此,Spring提供了一個統一型別轉換服務,它就是ConversionService

版本約定

  • Spring Framework:5.3.1
  • Spring Boot:2.4.0

✍正文

ConverterRegistry和ConversionService的關係密不可分,前者為後者提供轉換器管理支撐,後者面向使用者提供服務。本文涉及到的介面/類有:

  • ConverterRegistry:轉換器註冊中心。負責轉換器的註冊、刪除
  • ConversionService統一的型別轉換服務。屬於面向開發者使用的門面介面
  • ConfigurableConversionService:上兩個介面的組合介面
  • GenericConversionService:上個介面的實現,實現了註冊管理、轉換服務的幾乎所有功能,是個實現類而非抽象類
  • DefaultConversionService:繼承自GenericConversionService,在其基礎上註冊了一批預設轉換器(Spring內建),從而具備基礎轉換能力,能解決日常絕大部分場景

ConverterRegistry

Spring 3.0引入的轉換器註冊中心,用於管理新一套的轉換器們。

public interface ConverterRegistry {
	
	void addConverter(Converter<?, ?> converter);
	<S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter);
	void addConverter(GenericConverter converter);
	void addConverterFactory(ConverterFactory<?, ?> factory);
	
	// 唯一移除方法:按照轉換pair對來移除
	void removeConvertible(Class<?> sourceType, Class<?> targetType);
}

它的繼承樹如下:

ConverterRegistry有子介面FormatterRegistry,它屬於格式化器的範疇,故不放在本文討論。但仍舊屬於本系列專題內容,會在接下來的幾篇內容裡介入,敬請關注。

ConversionService

面向使用者的統一型別轉換服務。換句話說:站在使用層面,你只需要知道ConversionService介面API的使用方式即可,並不需要關心其內部實現機制,可謂對使用者非常友好。

public interface ConversionService {
	
	boolean canConvert(Class<?> sourceType, Class<?> targetType);
	boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
	
	<T> T convert(Object source, Class<T> targetType);
	Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

它的繼承樹如下:

可以看到ConversionService和ConverterRegistry的繼承樹殊途同歸,都直接指向了ConfigurableConversionService這個分支,下面就對它進行介紹。

ConfigurableConversionService

ConversionServiceConverterRegistry的組合介面,自己並未新增任何介面方法。

public interface ConfigurableConversionService extends ConversionService, ConverterRegistry {

}

它的繼承樹可參考上圖。接下來就來到此介面的直接實現類GenericConversionService。

GenericConversionService

ConfigurableConversionService介面提供了完整實現的實現類。換句話說:ConversionService和ConverterRegistry介面的功能均通過此類得到了實現,所以它是本文重點。

該類很有些值得學習的地方,可以細品,在我們自己設計程式時加以借鑑。

public class GenericConversionService implements ConfigurableConversionService {

	private final Converters converters = new Converters();
	private final Map<ConverterCacheKey, GenericConverter> converterCache = new ConcurrentReferenceHashMap<ConverterCacheKey, GenericConverter>(64);
}

它用兩個成員變數來管理轉換器們,其中converterCache是快取用於加速查詢,因此更為重要的便是Converters嘍。

Converters是GenericConversionService的內部類,用於管理(新增、刪除、查詢)轉換器們。也就說對ConverterRegistry介面的實現最終是委託給它去完成的,它是整個轉換服務正常work的核心,下面我們對它展開詳細敘述。

1、內部類Converters

它管理所有轉換器,包括新增、刪除、查詢。

GenericConversionService:

	// 內部類
	private static class Converters {
		private final Set<GenericConverter> globalConverters = new LinkedHashSet<GenericConverter>();
		private final Map<ConvertiblePair, ConvertersForPair> converters = new LinkedHashMap<ConvertiblePair, ConvertersForPair>(36);
	}

說明:這裡使用的集合/Map均為LinkedHashXXX,都是有序的(存入順序和遍歷取出順序保持一致)

用這兩個集合/Map儲存著註冊進來的轉換器們,他們的作用分別是:

  • globalConverters:存取通用的轉換器,並不限定轉換型別,一般用於兜底
  • converters:指定了型別對,對應的轉換器的對映關係。
    • ConvertiblePair:表示一對,包含sourceType和targetType
    • ConvertersForPair:這一對對應的轉換器(因為能處理一對的可能存在多個轉換器),內部使用一個雙端佇列Deque來儲存,保證順序
      • 小細節:Spring 5之前使用LinkedList,之後使用Deque(實際為ArrayDeque)儲存
final class ConvertiblePair {
	private final Class<?> sourceType;
	private final Class<?> targetType;
}
private static class ConvertersForPair {
	private final Deque<GenericConverter> converters = new ArrayDeque<>(1);
}
新增add
public void add(GenericConverter converter) {
	Set<ConvertiblePair> convertibleTypes = converter.getConvertibleTypes();
	if (convertibleTypes == null) {
		... // 放進globalConverters裡
	} else {
		... // 放進converters裡(若支援多組pair就放多個key)
	}
}

在此之前需要了解個前提:對於三種轉換器Converter、ConverterFactory、GenericConverter在新增到Converters之前都統一被適配為了GenericConverter,這樣做的目的是方便統一管理。對應的兩個介面卡是ConverterAdapter和ConverterFactoryAdapter,它倆都是ConditionalGenericConverter的內部類。

新增的邏輯被我用虛擬碼簡化後其實非常簡單,無非就是一個非此即彼的關係而已:

  • 若轉換器沒有指定處理的型別對,就放進全域性轉換器列表裡,用於兜底
  • 若轉換器有指定處理的型別對(可能還是多個),就放進converters裡,後面查詢時使用
刪除remove
public void remove(Class<?> sourceType, Class<?> targetType) {
	this.converters.remove(new ConvertiblePair(sourceType, targetType));
}

移除邏輯非常非常的簡單,這得益於新增時候做了統一適配的抽象

查詢find
@Nullable
public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) {
	// 找到該型別的類層次介面(父類 + 介面),注意:結果是有序列表
	List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType());
	List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType());

	// 雙重遍歷
	for (Class<?> sourceCandidate : sourceCandidates) {
		for (Class<?> targetCandidate : targetCandidates) {
			ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate);
			... // 從converters、globalConverters裡匹配到一個合適轉換器後立馬返回
		}
	}
	return null;
}

查詢邏輯也並不複雜,有兩個關鍵點需要關注:

  • getClassHierarchy(class):獲取該型別的類層次(父類 + 介面),注意:結果List是有序的List
    • 也就是說轉換器支援的型別若是父類/介面,那麼也能夠處理器子類
  • 根據convertiblePair匹配轉換器:優先匹配專用的converters,然後才是globalConverters。若都沒匹配上返回null

2、管理轉換器(ConverterRegistry)

瞭解了Converters之後再來看GenericConversionService是如何管理轉換器,就如魚得水,一目瞭然了。

新增

為了方便使用者呼叫,ConverterRegistry介面提供了三個新增方法,這裡一一給與實現。

說明:暴露給呼叫者使用的API介面使用起來應儘量的方便,過載多個是個有效途徑。內部做適配、歸口即可,使用者至上

@Override
public void addConverter(Converter<?, ?> converter) {
	// 獲取泛型型別 -> 轉為ConvertiblePair
	ResolvableType[] typeInfo = getRequiredTypeInfo(converter.getClass(), Converter.class);
	... 
	// converter適配為GenericConverter新增
	addConverter(new ConverterAdapter(converter, typeInfo[0], typeInfo[1]));
}

@Override
public <S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter) {
	addConverter(new ConverterAdapter(converter, ResolvableType.forClass(sourceType), ResolvableType.forClass(targetType)));
}

@Override
public void addConverter(GenericConverter converter) {
	this.converters.add(converter);
	invalidateCache();
}

前兩個方法都會呼叫到第三個方法上,每呼叫一次addConverter()方法都會清空快取,也就是converterCache.clear()。所以動態新增轉換器對效能是有損的,因此使用時候需稍加註意一些。

查詢

ConverterRegistry介面並未直接提供查詢方法,而只是在實現類內部做了實現。提供一個鉤子方法用於查詢給定sourceType/targetType對的轉換器。

@Nullable
protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
	ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType);
	
	// 1、查快取
	GenericConverter converter = this.converterCache.get(key);
	if (converter != null) {
		... // 返回結果
	}

	// 2、去converters裡查詢
	converter = this.converters.find(sourceType, targetType);
	if (converter == null) {
		// 若還沒有匹配的,就返回預設結果
		// 預設結果是NoOpConverter -> 什麼都不做
		converter = getDefaultConverter(sourceType, targetType);
	}

	... // 把結果裝進快取converterCache裡
	return null;
}

有了對Converters查詢邏輯的分析,這個步驟就很簡單了。繪製成圖如下:

3、轉換功能(ConversionService)

上半部分介紹完GenericConversionService對轉換器管理部分的實現(對ConverterRegistry介面的實現),接下來就看看它是如何實現轉換功能的(對ConversionService介面的實現)。

判斷
@Override
public boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType) {
	return canConvert((sourceType != null ? TypeDescriptor.valueOf(sourceType) : null), TypeDescriptor.valueOf(targetType));
}

@Override
public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
	if (sourceType == null) {
		return true;
	}
	
	// 查詢/匹配對應的轉換器
	GenericConverter converter = getConverter(sourceType, targetType);
	return (converter != null);
}

能否執行轉換判斷的唯一標準:能否匹配到可用於轉換的轉換器。而這個查詢匹配邏輯,稍稍抬頭往上就能看到。

轉換
@Override
@SuppressWarnings("unchecked")
@Nullable
public <T> T convert(@Nullable Object source, Class<T> targetType) {
	return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType));
}

@Override
@Nullable
public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
	if (sourceType == null) {
		return handleResult(null, targetType, convertNullSource(null, targetType));
	}
	// 校驗:source必須是sourceType的例項
	if (source != null && !sourceType.getObjectType().isInstance(source)) {
		throw new IllegalArgumentException("Source to convert from must be an instance of [" + sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
	}

	// ============拿到轉換器,執行轉換============
	GenericConverter converter = getConverter(sourceType, targetType);
	if (converter != null) {
		Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
		return handleResult(sourceType, targetType, result);
	}
	// 若沒進行canConvert的判斷直接調動,可能出現此種狀況:一般丟擲ConverterNotFoundException異常
	return handleConverterNotFound(source, sourceType, targetType);
}

同樣的,執行轉換的邏輯很簡單,非常好理解的兩個步驟:

  1. 查詢匹配到一個合適的轉換器(查詢匹配的邏輯同上)
  2. 拿到此轉換器執行轉換converter.convert(...)

說明:其餘程式碼均為一些判斷、校驗、容錯,並非核心,本文給與適當忽略。

GenericConversionService實現了轉換器管理、轉換服務的所有功能,是可以直接面向開發者使用的。但是開發者使用時可能並不知道需要註冊哪些轉換器來保證程式正常運轉,Spring並不能要求開發者知曉其內建實現。基於此,Spring在3.1又提供了一個預設實現DefaultConversionService,它對使用者更友好。

DefaultConversionService

Spirng容器預設使用的轉換服務實現,繼承自GenericConversionService,在其基礎行只做了一件事:構造時新增內建的預設轉換器。從而天然具備有了基本的型別轉換能力,適用於不同的環境。如:xml解析、@Value解析、http協議引數自動轉換等等。

小細節:它並非Spring 3.0就有,而是Spring 3.1新推出的API

// @since 3.1
public class DefaultConversionService extends GenericConversionService {
	
	// 唯一構造器
	public DefaultConversionService() {
		addDefaultConverters(this);
	}

}

本類核心程式碼就這一個構造器,構造器內就這一句程式碼:addDefaultConverters(this)。接下來需要關注Spring預設情況下給我們“安裝”了哪些轉換器呢?也就是了解下addDefaultConverters(this)這個靜態方法

預設註冊的轉換器們

// public的靜態方法,注意是public的訪問許可權
public static void addDefaultConverters(ConverterRegistry converterRegistry) {
	addScalarConverters(converterRegistry);
	addCollectionConverters(converterRegistry);

	converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
	converterRegistry.addConverter(new StringToTimeZoneConverter());
	converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
	converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());

	converterRegistry.addConverter(new ObjectToObjectConverter());
	converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
	converterRegistry.addConverter(new FallbackObjectToStringConverter());
	converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
}

該靜態方法用於註冊全域性的、預設的轉換器們,從而讓Spring有了基礎的轉換能力,進而完成絕大部分轉換工作。為了方便記憶這個註冊流程,我把它繪製成圖供以你儲存:

特別強調:轉換器的註冊順序非常重要,這決定了通用轉換器的匹配結果(誰在前,優先匹配誰,first win)。

針對這幅圖,你可能還會有如下疑問:

  1. JSR310轉換器只看到TimeZone、ZoneId等轉換,怎麼沒看見更為常用的LocalDate、LocalDateTime等這些型別轉換呢?難道Spring預設是不支援的?
    1. 答:當然不是。 這麼常見的場景Spring怎能會不支援呢?不過與其說這是型別轉換,倒不如說是格式化更合適。所以放在該系列後幾篇關於格式化章節中再做講述
  2. 一般的Converter都見名之意,但StreamConverter有何作用呢?什麼場景下會生效
    1. 答:上文已講述
  3. 對於兜底的轉換器,有何含義?這種極具通用性的轉換器作用為何
    1. 答:上文已講述

最後,需要特別強調的是:它是一個靜態方法,並且還是public的訪問許可權,且不僅僅只有本類呼叫。實際上,DefaultConversionService僅僅只做了這一件事,所以任何地方只要呼叫了該靜態方法都能達到前者相同的效果,使用上可謂給與了較大的靈活性。比如Spring Boot環境下不是使用DefaultConversionService而是ApplicationConversionService,後者是對FormattingConversionService擴充套件,這個話題放在後面詳解。

Spring Boot在web環境預設向容易註冊了一個WebConversionService,因此你有需要可直接@Autowired使用

ConversionServiceFactoryBean

顧名思義,它是用於產生ConversionService型別轉換服務的工廠Bean,為了方便和Spring容器整合而使用。

public class ConversionServiceFactoryBean implements FactoryBean<ConversionService>, InitializingBean {

	@Nullable
	private Set<?> converters;
	@Nullable
	private GenericConversionService conversionService;

	public void setConverters(Set<?> converters) {
		this.converters = converters;
	}
	@Override
	public void afterPropertiesSet() {
		// 使用的是預設實現哦
		this.conversionService = new DefaultConversionService();
		ConversionServiceFactory.registerConverters(this.converters, this.conversionService);
	}
	
	@Override
	@Nullable
	public ConversionService getObject() {
		return this.conversionService;
	}
	...
}

這裡只有兩個資訊量需要關注:

  1. 使用的是DefaultConversionService,因此那一大串的內建轉換器們都會被新增進來的
  2. 自定義轉換器可以通過setConverters()方法新增進來
    1. 值得注意的是方法入參是Set<?>並沒有明確泛型型別,因此那三種轉換器(1:1/1:N/N:N)你是都可以新增.

✍總結

通讀本文過後,相信能夠給與你這個感覺:曾經望而卻步的Spring型別轉換服務ConversionService,其實也不過如此嘛。通篇我用了多個簡單字眼來說明,因為拆開之後,無一高複雜度知識點。

迎難而上是積攢漲薪底氣和勇氣的途徑,況且某些知識點其實並不難,所以我覺得從價效比角度來看這類內容是非常划算的,你pick到了麼?

正所謂型別轉換和格式化屬於兩組近義詞,在Spring體系中也經常交織在一起使用,有種傻傻分不清楚之感。從下篇文章起進入到本系列關於Formatter格式化器知識的梳理,什麼日期格式化、@DateTimeFormat、@NumberFormat都將幫你捋清楚嘍,有興趣者可保持持續關注。


✔✔✔推薦閱讀✔✔✔

【Spring型別轉換】系列:

【Jackson】系列:

【資料校驗Bean Validation】系列:

【新特性】系列:

【程式人生】系列:

還有諸如【Spring配置類】【Spring-static】【Spring資料繫結】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】...更多原創專欄,關注BAT的烏托邦回覆專欄二字即可全部獲取,也可加我fsx1056342982,交個朋友。

有些已完結,有些連載中。我是A哥(YourBatman),我們們下期見

相關文章