天天看點

6. 抹平差異,統一類型轉換服務ConversionService✍前言✍正文✍總結

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
6. 抹平差異,統一類型轉換服務ConversionService✍前言✍正文✍總結

✍正文

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

  • ConverterRegistry

    :轉換器注冊中心。負責轉換器的注冊、删除
  • ConversionService

    :統一的類型轉換服務。屬于面向開發者使用的門面接口
  • ConfigurableConversionService

    :上兩個接口的組合接口
  • GenericConversionService

    :上個接口的實作,實作了注冊管理、轉換服務的幾乎所有功能,是個實作類而非抽象類
  • DefaultConversionService

    :繼承自

    GenericConversionService

    ,在其基礎上注冊了一批預設轉換器(Spring内建),進而具備基礎轉換能力,能解決日常絕大部分場景
6. 抹平差異,統一類型轉換服務ConversionService✍前言✍正文✍總結

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

它的繼承樹如下:

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

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

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

  • 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查找邏輯的分析,這個步驟就很簡單了。繪制成圖如下:

6. 抹平差異,統一類型轉換服務ConversionService✍前言✍正文✍總結

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有了基礎的轉換能力,進而完成絕大部分轉換工作。為了友善記憶這個注冊流程,我把它繪制成圖供以你儲存:

6. 抹平差異,統一類型轉換服務ConversionService✍前言✍正文✍總結

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

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

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

最後,需要特别強調的是:它是一個靜态方法,并且還是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),咱們下期見