分享、成長,拒絕淺藏辄止。關注公衆号【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
,在其基礎上注冊了一批預設轉換器(Spring内建),進而具備基礎轉換能力,能解決日常絕大部分場景GenericConversionService
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
ConversionService
和
ConverterRegistry
的組合接口,自己并未新增任何接口方法。
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;
}
查找邏輯也并不複雜,有兩個關鍵點需要關注:
-
:擷取該類型的類層次(父類 + 接口),注意:結果List是有序的ListgetClassHierarchy(class)
- 也就是說轉換器支援的類型若是父類/接口,那麼也能夠處理器子類
- 根據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);
}
同樣的,執行轉換的邏輯很簡單,非常好了解的兩個步驟:
- 查找比對到一個合适的轉換器(查找比對的邏輯同上)
- 拿到此轉換器執行轉換
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)。
針對這幅圖,你可能還會有如下疑問:
- JSR310轉換器隻看到TimeZone、ZoneId等轉換,怎麼沒看見更為常用的LocalDate、LocalDateTime等這些類型轉換呢?難道Spring預設是不支援的?
- 答:當然不是。 這麼常見的場景Spring怎能會不支援呢?不過與其說這是類型轉換,倒不如說是格式化更合适。是以放在該系列後幾篇關于格式化章節中再做講述
- 一般的Converter都見名之意,但StreamConverter有何作用呢?什麼場景下會生效
- 答:上文已講述
- 對于兜底的轉換器,有何含義?這種極具通用性的轉換器作用為何
最後,需要特别強調的是:它是一個靜态方法,并且還是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;
}
...
}
這裡隻有兩個資訊量需要關注:
- 使用的是DefaultConversionService,是以那一大串的内建轉換器們都會被添加進來的
- 自定義轉換器可以通過
方法添加進來setConverters()
- 值得注意的是方法入參是
并沒有明确泛型類型,是以那三種轉換器(1:1/1:N/N:N)你是都可以添加.Set<?>
- 值得注意的是方法入參是
✍總結
通讀本文過後,相信能夠給與你這個感覺:曾經望而卻步的Spring類型轉換服務
ConversionService
,其實也不過如此嘛。通篇我用了多個簡單字眼來說明,因為拆開之後,無一高複雜度知識點。
迎難而上是積攢漲薪底氣和勇氣的途徑,況且某些知識點其實并不難,是以我覺得從成本效益角度來看這類内容是非常劃算的,你pick到了麽?
正所謂類型轉換和格式化屬于兩組近義詞,在Spring體系中也經常交織在一起使用,有種傻傻分不清楚之感。從下篇文章起進入到本系列關于Formatter格式化器知識的梳理,什麼日期格式化、@DateTimeFormat、@NumberFormat都将幫你捋清楚喽,有興趣者可保持持續關注。
✔✔✔推薦閱讀✔✔✔
【Spring類型轉換】系列:
- 1. 揭秘Spring類型轉換 - 架構設計的基石
- 2. Spring早期類型轉換,基于PropertyEditor實作
- 3. 搞定收工,PropertyEditor就到這
- 4. 上新了Spring,全新一代類型轉換機制
- 5. 穿過擁擠的人潮,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的烏托邦
回複
專欄
二字即可全部擷取,也可加我
fsx1056342982
,交個朋友。
有些已完結,有些連載中。我是A哥(YourBatman),咱們下期見