4. 上新了Spring,全新一代類型轉換機制

2021-03-02 BAT的烏託邦
✍前言

你好,我是YourBatman。

上篇文章 介紹完了Spring類型轉換早期使用的PropertyEditor詳細介紹,關於PropertyEditor現存的資料其實還蠻少的,希望這幾篇文章能彌補這塊空白,貢獻一份微薄之力。

如果你也吐槽過PropertyEditor不好用,那麼本文將對會有幫助。Spring自3.0版本開始自建了一套全新類型轉換接口,這就是本文的主要內容,接下來逐步展開。

說明:Spring自3.0後笑傲群雄,進入大一統。Java從此步入Spring的時代

❞版本約定✍正文

在了解新一代的轉換接口之前,先思考一個問題:Spring為何要自己造一套輪子呢?  一向秉承不重複造輪子原則的Spring,不是迫不得已的話是不會去動他人奶酪的,畢竟互利共生才能長久。類型轉換,作為Spring框架的基石,扮演著異常重要的角色,因此對其可擴展性、可維護性、高效性均有很高要求。

基於此,我們先來了解下PropertyEditor設計上到底有哪些缺陷/不足(不能滿足現代化需求),讓Spring「被迫」走上了自建道路。

PropertyEditor設計缺陷

前提說明:本文指出它的設計缺陷,只討論把它當做類型轉換器在轉換場景下存在的一些缺陷。

職責不單一:該接口有非常多的方法,但只用到2個而已類型不安全:setValue()方法入參是Object,getValue()返回值是Object,依賴於約定好的類型強轉,不安全線程不安全:依賴於setValue()後getValue(),實例是線程不安全的語義不清晰:從語義上根本不能知道它是用於類型轉換的組件只能用於String類型:它只能進行String <-> 其它類型的轉換,而非更靈活的Object <-> Object

PropertyEditor存在這五宗「罪」,讓Spring決定自己設計一套全新API用於專門服務於類型轉換,這就是本文標題所述:新一代類型轉換Converter、ConverterFactory、GenericConverter。

關於PropertyEditor在Spring中的詳情介紹,請參見文章:3. 搞定收工,PropertyEditor就到這

❞新一代類型轉換

為了解決PropertyEditor作為類型轉換方式的設計缺陷,Spring 3.0版本重新設計了一套類型轉換接口,有3個核心接口:

Converter<S, T>:Source -> Target類型轉換接口,適用於1:1轉換ConverterFactory<S, R>:Source -> R類型轉換接口,適用於1:N轉換GenericConverter:更為通用的類型轉換接口,適用於N:N轉換

另外,還有一個條件接口ConditionalConverter,可跟上面3個接口搭配組合使用,提供前置條件判斷驗證。

這套接口,解決了PropertyEditor做類型轉換存在的所有缺陷,且具有非常高的靈活性和可擴展性。下面進入詳細了解。

Converter

將源類型S轉換為目標類型T。

@FunctionalInterface
public interface Converter<S, T> {
 T convert(S source);
}

它是個函數式接口,接口定義非常簡單。適合1:1轉換場景:可以將任意類型 轉換為 任意類型。它的實現類非常多,部分截圖如下:值得注意的是:幾乎所有實現類的訪問權限都是default/private,只有少數幾個是public公開的,下面我用代碼示例來「近距離」感受一下。

代碼示例
/**
 * Converter:1:1
 */
@Test
public void test() {
    System.out.println("-StringToBooleanConverter");
    Converter<String, Boolean> converter = new StringToBooleanConverter();

    // trueValues.add("true");
    // trueValues.add("on");
    // trueValues.add("yes");
    // trueValues.add("1");
    System.out.println(converter.convert("true"));
    System.out.println(converter.convert("1"));

    // falseValues.add("false");
    // falseValues.add("off");
    // falseValues.add("no");
    // falseValues.add("0");
    System.out.println(converter.convert("FalSe"));
    System.out.println(converter.convert("off"));
    // 注意:空串返回的是null
    System.out.println(converter.convert(""));


    System.out.println("-StringToCharsetConverter");
    Converter<String, Charset> converter2 = new StringToCharsetConverter();
    // 中間橫槓非必須,但強烈建議寫上   不區分大小寫
    System.out.println(converter2.convert("uTf-8"));
    System.out.println(converter2.convert("utF8"));
}

