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