5. 穿過擁擠的人潮,Spring已為你製作好高階賽道

YourBatman發表於2020-12-22

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

✍前言

你好,我是YourBatman。

上篇文章 大篇幅把Spring全新一代型別轉換器介紹完了,已經至少能夠考個及格分。在介紹Spring眾多內建的轉換器裡,我故意留下一個尾巴,放在本文專門撰文講解。

為了讓自己能在“擁擠的人潮中”顯得不(更)一(突)樣(出),A哥特意準備了這幾個特殊的轉換器助你破局,穿越擁擠的人潮,踏上Spring已為你製作好的高階賽道。

版本約定

  • Spring Framework:5.3.1
  • Spring Boot:2.4.0

✍正文

本文的焦點將集中在上文留下的4個型別轉換器上。

  • StreamConverter:將Stream流與集合/陣列之間的轉換,必要時轉換元素型別

這三個比較特殊,屬於“最後的”“兜底類”型別轉換器:

  • ObjectToObjectConverter:通用的將原物件轉換為目標物件(通過工廠方法or構造器)
  • IdToEntityConverter本文重點。給個ID自動幫你兌換成一個Entity物件
  • FallbackObjectToStringConverter:將任何物件呼叫toString()轉化為String型別。當匹配不到任何轉換器時,它用於兜底

預設轉換器註冊情況

Spring新一代型別轉換內建了非常多的實現,這些在初始化階段大都被預設註冊進去。註冊點在DefaultConversionService提供的一個static靜態工具方法裡:

static靜態方法具有與例項無關性,我個人覺得把該static方法放在一個xxxUtils裡統一管理會更好,放在具體某個元件類裡反倒容易產生語義上的誤導性

DefaultConversionService:

	public static void addDefaultConverters(ConverterRegistry converterRegistry) {
		// 1、新增標量轉換器(和數字相關)
		addScalarConverters(converterRegistry);
		// 2、新增處理集合的轉換器
		addCollectionConverters(converterRegistry);

		// 3、新增對JSR310時間型別支援的轉換器
		converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
		converterRegistry.addConverter(new StringToTimeZoneConverter());
		converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
		converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());

		// 4、新增兜底轉換器(上面處理不了的全交給這幾個哥們處理)
		converterRegistry.addConverter(new ObjectToObjectConverter());
		converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
		converterRegistry.addConverter(new FallbackObjectToStringConverter());
		converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
	}

	}

該靜態方法用於註冊全域性的、預設的轉換器們,從而讓Spring有了基礎的轉換能力,進而完成絕大部分轉換工作。為了方便記憶這個註冊流程,我把它繪製成圖供以你儲存:

特別強調:轉換器的註冊順序非常重要,這決定了通用轉換器的匹配結果(誰在前,優先匹配誰)。

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

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

StreamConverter

用於實現集合/陣列型別到Stream型別的互轉,這從它支援的Set<ConvertiblePair> 集合也能看出來:

@Override
public Set<ConvertiblePair> getConvertibleTypes() {
	Set<ConvertiblePair> convertiblePairs = new HashSet<ConvertiblePair>();
	convertiblePairs.add(new ConvertiblePair(Stream.class, Collection.class));
	convertiblePairs.add(new ConvertiblePair(Stream.class, Object[].class));
	convertiblePairs.add(new ConvertiblePair(Collection.class, Stream.class));
	convertiblePairs.add(new ConvertiblePair(Object[].class, Stream.class));
	return convertiblePairs;
}

它支援的是雙向的匹配規則:

程式碼示例

/**
 * {@link StreamConverter}
 */
@Test
public void test2() {
    System.out.println("----------------StreamConverter---------------");
    ConditionalGenericConverter converter = new StreamConverter(new DefaultConversionService());

    TypeDescriptor sourceTypeDesp = TypeDescriptor.valueOf(Set.class);
    TypeDescriptor targetTypeDesp = TypeDescriptor.valueOf(Stream.class);
    boolean matches = converter.matches(sourceTypeDesp, targetTypeDesp);
    System.out.println("是否能夠轉換:" + matches);

    // 執行轉換
    Object convert = converter.convert(Collections.singleton(1), sourceTypeDesp, targetTypeDesp);
    System.out.println(convert);
    System.out.println(Stream.class.isAssignableFrom(convert.getClass()));
}

執行程式,輸出:

----------------StreamConverter---------------
是否能夠轉換:true
java.util.stream.ReferencePipeline$Head@5a01ccaa
true