運行程序,正常輸出:

-StringToBooleanConverter
true
true
false
false
null
-StringToCharsetConverter
UTF-8
UTF-8

說明:StringToBooleanConverter/StringToCharsetConverter訪問權限都是default,外部不可直接使用。此處為了做示例用到一個小技巧 -> 將Demo的報名調整為和轉換器的一樣,這樣就可以直接訪問

關注點:true/on/yes/1都能被正確轉換為true的,且對於英文字母來說一般都不區分大小寫,增加了容錯性(包括Charset的轉換)。

不足

Converter用於解決1:1的任意類型轉換,因此它必然存在一個不足:解決1:N轉換問題需要寫N遍,造成重複冗餘代碼。

譬如:輸入是字符串,它可以轉為任意數字類型,包括byte、short、int、long、double等等,如果用Converter來轉換的話每個類型都得寫個轉換器,想想都麻煩有木有。

Spring早早就考慮到了該場景,提供了相應的接口來處理,它就是ConverterFactory<S, R>。

ConverterFactory

從名稱上看它代表一個轉換工廠:可以將對象S轉換為R的所有子類型,從而形成1:N的關係。

該接口描述為xxxFactory是非常合適的,很好的表達了1:N的關係

public interface ConverterFactory<S, R> {
 <T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

它同樣也是個函數式接口。該接口的實現類並不多,Spring Framework共提供了5個內建實現(訪問權限全部為default):以StringToNumberConverterFactory為例看看實現的套路:

final class StringToNumberConverterFactory implements ConverterFactory<String, Number> {

 @Override
 public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) {
  return new StringToNumber<T>(targetType);
 }


 // 私有內部類:實現Converter接口。用泛型邊界約束一類類型
 private static final class StringToNumber<T extends Number> implements Converter<String, T> {

  private final Class<T> targetType;
  public StringToNumber(Class<T> targetType) {
   this.targetType = targetType;
  }

  @Override
  public T convert(String source) {
   if (source.isEmpty()) {
    return null;
   }
   return NumberUtils.parseNumber(source, this.targetType);
  }
 }

}

由點知面,ConverterFactory作為Converter的工廠,對Converter進行包裝,從而達到屏蔽內部實現的目的,對使用者友好,這不正是工廠模式的優點麼,符合xxxFactory的語義。但你需要清除的是,工廠內部實現其實也是通過眾多if else之類的去完成的,本質上並無差異。

代碼示例
/**
 * ConverterFactory:1:N
 */
@Test
public void test2() {
    System.out.println("-StringToNumberConverterFactory");
    ConverterFactory<String, Number> converterFactory = new StringToNumberConverterFactory();
    // 注意:這裡不能寫基本數據類型。如int.class將拋錯
    System.out.println(converterFactory.getConverter(Integer.class).convert("1").getClass());
    System.out.println(converterFactory.getConverter(Double.class).convert("1.1").getClass());
    System.out.println(converterFactory.getConverter(Byte.class).convert("0x11").getClass());
}

運行程序,正常輸出:

-StringToNumberConverterFactory
class java.lang.Integer
class java.lang.Double
class java.lang.Byte

關注點:數字類型的字符串,是可以被轉換為任意Java中的數字類型的,String(1) -> Number(N)。這便就是ConverterFactory的功勞,它能處理這一類轉換問題。

不足

既然有了1:1、1:N,自然就有N:N。比如集合轉換、數組轉換、Map到Map的轉換等等,這些N:N的場景,就需要藉助下一個接口GenericConverter來實現。

GenericConverter

它是一個通用的轉換接口,用於在兩個或多個類型之間進行轉換。相較於前兩個,這是最靈活的SPI轉換器接口,但也是最複雜的。

