5. 穿過擁擠的人潮,Spring已爲你製做好高級賽道

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

✍前言

你好,我是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裏統一管理會更好,放在具體某個組件類裏反倒容易產生語義上的誤導性app

DefaultConversionService:

	public static void addDefaultConverters(ConverterRegistry converterRegistry) {
		// 一、添加標量轉換器(和數字相關)
		addScalarConverters(converterRegistry);
		// 二、添加處理集合的轉換器
		addCollectionConverters(converterRegistry);

		// 三、添加對JSR310時間類型支持的轉換器
		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有了基礎的轉換能力,進而完成絕大部分轉換工做。爲了方便記憶這個註冊流程,我把它繪製成圖供以你保存:框架

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

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

  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用於建立目標實例,也就是上圖的一、二、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),我們下期再見

相關文章
相關標籤/搜索