關注點:底層依舊依賴DefaultConversionService完成元素與元素之間的轉換。譬如本例Set -> Stream的實際步驟為:

也就是說任何集合/陣列型別是先轉換為中間狀態的List,最終呼叫list.stream()轉換為Stream流的;若是逆向轉換先呼叫source.collect(Collectors.<Object>toList())把Stream轉為List後,再轉為具體的集合or陣列型別。

說明:若source是陣列型別,那底層實際使用的就是ArrayToCollectionConverter,注意舉一反三

使用場景

StreamConverter它的訪問許可權是default,我們並不能直接使用到它。通過上面介紹可知Spring預設把它註冊進了註冊中心裡,因此面向使用者我們直接使用轉換服務介面ConversionService便可。

@Test
public void test3() {
    System.out.println("----------------StreamConverter使用場景---------------");
    ConversionService conversionService = new DefaultConversionService();
    Stream<Integer> result = conversionService.convert(Collections.singleton(1), Stream.class);

    // 消費
    result.forEach(System.out::println);
    // result.forEach(System.out::println); //stream has already been operated upon or closed
}

執行程式,輸出:

----------------StreamConverter使用場景---------------
1

再次特別強調:流只能被讀(消費)一次

因為有了ConversionService提供的強大能力,我們就可以在基於Spring/Spring Boot做二次開發時使用它,提高系統的通用性和容錯性。如:當方法入參是Stream型別時,你既可以傳入Stream型別,也可以是Collection型別、陣列型別,是不是瞬間逼格高了起來。

兜底轉換器

按照新增轉換器的順序,Spring在最後新增了4個通用的轉換器用於兜底,你可能平時並不關注它,但它實時就在發揮著它的作用。

ObjectToObjectConverter

將源物件轉換為目標型別,非常的通用:Object -> Object:

@Override
public Set<ConvertiblePair> getConvertibleTypes() {
	return Collections.singleton(new ConvertiblePair(Object.class, Object.class));
}

雖然它支援的是Object -> Object,看似沒有限制但其實是有約定條件的:

@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
	return (sourceType.getType() != targetType.getType() &&
			hasConversionMethodOrConstructor(targetType.getType(), sourceType.getType()));
}

是否能夠處理的判斷邏輯在於hasConversionMethodOrConstructor方法,直譯為:是否有轉換方法或者構造器。程式碼詳細處理邏輯如下截圖:

此部分邏輯可分為兩個part來看:

  • part1:從快取中拿到Member,直接判斷Member的可用性,可用的話迅速返回
  • part2:若part1沒有返回,就執行三部曲,嘗試找到一個合適的Member,然後放進快取內(若沒有就返回null)

part1:快速返回流程

當不是首次進入處理時,會走快速返回流程。也就是第0步isApplicable判斷邏輯,有這幾個關注點:

  1. Member包括Method或者Constructor
  2. Method:若是static靜態方法,要求方法的第1個入參型別必須是源型別sourceType;若不是static方法,則要求源型別sourceType必須是method.getDeclaringClass()的子型別/相同型別
  3. Constructor:要求構造器的第1個入參型別必須是源型別sourceType