public interface GenericConverter {

 Set<ConvertiblePair> getConvertibleTypes();
 Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
 
 // 普通POJO
 final class ConvertiblePair {
  private final Class<?> sourceType;
  private final Class<?> targetType;
 }
}

該接口並非函數式接口,雖然方法不多但稍顯複雜。現對出現的幾個類型做簡單介紹:

ConvertiblePair:維護sourceType和targetType的POJOgetConvertibleTypes()方法返回此Pair的Set集合。由此也能看出該轉換器是可以支持N:N的(大多數情況下只寫一對值而已,也有寫多對的)TypeDescriptor:類型描述。該類專用於Spring的類型轉換場景,用於描述from or to的類型比單獨的Type類型強大,內部藉助了ResolvableType來解決泛型議題

GenericConverter的內置實現也比較多,部分截圖如下:

ConditionalGenericConverter是GenericConverter和條件接口ConditionalConverter的組合,作用是在執行GenericConverter轉換時增加一個前置條件判斷方法。

❞轉換器描述示例ArrayToArrayConverter數組轉數組Object[] -> Object[]["1","2"] -> [1,2]ArrayToCollectionConverter數組轉集合 Object[] -> Collection同上CollectionToCollectionConverter數組轉集合 Collection -> Collection同上StringToCollectionConverter字符串轉集合String -> Collection1,2 -> [1,2]StringToArrayConverter字符串轉數組String -> Array同上MapToMapConverterMap -> Map(需特別注意:key和value都支持轉換才行)略CollectionToStringConverter集合轉字符串Collection -> String[1,2] -> 1,2ArrayToStringConverter委託給CollectionToStringConverter完成同上-「StreamConverter」集合/數組 <-> Stream互轉集合/數組類型 -> Stream類型「IdToEntityConverter」ID->Entity的轉換傳入任意類型ID -> 一個Entity實例「ObjectToObjectConverter」很複雜的對象轉換,任意對象之間obj -> obj「FallbackObjectToStringConverter」上個轉換器的兜底,調用Obj.toString()轉換obj -> String❝

說明:分割線下面的4個轉換器比較特殊,字面上不好理解其實際作用,比較「高級」。它們如果能被運用在日常工作中可以事半功弎,因此放在在下篇文章專門給你介紹

下面以CollectionToCollectionConverter為例分析此轉換器的「複雜」之處:

final class CollectionToCollectionConverter implements ConditionalGenericConverter {

 private final ConversionService conversionService;
 public CollectionToCollectionConverter(ConversionService conversionService) {
  this.conversionService = conversionService;
 }

 
 // 集合轉集合:如String集合轉為Integer集合
 @Override
 public Set<ConvertiblePair> getConvertibleTypes() {
  return Collections.singleton(new ConvertiblePair(Collection.class, Collection.class));
 }
}

這是唯一構造器,必須傳入ConversionService:元素與元素之間的轉換是依賴於conversionService轉換服務去完成的,最終完成集合到集合的轉換。

CollectionToCollectionConverter:

 @Override
 public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
  return ConversionUtils.canConvertElements(sourceType.getElementTypeDescriptor(), targetType.getElementTypeDescriptor(), this.conversionService);
 }

判斷能否轉換的依據:集合裡的元素與元素之間是否能夠轉換,底層依賴於ConversionService#canConvert()這個API去完成判斷。

接下來再看最複雜的轉換方法:

CollectionToCollectionConverter:

 @Override
 public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
  if (source == null) {
   return null;
  }
  Collection<?> sourceCollection = (Collection<?>) source;

  
  // 判斷:這些情況下,將不用執行後續轉換動作了,直接返回即可
  boolean copyRequired = !targetType.getType().isInstance(source);
  if (!copyRequired && sourceCollection.isEmpty()) {
   return source;
  }
  TypeDescriptor elementDesc = targetType.getElementTypeDescriptor();
  if (elementDesc == null && !copyRequired) {
   return source;
  }

  Collection<Object> target = CollectionFactory.createCollection(targetType.getType(),
    (elementDesc != null ? elementDesc.getType() : null), sourceCollection.size());
  // 若目標類型沒有指定泛型(沒指定就是Object),不用遍歷直接添加全部即可
  if (elementDesc == null) {
   target.addAll(sourceCollection);
  } else {
   // 遍歷:一個一個元素的轉,時間複雜度還是蠻高的
   // 元素轉元素委託給conversionService去完成
   for (Object sourceElement : sourceCollection) {
    Object targetElement = this.conversionService.convert(sourceElement,
      sourceType.elementTypeDescriptor(sourceElement), elementDesc);
    target.add(targetElement);
    if (sourceElement != targetElement) {
     copyRequired = true;
    }
   }
  }

  return (copyRequired ? target : source);
 }

該轉換步驟稍微有點複雜,我幫你屢清楚後有這幾個關鍵步驟:

目標集合沒指定泛型,那就是Object,因此可以接納一切,還轉換個啥若目標元素類型是元素類型的子類型(或相同),就沒有轉換的必要了(copyRequired = false)若源集合為空,或者目標集合沒指定泛型,也不需要做轉換動作若沒有觸發快速返回。給目標創建一個新集合,然後把source的元素一個一個的放進新集合裡去,這裡又分為兩種處理case若新集合(目標集合)沒有指定泛型類型(那就是Object),就直接putAll即可,並不需要做類型轉換若新集合(目標集合指定了泛型類型),就遍歷源集合委託conversionService.convert()對元素一個一個的轉代碼示例

以CollectionToCollectionConverter做示範:List<String> -> Set<Integer>

@Test
public void test3() {
    System.out.println("-CollectionToCollectionConverter");
    ConditionalGenericConverter conditionalGenericConverter = new CollectionToCollectionConverter(new DefaultConversionService());
    // 將Collection轉為Collection(注意:沒有指定泛型類型哦)
    System.out.println(conditionalGenericConverter.getConvertibleTypes());

    List<String> sourceList = Arrays.asList("1", "2", "2", "3", "4");
    TypeDescriptor sourceTypeDesp = TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class));
    TypeDescriptor targetTypeDesp = TypeDescriptor.collection(Set.class, TypeDescriptor.valueOf(Integer.class));

    System.out.println(conditionalGenericConverter.matches(sourceTypeDesp, targetTypeDesp));
    Object convert = conditionalGenericConverter.convert(sourceList, sourceTypeDesp, targetTypeDesp);
    System.out.println(convert.getClass());
    System.out.println(convert);
}

運行程序,正常輸出:

[java.util.Collection -> java.util.Collection]
true
class java.util.LinkedHashSet
[1, 2, 3, 4]

關注點:target最終使用的是LinkedHashSet來存儲,這結果和CollectionFactory#createCollection該API的實現邏輯是相關(Set類型默認創建的是LinkedHashSet實例)。

不足

如果說它的優點是功能強大,能夠處理複雜類型的轉換(PropertyEditor和前2個接口都只能轉換單元素類型),那麼缺點就是使用、自定義實現起來比較複雜。這不官方也給出了使用指導意見:在Converter/ConverterFactory接口能夠滿足條件的情況下,可不使用此接口就不使用。

ConditionalConverter

條件接口,@since 3.2。它可以為Converter、GenericConverter、ConverterFactory轉換增加一個前置判斷條件

