11. 春節禮物:Spring的Registrar倒排思想送給你

前言

你好,我是A哥(YourBatman)。

今天是2021辛丑年(牛年)春節,首先當然是祝福打開文章的您及家人:新年快樂。

請人喫飯不如請人出汗,請人出汗不如送人以漁。A哥春節繼續營業,這個時候還能看得下去這種技術文章的同學我猜有三類:

  1. 要麼孤獨了
  2. 要麼喝醉了
  3. 要麼喝醉後覺得孤獨了

現實情況往往挺扎心,所以牢記使命,砥礪前行是個好辦法。

上篇文章 把@DateTimeFormat和@NumberFormat註解的實現原理搞清楚了,通過面向元數據編程屏蔽了理解層面、實施層面上的差異化。同時,通過手敲代碼案例,紮紮實實、徹徹底底搞明白了@DateTimeFormat等註解有何用以及如何用,從此不再虛。

像AnnotationFormatterFactory、xxxConverter這種均屬於low-level底層API,上手起來一般頗具難度。一個良好的、流行的框架最起碼應該是上手簡單的,所以開發者應該是最多關心到FormattingConversionService/ConversionService層面即止。本文帶你看看Spring是如何做到醬紫的~

本文提綱

版本約定

  • Spring Framework:5.3.x
  • Spring Boot:2.4.x

正文

上文是通過手動調用API的方式實現元數據的解析從而達到數據格式化(轉換)的目的,而在實際應用場景中,作爲業務開發者是不可能去直接去操縱API的,畢竟說到底那對開發者太不友好,使用門檻過高。

因此,本文將介紹的是一種更爲“高級”的使用方案,看看Spring是如何做到兼具高擴展性的整合,從而對開發者十分友好,相信這便也是Spring最有魅力的地方,一起來學習學習吧。

FormatterRegistry:註冊中心

對於多組件的管理,註冊中心是個很好的解決方案。

FormatterRegistry其實在:9. 細節見真章,Formatter註冊中心的設計很討巧 這篇文章已經有過很詳細的分析,學到了它那非常巧妙的設計,這裏也順道推薦你花幾分鐘前往看看。在這篇文章的末尾,A哥故意留下了一個小尾巴沒講:註冊中心對註解工廠AnnotationFormatterFactory的支持,也就是這個接口方法:

FormatterRegistry:

 void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);

現在時機成熟,本文就來重點關照它。

提示:看本文之前,建議你務必對AnnotationFormatterFactory瞭解後效果更佳。相關文章,點擊這裏電梯直達

該接口方法的唯一實現在FormattingConversionService裏:

:從AnnotationFormatterFactory的泛型類型中提取到註解類型。注意:若沒有指定泛型(沒有指定註解類型)就拋出異常:該工廠類支持的類型們:對於支持的每個類型,均註冊一個Printer/Parser

重點在於步驟③,AnnotationPrinterConverter和AnnotationParserConverter均是一個ConditionalGenericConverter轉換器,底層實現實際委託給AnnotationFormatterFactory去完成,所以說對AnnotationFormatterFactory的理解格外的重要,還好上篇文章對它已經做了詳盡分析,點擊這裏電梯直達

下面以AnnotationPrinterConverter爲例觀其源碼:

:該轉換器只負責將fieldType類型轉換爲String類型:只有fieldType上標註有指定的這個註解,此轉換器纔會生效:轉換邏輯。這種緩存式處理邏輯很是常見,其實最核心的代碼往往只有一句,本處就是它:this.annotationFormatterFactory.getPrinter(...)。獲取到合適的Printer,然後適配爲PrinterConverter從而完成最終的convert轉換動作

說明:PrinterConverter和ParserConverter在本系列前面文章已介紹,相關內容可出門左拐在本系列內很容易找到

AnnotationParserConverter的實現邏輯如出一轍,這裏就不再囉嗦了。

FormattingConversionService它實現了FormatterRegistry接口的所有接口方法,但是它並未提供一些默認行爲。換句話講:實現了所有的組件註冊/管理的能力,但並沒有“幫你”註冊任何組件,所以還不具備能夠直接提供服務的條件,若要使用還需“人工干預”放些組件進去纔行。

一般來講,對於這種情況一般在外部再包一層 DefaultXXX來提供默認服務是一種對開發者十分友好的解決方案,Spring也是這麼幹的,下面來看看DefaultFormattingConversionService爲我們默認註冊了哪些基礎組件,提供了哪些能力呢。

DefaultFormattingConversionService

默認的格式化器轉換服務,該默認行爲適用於大多數應用程序對格式化器、轉換器的需求。

