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

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

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

✍前言

你好,我是YourBatman。程序員

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

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

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

版本約定

  • Spring Framework:5.3.1
  • Spring Boot:2.4.0

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

✍正文

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

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

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

ConverterRegistry

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

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);
}

它的繼承樹以下:ide

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

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);
}

它的繼承樹以下:

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

能夠看到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的內核,下面咱們對它展開詳細敘述。

一、內部類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

二、管理轉換器(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);

    // 一、查緩存
    GenericConverter converter = this.converterCache.get(key);
    if (converter != null) {
        ... // 返回結果
    }

    // 二、去converters裏查找
    converter = this.converters.find(sourceType, targetType);
    if (converter == null) {
        // 若尚未匹配的,就返回默認結果
        // 默認結果是NoOpConverter -> 什麼都不作
        converter = getDefaultConverter(sourceType, targetType);
    }

    ... // 把結果裝進緩存converterCache裏
    return null;
}

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

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

三、轉換功能(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有了基礎的轉換能力,進而完成絕大部分轉換工做。爲了方便記憶這個註冊流程,我把它繪製成圖供以你保存:

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

特別強調:轉換器的註冊順序很是重要,這決定了通用轉換器的匹配結果(誰在前,優先匹配誰,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&lt;?&gt;並無明確泛型類型,所以那三種轉換器(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),我們下期見

相關文章
相關標籤/搜索