public interface ConditionalConverter {
 boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

該接口的實現,截圖如下:可以看到,只有通用轉換器GenericConverter和它進行了合體。這也很容易理解,作為通用的轉換器,加個前置判斷將更加嚴謹和更安全。對於專用的轉換器如Converter,它已明確規定了轉換的類型,自然就不需要做前置判斷嘍。

✍總結

本文詳細介紹了Spring新一代的類型轉換接口,類型轉換作為Spring的基石,其重要性可見一斑。

PropertyEditor作為Spring早期使用「轉換器」,因存在眾多設計缺陷自Spring 3.0起被新一代轉換接口所取代,主要有:

Converter<S, T>:Source -> Target類型轉換接口,適用於1:1轉換ConverterFactory<S, R>:Source -> R類型轉換接口,適用於1:N轉換GenericConverter:更為通用的類型轉換接口,適用於N:N轉換

下篇文章將針對於GenericConverter的幾個特殊實現撰專文為你講解,你也知道做難事必有所得,做難事才有可能破局、破圈,歡迎保持關注。

✔✔✔推薦閱讀✔✔✔

【Spring類型轉換】系列:

【Jackson】系列:

【數據校驗Bean Validation】系列:

【新特性】系列:

【程序人生】系列:

還有諸如【Spring配置類】【Spring-static】【Spring數據綁定】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】...更多原創專欄,關注BAT的烏託邦回復專欄二字即可全部獲取,分享、成長,拒絕淺藏輒止。

有些專欄已完結,有些正在連載中,期待你的關注、共同進步

相關焦點