繼承自FormattingConversionService,這個默認行爲是爲該實例而設計的,但爲了方便使用,它對外暴露了其static靜態方法addDefaultFormatters(),這個設計方式同DefaultConversionService暴露了靜態方法addDefaultConverters()如出一轍。

默認註冊了哪些組件?

對於一個默認的Service服務,最關心的當屬它提供了哪些能力。換句話講:它默認幫我們註冊了哪些組件呢?

要回答這個問題可不能靠“背答案”,方式方法其實非常的簡單,爬進去它的源碼處一看便知:

:雖然說本類(其實是父類)實現了EmbeddedValueResolverAware接口,但構造時依舊可以指定佔位符處理器StringValueResolver,當然一般情況下傳入null即可:調用DefaultConversionService的靜態方法,把默認的轉換器們都註冊進來。那麼,默認到底註冊了哪些轉換器呢?DefaultConversionService.addDefaultConverters(this)該靜態方法其實是本系列前面文章所講的內容,這裏A哥順道也貼在這吧:

:若registerDefaultFormatters爲true就添加默認的格式化器們,一般來講,此值都爲true。那麼,默認到底註冊了哪些格式化器呢?

  • :對@NumberFormat註解提供支持,格式化數字(Currency、數字、百分數等)
  • :對JSR 354錢幣類型javax.money.CurrencyUnit、Monetary等類型提供支持。一般情況下, 用不着,所以此part不會被真的註冊
  • :對JSR-310日期時間的格式化提供支持。這裏使用到了其專用的註冊器DateTimeFormatterRegistrar統一操作
  • ④、⑤:第4、5步是互斥操作,若有Jota-Time就提供對它的支持而不觸發java.util.Date的註冊器,否則使用後者註冊器。

注意:你以爲④、⑤是真的互斥嗎?難道導入了joda-time的包後java.util.Date相關模塊就失效了?很明顯不是這樣的,讓你“放心”的地方在於JodaTimeFormatterRegistrar註冊器內部包含了java.util.Date格式化器的註冊關係,因此一切都還得到xxxRegistrar裏去看才能揭曉。

總之,DefaultFormattingConversionService作爲默認的格式化轉換服務,它是DefaultConversionService的超集,在其基礎上擴展了格式化器,格式化註解支持等相關能力。在Spring環境下,大多數情況使用都是它而非DefaultConversionService。

現在,對FormatterRegistry類一個籠統的認識,知道它默認給註冊了哪些組件,支持哪些功能,但是細節部分還不清晰。比如說:支持哪些數據類型?支持哪些格式?這些都藏在相應的xxxRegistrar裏~


FormatterRegistrar:註冊員

registrar:登記員;註冊主任。

xxxRegistrar它是一種“倒排”思想的設計體現,能達到高內聚的效果。Spring、Spring Boot慣用的“伎倆”,譬如你隨便一搜就能看能看到很多很多:

FormatterRegistrar代表的是格式化器註冊員接口,接口定義:

public interface FormatterRegistrar {
 void registerFormatters(FormatterRegistry registry);
}

接口方法含義:將Converter和Formatter註冊進FormatterRegistry註冊中心裏,至於註冊哪些組件由各子類自行管理和負責,而非Registry註冊中心主動去編排。這是一種倒排設計思想,能夠很好的達到高內聚的目的。

注意:雖然存在ConverterRegistry和FormatterRegistry兩個接口,但只有FormatterRegistrar而 沒有 ConverterRegistrar哦

該接口有三個實現類:

見名之意,每個實現子類都維護着自己分內之事,邊界十分清晰。

DateFormatterRegistrar:Date註冊員

提供對java.util.Date、java.util.Calendar、long類型的日期時間的註冊支持。

接口方法實現如下:

:添加常規轉換器,支持DateToLong、DateToCalendar、LongToCalendar等基礎轉換能力:若有個性化指定格式化器,那就給Calendar專門使用。當然,大多數情況下並不會這麼做,這步邏輯是爲了向後兼容性而考慮而已,一般可忽略添加@DateTimeFormat註解的解析支持

代碼示例

下面介紹DateFormatterRegistrar註冊員的使用示例。

普通使用方式

最常規的轉換,Date、Long、Calendar等日期時間類型似乎是可以互轉的。

@Test
public void test1() {
    FormattingConversionService conversionService = new FormattingConversionService();
    // 註冊員負責添加格式化器以支持Date系列的轉換
    new DateFormatterRegistrar().registerFormatters((FormatterRegistry) conversionService);

    // 1、普通使用
    long currMills = System.currentTimeMillis();
    System.out.println("當前時間戳:" + currMills);
    // Date -> Calendar
    System.out.println(conversionService.convert(new Date(currMills), Calendar.class));
    // Long ->  Date
    System.out.println(conversionService.convert(currMills, Date.class));
    // Calendar -> Long
    Calendar calendar = Calendar.getInstance(TimeZone.getDefault());
    calendar.setTimeInMillis(currMills);
    System.out.println(conversionService.convert(calendar, Long.class));
}

