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

分享、成長,拒絕淺藏輒止。關注公衆號【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),咱們下期見

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