  • Spring自定義轉換類,讓@Value更方便
    我們可以在使用屬性的時候,再轉換成其它Bean。但這樣做有一些問題:無法做配置檢驗,不管是否配置錯誤,String類型的屬性都是可以讀取的;3 自定義轉換類使用自定義轉換類是更方便和安全的做法。我們來看看怎麼實現。
  • 重試機制!java retry(重試) spring retry, guava retrying 詳解
    設計思路springjava 註解註解可在方法上使用,定義需要重試的次數攔截指定需要重試的方法,解析對應的重試次數,然後進行對應次數的重試。還有一種方式,是開發者自己編寫重試機制,但是大多不夠優雅。註解式使用重試條件:遇到 RuntimeException重試次數:3重試策略:重試的時候等待 5S, 後面時間依次變為原來的 2 倍數。熔斷機制:全部重試失敗,則調用 recover() 方法。
  • 蘋果官方解讀全新 iPad Air 4:配備新一代 5nm 晶片 A14 仿生
    「今天,我們很高興推出這款全新設計的、更為強大的 iPad Air,同時這也是新一代 Apple 晶片,A14 仿生的首秀。」 Apple 全球市場營銷高級副總裁 Greg Joswiak 表示。▲ iPad Air 採用全面屏設計,配備更寬大的 10.9 英寸 Liquid 視網膜屏,全新觸控 ID 傳感器整合進頂部按鈕,攝像頭和音頻系統進一步升級,更首度搭載新一代 Apple 晶片 A14 仿生。
  • 詳解Spring框架的AOP機制
    ● 理解AOP的編程思想及原理● 掌握AOP的實現技術Spring框架的AOP機制可以讓開發者把業務流程中的通用功能抽取出來,單獨編寫功能代碼。在業務流程執行過程中,Spring框架會根據業務流程要求,自動把獨立編寫的功能代碼切入到流程的合適位置。
  • Adobe 在2018 MAX 大會上發布全新一代 Creative Cloud
    發布各旗艦應用程式的重大更新,進一步提升 Creative Cloud 的創新與價值 Adobe今日於 Adobe MAX創新大會上發布了Creative Cloud 中的幾項重大更新,其中包括 Photoshop CC、Lightroom
  • spring和spring boot常用註解及使用
    4. @Repository持久層組件,用於標註數據訪問組件,即DAO組件。5. @Component把普通pojo實例化到spring容器中,相當於配置文件中的 <bean id=""/>。
  • Spring Boot 單元測試
    MockMvc是由spring-test包提供,實現了對Http請求的模擬,能夠直接使用網絡的形式,轉換到Controller的調用,使得測試速度快、不依賴網絡環境。同時提供了一套驗證的工具,結果的驗證十分方便。
  • 大家轎 新體驗 試駕全新一代名圖
    這一價格更讓我確信,在新造型、舒適性、高級感、智能化四方面,都有著鮮明亮點的全新一代名圖,就是北京現代為中國車市帶來的全新選項:大家轎。  氣度不凡,大有可觀  全新一代名圖之所以能夠被稱之為「大家轎」,首先就是因為它真的很大,其長寬高分別為4780*1815*1460mm,軸距為2770mm,與上一代車型相比,車長增加了70mm,並且車高降低了10mm,因此,全新一代名圖看上去也更大氣,更沉穩、幹練。
  • Golang 類型斷言 vs 類型轉換
    類型斷言顧名思義,類型斷言用於斷言變量是屬於某種類型。類型斷言只能發生在interface{}類型上。上面類型斷言的例子,greeting是一個interface{}類型,我們為其分配了一個字符串。現在,我們可以認為greeting實際上是一個string,但是對外展示的是一個interface{}。
  • 面試官:你了解spring嗎?spring的兩大核心是什麼?
    bean還會去檢查是否存在該類型的子孫類型的bean,如果有則返回,如果找不到或找到多個則拋出異常。spring需要先創建出實例工廠的對象,在調用實例工廠對象上指定的普通方法來創建對象。所以實例工廠也需要配置到Spring中管理。
  • PowerJob全新一代分布式調度與計算框架
    PowerJob(原OhMyScheduler)是全新一代分布式調度與計算框架,能讓您輕鬆完成作業的調度與繁雜任務的分布式計算。簡介主要特性使用簡單:提供前端Web界面,允許開發者可視化地完成調度任務的管理(增、刪、改、查)、任務運行狀態監控和運行日誌查看等功能。
  • 【保姆級教程】Spring Boot 單元測試
    Spring Boot中引入單元測試很簡單,添加如下依賴(即spring-boot-starter-test依賴):<dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-test
  • spring自動裝配是什麼?spring自動裝配方式
    自動裝配是Spring框架的重要功能,是使用Spring滿足bean依賴的一種方式,Spring會在應用中為某個bean尋找其依賴的bean,Spring中bean有三種裝配機制,分別是:在xml中顯示配置、在java中顯示配置以及隱式的bean發現機制和自動裝配。
  • 一篇文章搞定JavaScript類型轉換
    還有 1 種複雜數據類型—對象(Object),Object 本質上是由一組無序的名值對組成的。ECMAScript 不支持任何創建自定義類型的機制,而所有值最終都將是上述 6 種數據類型之一。ES6 引入了一種新的原始數據類型 Symbol,表示獨一無二的值。
  • Adobe 在 MAX 2020 大會上發布全新一代 Creative Cloud,激發全民...
    中國,北京—2020年10月21日—Adobe (Nasdaq:ADBE) 於今日在賦能全球創意人士的創意大會 Adobe MAX 上發布了全新一代激發無限創造力:全新一代Creative CloudAdobe 今日發布的全新 Creative Cloud 包含數百個專業級設計新功能,旨在幫助創意工作者將其創造力發揮至最高水平。
  • 2021-Java後端工程師面試指南-(SpringBoot+SpringCloud)
    了解spring boot 中的spring factories 機制嗎?Spring Factories.這種機制實際上是仿照java中的SPI擴展機制實現的。二級緩存:Loading<Key,Value> readWriteCacheMap,本質上是guava的緩存,包含失效機制,保存服務信息的對外輸出數據結構。
  • 學習學習SpringSecurity
    ;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;
  • 釐清C++類型轉換
    1、舊式類型轉換開門見山,先聊聊筆者對類型轉換的看法吧。從設計上看,一門面向對象的語言是不一樣提供類型轉換的,這種方式破壞了類型系統。C++為了兼容C也不得不吞下這個苦果,在實際進行編程工作的過程之中,並不太推薦大家使用類型轉換。(Java在這裡就做了一些妥協,在基本類型之中提供了類型轉換。
  • 《科學》:細胞可用全新機制使用儲存能量
  • 這可能是對 Spring 事務原理講解最透徹的文章了
    Spring的事務機制向我們經常使用的JPA、mybatis,hibernate等數據訪問技術都有事務處理機制,他們提供了用來開啟事務、提交事務來完成數據操作的相關API,或者在發生錯誤的時候回滾數據。