建立目標物件的例項,此轉換器支援兩種方式:

  1. 通過工廠方法/例項方法建立例項(method.invoke(source)
  2. 通過構造器建立例項(ctor.newInstance(source)

以上case,在下面均會給出程式碼示例。

part2:三部曲流程

對於首次處理的轉換,就會進入到詳細的三部曲邏輯:通過反射嘗試找到合適的Member用於建立目標例項,也就是上圖的1、2、3步。

step1:determineToMethod,從sourceClass裡找例項方法,對方法有如下要求:

  • 方法名必須叫 "to" + targetClass.getSimpleName(),如toPerson()
  • 方法的訪問許可權必須是public
  • 該方法的返回值必須是目標型別或其子型別

step2:determineFactoryMethod,找靜態工廠方法,對方法有如下要求:

  • 方法名必須為valueOf(sourceClass) 或者 of(sourceClass) 或者from(sourceClass)
  • 方法的訪問許可權必須是public

step3:determineFactoryConstructor,找構造器,對構造器有如下要求:

  • 存在一個引數,且引數型別是sourceClass型別的構造器
  • 構造器的訪問許可權必須是public

特別值得注意的是:此轉換器支援Object.toString()方法將sourceType轉換為java.lang.String。對於toString()支援,請使用下面介紹的更為兜底的FallbackObjectToStringConverter

程式碼示例

  • 例項方法
// sourceClass
@Data
public class Customer {
    private Long id;
    private String address;

    public Person toPerson() {
        Person person = new Person();
        person.setId(getId());
        person.setName("YourBatman-".concat(getAddress()));
        return person;
    }

}

// tartgetClass
@Data
public class Person {
    private Long id;
    private String name;
}

書寫測試用例:

@Test
public void test4() {
    System.out.println("----------------ObjectToObjectConverter---------------");
    ConditionalGenericConverter converter = new ObjectToObjectConverter();

    Customer customer = new Customer();
    customer.setId(1L);
    customer.setAddress("Peking");

    Object convert = converter.convert(customer, TypeDescriptor.forObject(customer), TypeDescriptor.valueOf(Person.class));
    System.out.println(convert);

    // ConversionService方式(實際使用方式)
    ConversionService conversionService = new DefaultConversionService();
    Person person = conversionService.convert(customer, Person.class);
    System.out.println(person);
}

執行程式,輸出:

----------------ObjectToObjectConverter---------------
Person(id=1, name=YourBatman-Peking)
Person(id=1, name=YourBatman-Peking)
  • 靜態工廠方法
// sourceClass
@Data
public class Customer {
    private Long id;
    private String address;
}

// targetClass
@Data
public class Person {

    private Long id;
    private String name;

    /**
     * 方法名稱可以是:valueOf、of、from
     */
    public static Person valueOf(Customer customer) {
        Person person = new Person();
        person.setId(customer.getId());
        person.setName("YourBatman-".concat(customer.getAddress()));
        return person;
    }
}

測試用例完全同上,再次執行輸出:

----------------ObjectToObjectConverter---------------
Person(id=1, name=YourBatman-Peking)
Person(id=1, name=YourBatman-Peking)

方法名可以為valueOf、of、from任意一種,這種命名方式幾乎是業界不成文的規矩,所以遵守起來也會比較容易。但是:建議還是註釋寫好,防止別人重新命名而導致轉換生效。

  • 構造器

基本同靜態工廠方法示例,略

使用場景

基於本轉換器可以完成任意物件 -> 任意物件的轉換,只需要遵循方法名/構造器預設的一切約定即可,在我們平時開發書寫轉換層時是非常有幫助的,藉助ConversionService可以解決這一類問題。

對於Object -> Object的轉換,另外一種方式是自定義Converter<S,T>,然後註冊到註冊中心。至於到底選哪種合適,這就看具體應用場景嘍,本文只是多給你一種選擇

IdToEntityConverter

Id(S) --> Entity(T)。通過呼叫靜態查詢方法將實體ID兌換為實體物件。Entity裡的該查詢方法需要滿足如下條件find[EntityName]([IdType])

  1. 必須是static靜態方法
  2. 方法名必須為find + entityName。如Person類的話,那麼方法名叫findPerson
  3. 方法引數列表必須為1個
  4. 返回值型別必須是Entity型別

說明:此方法可以不必是public,但建議用public。這樣即使JVM的Security安全級別開啟也能夠正常訪問

支援的轉換Pair如下:ID和Entity都可以是任意型別,能轉換就成

@Override
public Set<ConvertiblePair> getConvertibleTypes() {
	return Collections.singleton(new ConvertiblePair(Object.class, Object.class));
}

判斷是否能執行準換的條件是:存在符合條件的find方法,且source可以轉換為ID型別(注意source能轉換成id型別就成,並非目標型別哦)

@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
	Method finder = getFinder(targetType.getType());
	return (finder != null 
		&& this.conversionService.canConvert(sourceType, TypeDescriptor.valueOf(finder.getParameterTypes()[0])));
}

根據ID定位到Entity實體物件簡直太太太常用了,運用好此轉換器的提供的能力,或許能讓你事半功倍,大大減少重複程式碼,寫出更優雅、更簡潔、更易於維護的程式碼。

程式碼示例

Entity實體:準備好符合條件的findXXX方法

@Data
public class Person {

    private Long id;
    private String name;

    /**
     * 根據ID定位一個Person例項
     */
    public static Person findPerson(Long id) {
        // 一般根據id從資料庫查,本處通過new來模擬
        Person person = new Person();
        person.setId(id);
        person.setName("YourBatman-byFindPerson");
        return person;
    }

}

應用IdToEntityConverter,書寫示例程式碼:

@Test
public void test() {
    System.out.println("----------------IdToEntityConverter---------------");
    ConditionalGenericConverter converter = new IdToEntityConverter(new DefaultConversionService());

    TypeDescriptor sourceTypeDesp = TypeDescriptor.valueOf(String.class);
    TypeDescriptor targetTypeDesp = TypeDescriptor.valueOf(Person.class);
    boolean matches = converter.matches(sourceTypeDesp, targetTypeDesp);
    System.out.println("是否能夠轉換:" + matches);

    // 執行轉換
    Object convert = converter.convert("1", sourceTypeDesp, targetTypeDesp);
    System.out.println(convert);
}

執行程式,正常輸出:

----------------IdToEntityConverter---------------
是否能夠轉換:true
Person(id=1, name=YourBatman-byFindPerson)

示例效果為:傳入字串型別的“1”,就能返回得到一個Person例項。可以看到,我們傳入的是字串型別的的1,而方法入參id型別實際為Long型別,但因為它們能完成String -> Long轉換,因此最終還是能夠得到一個Entity例項的。

使用場景

這個使用場景就比較多了,需要使用到findById()的地方都可以通過它來代替掉。如:

Controller層:

@GetMapping("/ids/{id}")
public Object getById(@PathVariable Person id) {
    return id;
}

@GetMapping("/ids")
public Object getById(@RequestParam Person id) {
    return id;
}

Tips:在Controller層這麼寫我並不建議,因為語義上沒有對齊,勢必在程式碼書寫過程中帶來一定的麻煩。

Service層:

@Autowired
private ConversionService conversionService;

public Object findById(String id){
    Person person = conversionService.convert(id, Person.class);

    return person;
}

Tips:在Service層這麼寫,我個人覺得還是OK的。用型別轉換的領域設計思想代替了自上而下的過程程式設計思想。

FallbackObjectToStringConverter

通過簡單的呼叫Object#toString()方法將任何支援的型別轉換為String型別,它作為底層兜底。

@Override
public Set<ConvertiblePair> getConvertibleTypes() {
	return Collections.singleton(new ConvertiblePair(Object.class, String.class));
}

該轉換器支援CharSequence/StringWriter等型別,以及所有ObjectToObjectConverter.hasConversionMethodOrConstructor(sourceClass, String.class)的型別。

說明:ObjectToObjectConverter不處理任何String型別的轉換,原來都是交給它了

程式碼示例

略。

ObjectToOptionalConverter

將任意型別轉換為一個Optional<T>型別,它作為最最最最最底部的兜底,稍微瞭解下即可。

程式碼示例

@Test
public void test5() {
    System.out.println("----------------ObjectToOptionalConverter---------------");
    ConversionService conversionService = new DefaultConversionService();
    Optional<Integer> result = conversionService.convert(Arrays.asList(2), Optional.class);

    System.out.println(result);
}

執行程式,輸出:

----------------ObjectToOptionalConverter---------------
Optional[[2]]

使用場景

一個典型的應用場景:在Controller中可傳可不傳的引數中,我們不僅可以通過@RequestParam(required = false) Long id來做,還是可以這麼寫:@RequestParam Optional<Long> id

✍總結

本文是對上文介紹Spring全新一代型別轉換機制的補充,因為關注得人較少,所以才有機會突破。

針對於Spring註冊轉換器,需要特別注意如下幾點:

  1. 註冊順序很重要。先註冊,先服務(若支援的話)
  2. 預設情況下,Spring會註冊大量的內建轉換器,從而支援String/數字型別轉換、集合型別轉換,這能解決協議層面的大部分轉換問題。
    1. 如Controller層,輸入的是JSON字串,可用自動被封裝為數字型別、集合型別等等
    2. 如@Value注入的是String型別,但也可以用數字、集合型別接收

對於複雜的物件 -> 物件型別的轉換,一般需要你自定義轉換器,或者參照本文的標準寫法完成轉換。總之:Spring提供的ConversionService專注於型別轉換服務,是一個非常非常實用的API,特別是你正在做基於Spring二次開發的情況下。

當然嘍,關於ConversionService這套機制還並未詳細介紹,如何使用?如何執行?如何擴充套件?帶著這三個問題,我們們下篇見。


✔✔✔推薦閱讀✔✔✔

【Spring型別轉換】系列:

【Jackson】系列:

【資料校驗Bean Validation】系列:

【新特性】系列:

【程式人生】系列:

還有諸如【Spring配置類】【Spring-static】【Spring資料繫結】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】...更多原創專欄,關注BAT的烏托邦回覆專欄二字即可全部獲取,也可加我fsx1056342982,交個朋友。

有些已完結,有些連載中。我是A哥(YourBatman),我們們下期再見

相關文章