運行程序,輸出:

當前時間戳:1612741385457
java.util.GregorianCalendar[time=1612741385457 ...
Mon Feb 08 07:43:05 CST 2021
1612741385457

完美。

註解使用方式

使用更高級的註解方式,如@DateTimeFormat

// 準備一個Java Bean:
@Data
@AllArgsConstructor
class Son {

    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private Date birthday;

}

測試代碼:

@Test
public void test1() {
    FormattingConversionService conversionService = new FormattingConversionService();
    // 重要:重要:重要:註冊基礎的轉換能力
    DefaultConversionService.addDefaultConverters((ConverterRegistry) conversionService);
    // 註冊員負責添加格式化器以支持Date系列的轉換
    new DateFormatterRegistrar().registerFormatters((FormatterRegistry) conversionService);

    // 1、註解使用
    Son son = new Son(new Date());
    // 輸出:將Date類型輸出爲Long類型
    System.out.println(conversionService.convert(son.getBirthday(), Long.class));
    // 輸出:將String烈性輸入爲Date類型
    // System.out.println(conversionService.convert("2021-02-12", Date.class)); // 報錯
    System.out.println(conversionService.convert(1613034123709L, Date.class));
}

運行程序,輸出:

1613034230018
Thu Feb 11 17:02:03 CST 2021

完美。實現了Long類型 <->  Date類型的互轉。

可能有同學會問了,爲毛"2021-02-12"就不能convert到Date類型呢?這個原因,額,嗯,哼,若你看了上篇文章 的話,這將不會是個問題。

當然,在實際使用中,更多的情況是String -> Date的轉換case,怎麼破?有兩個辦法:

  1. 回味本系列前面文章,因爲前面有講了不止一次
  2. 關注後面文章。因爲此case過於常見,後面(特別是在Spring MVC下使用)依舊會重點提及

總結

本文重點是想經由FormatterRegistry註冊中心,引述出Spring常用的Registrar註冊員設計思想,它是一種面向對象編程思想的體現,是不是比面向過程優雅很多呢?本文以DateTimeFormatterRegistrar爲示例進行了打樣,可以看到Spring在API抽象這塊着實是非常優秀的,擴展性和方便性兼具,這個度把握得絕佳,或許這也算是設計美學吧。

到這應該感謝自己竟在春節這一天讀了一篇技術文章,實屬不易,也可以給自己說聲:哎喲,不錯!

最後,A哥真誠的祝願您以及您的家人新春愉快,身體健康,牛氣沖天。2021,一起衝,目標是星辰大海,堅持不懈,永不止步。

預告:JSR 310是現在的日期時間主流,Spring對它提供的支持非常非常非常全面,自然也稍顯複雜。下篇文章就一起搞定它,啃下這個硬骨頭,絕對小小知識點,大大收穫喲。春節營業,歡迎關注!

本文思考題

本文所屬專欄:Spring類型轉換,後臺回覆專欄名即可獲取全部內容,已被https://www.yourbatman.cn收錄。

看完了不一定懂,看懂了不一定會。來,文末3個思考題幫你覆盤:

  1. xxxRegistrar是什麼設計思想的體現?
  2. 你覺得xxxRegistrar有什麼好處?
  3. 如何實現String ->  Date類型的轉換?回憶一下?

系列推薦

System.out.println("點個贊吧!");
print_r('關注【BAT的烏托邦】!');
var_dump('私聊A哥:fsx1056342982');
console.log("點個贊吧!");
NSLog(@"關注【BAT的烏托邦】!");
print("私聊A哥:fsx1056342982");
echo("點個贊吧!");
cout << "關注【BAT的烏托邦】!" << endl;
printf("私聊A哥:fsx1056342982");
Console.WriteLine("點個贊吧!");
fmt.Println("關注【BAT的烏托邦】!");
Response.Write("私聊A哥:fsx1056342982");
alert("點個贊吧!");

A哥(YourBatman):Spring Framework開源貢獻者,Java架構師,領域專家。文章不標題黨,不譁衆取寵,每篇文章都成系列去系統的攻破一個知識點,每個系列可能是全網最佳/唯一。注重基本功修養,底層基礎決定上層建築。現有IDEA系列、Spring N多系列、Bean Validation系列、日期時間系列......關注免費獲取

本文分享自微信公衆號 - BAT的烏托邦(BAT-utopia)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章