前言
你好,我是A哥(YourBatman)。好久不見,甚是想念,女王節快樂!
回憶本系列前兩篇的文章內容,主要講解了@DateTimeFormat
和@NumberFormat
註解的實現原理細節,以及FormatterRegistrar格式化器註冊員等相關內容。由於時間相隔稍微有點久了,所以鏈接貼在這方便你做回顧:
根據反饋,這兩篇文章所講“知識點”依然存在可能不全,或者沒講太明白的地方。我自己反覆讀了兩遍,覺得會有至少這兩點疑惑:
-
@DateTimeFormat
等註解到底是如何工作的? -
JSR 310日期時間註冊員Registrar做了哪些工作?
以這兩個問題爲主線,此文將繼續把這(兩)部分內容補充完整。
本文提綱
版本約定
-
Spring Framework:5.3.x -
Spring Boot:2.4.x
正文
Spring中的轉換器、格式化器是整個Spring技術棧體系中非常重要的一份子,是衆多高級特性的基礎支撐。
作爲一個Spring的使用者,也許你工作了好幾年都只接觸到@DateTimeFormat
這個註解才感知到Spring是有格式化能力的;也許你在使用xml配置、Spring MVC時全然不知自動化封裝的流程,也就感知不到Converter轉換器模塊的存在;也許你還一直不確定@DateTimeFormat
能標註在哪些類型上,每次使用時都得用谷歌百度一下......
作爲一個Spring的開發者,以上不應該再成爲問題。而是能說會道,滾瓜爛熟。下面將本文補充內容傳遞給你,坐穩發車嘍。
@DateTimeFormat註解到底做了什麼?
不用猜,很多程序員同學知道/使用@DateTimeFormat註解是在Spring MVC場景,甚至只是在此場景:前端傳一個日期時間格式的值,後端使用Date/LocalDateTime接收此值時使用。
Request的請求實體形如這樣:
@Data
public class Person{
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime arriveTime;
}
這麼一來,前端傳入"2021-03-07 21:00:00"這種格式的字符串就能被自動封裝進arriveTime了。
❝說明:String -> LocalDateTime arriveTime屬於Parser功能(也稱作輸入),此註解在xxx -> String輸出時(Printer功能)也會生效的
❞
使用了@DateTimeFormat這麼久,你是否知道它並不屬於spring-web/spring-webmvc
模塊的類,而是屬於spring-context:org.springframework.format.annotation.DateTimeFormat
。換句話講:@DateTimeForma它屬於基礎設施類,並不是只能用於web層,而是可用於所有有需要轉換的地方。
通過上篇文章 我們知道了,@DateTimeFormat和@NumberFormat註解的功能底層是依賴於AnnotationFormatterFactory
以及格式化器註冊中心FormatterRegistry
核心API去完成的。那麼這個流程是怎樣的呢?
可能這麼說還是覺得比較抽象,那麼我嘗試畫了一幅流程圖,可助你掌握這部分的核心工作原理(執行流程):該流程可釋義爲:通過格式化器註冊中心FormatterRegistry的API向其註冊註解工廠AnnotationFormatterFactory以支持格式化註解。但是,底層其實都(爲每個FieldType類型)適配爲了Converter才註冊到FormatterRegistry進去的。換句話講:FormatterRegistry(其實是ConverterRegistry)底層管理的永遠是一些簡單的Converter轉換器們,這便也符合了越底層越抽象,越上層越具體的設計原則,是一種良好的設計方案。
❝值得注意:ConverterRegistry管理的底層這些Converter是分爲三大類的喲。1:1、1:N、N:N
❞
向註冊中心註冊完成後,轉換服務就具備了AnnotationFormatterFactory所支持的類型FieldType <-> String
互相轉換的能力了。當然嘍,讓其能執行轉換動作還有個前提條件是FieldType上必須標註有AnnotationFormatterFactory指定的註解類型纔行,這個時候@DateTimeFormat就發揮作用啦。
這麼來看,@DateTimeFormat
註解自己其實並未做什麼,只是純被當做Field上的一個元數據被用作參與判斷、格式化時所需參數的指定,此註解它是面向開發者的。真正做了“很多事”的其實是AnnotationFormatterFactory和FormatterRegistry等底層核心API,它們在初始化階段就默默全部完成,而這一切(較爲複雜)的邏輯對開發者是完全透明的。
JSR 310日期時間註冊員
上篇文章 介紹了Spring格式化器倒排思想,其具體體現在FormatterRegistrar接口的設計,上文用“比較古老”的支持java.util.Date類型的DateFormatterRegistrar打了個樣,體驗了一把倒排設計的好處。
我們知道在Java領域日期時間類型分爲三大領域:老Date體系、JSR 310體系、Joda-time體系。這不FormatterRegistrar接口的繼承體系三個實現類剛好與之對應:A哥不建議在開發中再以任何理由再使用Date類型,而是用JSR 310取以代之。因此接下來,就看看DateTimeFormatterRegistrar註冊員爲我們做了哪些事。
DateTimeFormatterRegistrar:JSR 310註冊員
Since 4.0。在Spring下使用以支持JSR 310日期時間的格式化/轉換。
我們知道,JSR 310對日期時間的格式化其實已經非常完善了,具體都體現在java.time.format.DateTimeFormatter
這個Java原生API裏。Spring針對於JSR 310日期時間類型格式化只是在DateTimeFormatter的基礎上做了簡單封裝和適配,讓它使用起來的姿勢儘量和Date/JodaTime保持一致,以便對開發者更加友好,代碼結構設計上也能夠趨近於統一。
本系列前面文章介紹過的DateTimeFormatterFactory便是對DateTimeFormatter的簡單包裝,用於生產格式化器實例的工廠。此處的DateTimeFormatterRegistrar就使用它倆來進行一系列註冊動作,因此可理解爲他是更上層的封裝形式。
源碼分析
下面從源碼下手一探究竟。截圖裏示例出該實現類支持的類型,這裏用自定義的枚舉類來更抽象的方式定義爲三類了,即日期、時間、日期時間。這三大類其實包含了JSR 310類型的主要API,包括:LocalDate、LocalTime、LocalDateTime、ZonedDateTime、OffsetDateTime、OffsetTime
共計6個API。對比一下這不正就是Jsr310DateTimeFormatAnnotationFormatterFactory所支持的六大類型麼,如下截圖所示:說明:該份截圖是說明@DateTimeFormat只能標註在JSR 310日期時間的這6種類型上纔有效哦。
其實,在任何時候Spring都不建議你直接使用原生的DateTimeFormatter
這個API,而是用其封裝過的org.springframework.format.datetime.standard.DateTimeFormatterFactory
來獲得一個DateTimeFormatter實例,以便使用起來更具統一性和靈活性。
這不DateTimeFormatterRegistrar
它就是這麼來乾的:這是唯一構造器:3個類型對應的DateTimeFormatter均由Spring封裝過的DateTimeFormatterFactory工廠來“動態”產生,而非直接綁定。由於DateTimeFormatter被設計爲不可變,若初始化時就綁定上,後面將無法做定製化設置。這也是引入DateTimeFormatterFactory來做定製化參數“緩存”的又一作用~
由於使用DateTimeFormatterFactory而並非直接使用DateTimeFormatter,就可以很方便的對不同類型做參數定製化,如下方法們,它們是作用在DateTimeFormatterFactory上的,從而可以確保多個條件共存:當然,最重要的當屬對FormatterRegistrar 接口方法 的實現邏輯:①:這個 步驟類似於上文講述DateFormatterRegistrar時調用其public靜態方法addDateConverters(registry)
,作用爲註冊基礎轉換器(如Date -> Calendar,Date -> Long的Converter轉換器),從而提供基本的轉換能力。值得注意的是:DateTimeConverters.registerConverters(registry)
內部調用了DateFormatterRegistrar.addDateConverters(registry)
,並且額外增加了LocalDate、Calendar、Long、Instant等等的Converter轉換器(如ZonedDateTimeToLocalDateConverter、LongToInstantConverter等等),後者是前者的超集。
❝無獨有偶:jodaTime的
❞JodaTimeConverters.registerConverters(registry)
內部必然也調用了DateFormatterRegistrar.addDateConverters(registry)嘍,感興趣可自己去瞅瞅確認下
②:生成每個類型對應的格式化器。簡單的講就是通過DateTimeFormatterFactory創建出對應的格式化器DateTimeFormatter③:這一步的作用在源碼中的註釋部分解釋得很清楚了,這一大段代碼的作用是使用ISO_LOCAL_*
這種變種格式化器來代替執行,效果是性能提升2倍
❝說明:這個做法在前文提到的
❞Jsr310DateTimeFormatAnnotationFormatterFactory
裏getPrinter()生成格式化器時也被用到了用以成倍提升轉換性能
④:對於不需要特殊提速的類型,註冊綁定上專用的格式化器org.springframework.format.Formatter
即可。如PeriodFormatter、DurationFormatter等
⑤:讓@DateTimeFormat
註解對JSR 310日期時間提供支持。關於格式化註解方面的知識,請向上爬2層樓 or 點擊文首/文末推薦鏈接均可進入文章進行詳細瞭解,加深記憶。
代碼示例
下面介紹DateTimeFormatterRegistrar註冊員的使用示例,其中包括API使用方式,以及面向註解的使用方式。
API使用方式
此類使用方式一般門檻較高,需要對底層API有較熟了解才能運用自如,一般是需要在Spring基礎上做二次開發的小夥伴纔會用到,用個簡單示例瞭解一下用法:
@Test
public void test1() {
FormattingConversionService conversionService = new FormattingConversionService();
// 註冊員負責添加格式化器以支持Date系列的轉換
new DateTimeFormatterRegistrar().registerFormatters((FormatterRegistry) conversionService);
// 1、普通使用(API方式)
LocalDateTime now = LocalDateTime.now();
System.out.println("當前時間:" + now);
System.out.println("LocalDateTime轉爲LocalDate:" + conversionService.convert(now, LocalDate.class));
System.out.println("LocalDateTime轉爲LocalTime:" + conversionService.convert(now, LocalTime.class));
// 時間戳轉Instant
long currMills = System.currentTimeMillis();
System.out.println("當前時間戳:" + currMills);
System.out.println("時間戳轉Instant:" + conversionService.convert(currMills, Instant.class));
}
運行程序,輸出:
當前時間:2021-03-07T21:19:39.752
LocalDateTime轉爲LocalDate:2021-03-07
LocalDateTime轉爲LocalTime:21:19:39.752
當前時間戳:1615123179763
時間戳轉Instant:2021-03-07T13:19:39.763Z
完美。
通過這個示例,現在知道爲啥前端傳個時間戳,後端不用Long而使用Instant也能“接得住”不報錯了吧~
註解使用方式
見與Spring MVC整合使用方式章節,詳細解釋。
JodaTimeFormatterRegistrar:joda-time註冊員
@deprecated as of 5.3,請使用Java標準的JSR 310日期時間代替
❝Tips:JodaDateTimeFormatAnnotationFormatterFactoryy也一樣在5.3版本被標記爲過期了
❞
jodaTime曾經乃是絕對的王者,拯救Java日期時間於水火,直到JSR 310體系的出現。同樣的那句話送給你:建議不要在(新)項目中以任何理由去使用jodaTime,而是和Date一樣完全放棄,使用JSR 310足矣。
❝說明:現在不建議再使用JodaTime並非卸磨殺驢,而是JSR 310就是jodaTime的作者/組織捐贈給Java的(你看那語法,多像!),所以現在叫功成身退更爲恰當
❞
由於jodaTime不像Date一樣有那麼重的歷史包袱(關鍵Date還是JDK內置的核心類),並且它和JSR 310一脈相承,因此在可預見的將來它將徹底告別Java舞臺,逐漸消亡。所以呢,我個人認爲,再去學習jodaTime(包括周邊)已再無必要,so此part就暫且略過嘍。
總結
作爲“失聯”很久的“第一篇”文章,本文沒有太多新內容,主要是對前兩篇收個尾,爲下一場做足鋪墊。本文雖爲補充性內容,但“含金量”依舊還是有的,希望對你有所幫助,敬請期待本系列接下來的精彩內容。
本文思考題
本文所屬專欄:Spring類型轉換,後臺回覆專欄名即可獲取全部內容,已被https://yourbatman.cn收錄。
看完了不一定懂,看懂了不一定會。來,文末3個思考題幫你覆盤:
-
@DateTimeFormat能標註在LocalDateTime上面嗎? -
JSR 310日期時間有哪些常見API? -
@DateTimeFormat註解如何在普通Java Bean上使用?
系列推薦
-
11. 春節禮物:Spring的Registrar倒排思想送給你 -
10. 原來是這麼玩的,@DateTimeFormat和@NumberFormat -
9. 細節見真章,Formatter註冊中心的設計很討巧
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源創計劃”,歡迎正在閱讀的你也加入,一起分享。