原來是這么玩的,@DateTimeFormat和@NumberFormat
前言你好,我是A哥(YourBatman)。
在本系列中間,用幾篇文章徹徹底底的把JDK日期時間系列深入講解了一遍,此系列有可能是把JDK日期時間講得最好最全的,強烈建議你前往看它一看。
本系列的上篇文章 對格式化器Formatter進行了剖析,Spring對日期時間、數(shù)字、錢幣等常用類型都內(nèi)置了相應的格式化器實現(xiàn),開發(fā)者可拿來就用。但是,這在使用上依舊有一定門檻:開發(fā)者需要知道對應API的細節(jié)。比如若需要對Date、LocalDate進行格式化操作的話,就需要分別了解處理他倆的正確API,這在使用上是存在一定“難度”的。
另外,在面向元數(shù)編程大行其道的今天,硬編碼往往是被吐槽甚至被拒絕的,聲明式才會受到歡迎。Spring自3.0起大量的引入了“更為時尚”的元數(shù)據(jù)編程支持,從而穩(wěn)固了其“江湖地位”。@DateTimeFormat和@NumberFormat兩個注解是Spring在類型轉(zhuǎn)換/格式化方面的元編程代表,本文一起來探討下。
本文提綱

版本約定
- Spring Framework:5.3.x
- Spring Boot:2.4.x
正文
據(jù)我了解,@DateTimeFormat是開發(fā)中出鏡率很高的一個注解,其作用見名之意很好理解:對日期時間格式化。但使用起來常常迷糊。比如:使用它還是com.fasterxml.jackson.annotation.JsonFormat注解呢?能否使用在Long類型上?能否使用在JSR 310日期時間類型上?
有這些問號其實蠻正常,但切忌囫圇吞棗,也不建議強記這些問題的答案,而是通過規(guī)律在原理層面去理解,不僅能更牢靠而且更輕松,這或許是學習編程最重要的必備技巧之一。
@DateTimeFormat和@NumberFormat
在類型轉(zhuǎn)換/格式化方面注解,Spring提供了兩個:
- @DateTimeFormat:將Field/方法參數(shù)格式化為日期/時間類型
- @NumberFormat:將Field/方法參數(shù)格式化為數(shù)字類型
值得關(guān)注的是:這里所說的日期/時間類型有很多,如最古老的java.util.Date類型、JSR 310的LocalDate類型甚至時間戳Long類型都可稱作日期時間類型;同樣的,數(shù)字類型也是個泛概念,如Number類型、百分數(shù)類型、錢幣類型也都屬此范疇。
- ❝話外音:這兩個注解能夠作用的類型很廣很廣❞分別看看這兩個注解定義,不可謂不簡單:
- @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
- public @interface DateTimeFormat {
- String style() default "SS";
- ISO iso() default ISO.NONE;
- String pattern() default "";
- }
- @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
- public @interface NumberFormat {
- Style style() default Style.DEFAULT;
- String pattern() default "";
- }
哥倆有兩個共通的點:
- 都支持標注在方法Method、字段Field、方法參數(shù)Parameter上
- 均支持靈活性極大的pattern屬性,此屬性支持Spring占位符書寫形式
- 對于pattern屬性你既可以用字面量寫死,也可以用形如${xxx.xxx.pattern}占位符形式這種更富彈性的寫法
咱們在使用這兩個注解時,最最最常用的是pattern這個屬性沒有之一,理由是它非常的靈活強大,能滿足各式各樣格式化需求。從這一點也側(cè)面看出,咱們在日期/時間、數(shù)字方面的格式化,并不遵循國際標準(如ISO),而普遍使用的“中國標準”。
由于這兩個注解幾乎所有同學都在Spring MVC上使用過,那么本文就先原理再示例。在知曉了其背后原理后再去使用,別有一番體會。
AnnotationFormatterFactory
說到格式化注解,就不得不提該工廠類,它是實現(xiàn)原理的核心所在。
字面含義:注解格式化工廠。用大白話解釋:該工廠用于為標注在Field字段上的注解創(chuàng)建對應的格式化器進而對值進行格式化處理。從這句話里可提取出幾個關(guān)鍵因素:

- 注解
- 字段Field
- 這里Field并不只表示java.lang.reflect.Field,像方法返回類型、參數(shù)類型都屬此范疇,下面使用示例會有所體會
- 格式化器Formatter
接口定義:
- public interface AnnotationFormatterFactory<A extends Annotation> {
- Set<Class<?>> getFieldTypes();
- Printer<?> getPrinter(A annotation, Class<?> fieldType);
- Parser<?> getParser(A annotation, Class<?> fieldType);
- }

雖然實現(xiàn)有5個之多,但其實只有兩類,也就是說面向使用者而言只需做兩種區(qū)分即可,分別對應上面所講的兩個注解。這里A哥把它繪制成圖所示:

紅色框框部分(以及其處理的Field類型)是咱們需要關(guān)注的重點,其它的留個印象即可。
關(guān)于日期時間類型,我在多篇文章里不再推薦使用java.util.Date類型(更不建議使用Long類型嘍),而是使用Java 8提供的JSR 310日期時間類型100%代替(包括代替joda-time)。但是呢,在當下階段java.util.Date類型依舊不可忽略(龐大存量市場,龐大“存量”程序員的存在),因此決定把DateTimeFormatAnnotationFormatterFactory依舊還是抬到桌面上來敘述敘述,但求做得更全面些。
❝關(guān)于JDK的日期時間我寫了一個非常全的系列,詳情點擊這里直達:日期時間系列,建議先行了解❞
DateTimeFormatAnnotationFormatterFactory
對應的格式化器API是:org.springframework.format.datetime.DateFormatter。
@since 3.2版本就已存在,專用于對java.util.Date體系 + @DateTimeFormat的支持:創(chuàng)建出相應的Printer/Parser。下面解讀其源碼:

①:該工廠類專為@DateTimeFormat注解服務(wù)②:借助Spring的StringValueResolver對占位符(若存在)做替換

這部分源碼告訴我們:@DateTimeFormat注解標注在如圖的這些類型上時才有效,才能被該工廠處理從而完成相應創(chuàng)建工作。
- ❝注意:除了Date和Calendar類型外,還有Long類型哦,請不要忽略了❞

核心處理邏輯也比較好理解:不管是Printer還是Parser最終均委托給DateFormatter去完成,而此API在本系列前面文章已做了詳細講解。電梯直達
值得注意的是:DateFormatter 只能 處理Date類型。換句話講getFormatter()方法的第二個參數(shù)fieldType在此方法里并沒有被使用,也就是說缺省情況下@DateTimeFormat注解并不能正常處理其標注在Calendar、Long類型的Case。若要得到支持,需自行重寫其getPrinter/getParser等方法。
使用示例
由于@DateTimeFormat可以標注在成員屬性、方法參數(shù)、方法(返回值)上,且當其標注在Date、Calendar、Long等類型上時方可交給本工廠類來處理生成相應的處理類,本文共用三個案例case進行覆蓋。
case1:成員屬性 + Date類型。輸入 + 輸出
準備一個標注有@DateTimeFormat注解的Field屬性,為Date類型
- @Data
- @AllArgsConstructor
- class Person {
- @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
- private Date birthday;
- }
書寫測試程序:
- @Test
- public void test1() throws Exception {
- AnnotationFormatterFactory annotationFormatterFactory = new DateTimeFormatAnnotationFormatterFactory();
- // 找到該field
- Field field = Person.class.getDeclaredField("birthday");
- DateTimeFormat annotation = field.getAnnotation(DateTimeFormat.class);
- Class<?> type = field.getType();
- // 輸出:
- System.out.println("輸出:Date -> String====================");
- Printer printer = annotationFormatterFactory.getPrinter(annotation, type);
- Person person = new Person(new Date());
- System.out.println(printer.print(person.getBirthday(), Locale.US));
- // 輸入:
- System.out.println("輸入:String -> Date====================");
- Parser parser = annotationFormatterFactory.getParser(annotation, type);
- Object output = parser.parse("2021-02-06 19:00:00", Locale.US);
- person = new Person((Date) output);
- System.out.println(person);
- }
運行程序,輸出:
- 輸出:Date -> String====================
- 2021-02-06 22:21:56
- 輸入:String -> Date====================
- Person(birthday=Sat Feb 06 19:00:00 CST 2021)
完美。
case2:方法參數(shù) + Calendar。輸入
- @Test
- public void test2() throws NoSuchMethodException, ParseException {
- AnnotationFormatterFactory annotationFormatterFactory = new DateTimeFormatAnnotationFormatterFactory();
- // 拿到方法入?yún)?nbsp;
- Method method = this.getClass().getDeclaredMethod("method", Calendar.class);
- Parameter parameter = method.getParameters()[0];
- DateTimeFormat annotation = parameter.getAnnotation(DateTimeFormat.class);
- Class<?> type = parameter.getType();
- // 輸入:
- System.out.println("輸入:String -> Calendar====================");
- Parser parser = annotationFormatterFactory.getParser(annotation, type);
- Object output = parser.parse("2021-02-06 19:00:00", Locale.US);
- // 給該方法傳入“轉(zhuǎn)換好的”參數(shù),表示輸入
- method((Calendar) output);
- }
- public void method(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Calendar calendar) {
- System.out.println(calendar);
- System.out.println(calendar.getTime());
- }
運行程序,報錯:
- 輸入:String -> Calendar====================
- java.lang.ClassCastException: java.util.Date cannot be cast to java.util.Calendar
- ...
通過文上的闡述,這個錯誤是在意料之中的。下面通過自定義一個增強實現(xiàn)來達到目的:
- class MyDateTimeFormatAnnotationFormatterFactory extends DateTimeFormatAnnotationFormatterFactory {
- @Override
- public Parser<?> getParser(DateTimeFormat annotation, Class<?> fieldType) {
- if (fieldType.isAssignableFrom(Calendar.class)) {
- return new Parser<Calendar>() {
- @Override
- public Calendar parse(String text, Locale locale) throws ParseException {
- // 先翻譯為Date
- Formatter<Date> formatter = getFormatter(annotation, fieldType);
- Date date = formatter.parse(text, locale);
- // 再翻譯為Calendar
- Calendar calendar = Calendar.getInstance(TimeZone.getDefault());
- calendar.setTime(date);
- return calendar;
- }
- };
- }
- return super.getParser(annotation, fieldType);
- }
- }
將測試程序中的工廠類換為自定義的增強實現(xiàn):
- AnnotationFormatterFactory annotationFormatterFactory = new MyDateTimeFormatAnnotationFormatterFactory();
再次運行程序,輸出:
- 輸入:String -> Calendar====================
- java.util.GregorianCalendar[time=1612609200000, ...
- Sat Feb 06 19:00:00 CST 2021
完美。
case3:方法返回值 + Long。輸出 建議自行實現(xiàn),略
❝時間戳被經(jīng)常用來做時間傳遞,那么傳輸中的Long類型如何被自動封裝 為Date類型(輸入)呢?動動手鞏固下吧~❞
Jsr310DateTimeFormatAnnotationFormatterFactory
對應的格式化器API是:Spring的org.springframework.format.datetime.standard.DateTimeFormatterFactory以及JDK的java.time.format.DateTimeFormatter。
@since 4.0。JSR 310時間是伴隨著Java 8的出現(xiàn)而出現(xiàn)的,Spring自4.0 開始支持 Java 8,自5.0至少基于 Java 8,因此此類since 4.0就不好奇嘍。
從類名能讀出它用于處理JSR 310日期時間。下面解讀一下它的部分源碼,透過現(xiàn)象看其本質(zhì):

①:該工廠專為@DateTimeFormat注解服務(wù)②:借助Spring的StringValueResolver對占位符(若存在)做替換

@DateTimeFormat注解標注在這些類型上時,就會交給此工廠類來負責其格式化器的創(chuàng)建工作。

①:得到一個JDK的java.time.format.DateTimeFormatter,由它負責將 日期/時間 -> String類型的格式化。由于JSR 310日期/時間的格式化JDK自己實現(xiàn)得已經(jīng)非常完善,Spring只需要將它整合進來就成。但是呢,DateTimeFormatter它是線程安全的無法同時設(shè)置iso、pattern等個性化參數(shù),于是Spring就造了DateTimeFormatterFactory工廠類,用它用來抹平使用上的差異,達到(和java.util.Date)一致的使用體驗。當然嘍,這個知識點屬于上篇文章的內(nèi)容,欲回顧詳情可點擊這里電梯直達。
回到本處,getFormatter()方法得到格式化器實例是關(guān)鍵,具體代碼如下:

使用Spring的工廠類DateTimeFormatterFactory構(gòu)建出一個JSR 310的日期時間格式化器DateTimeFormatter來處理。有了上篇文章的鋪墊,相信這個邏輯無需再多費一言解釋了哈。
②:這一大塊是對LocalXXX(含LocalDate/Time)標準格式化器做的特殊處理:將ISO_XXX格式化模版適配為更加適用的ISO_Local_XXX格式化模版,更加精確。③:TemporalAccessorPrinter它就是個Printer

強調(diào):別看這個特性很小,但非常有用,有四兩撥千斤的功效。因為它和我們業(yè)務(wù)系統(tǒng)息息相關(guān),掌握這個點可輕松實現(xiàn)事半功倍的效果,別人加班你加薪。關(guān)于此知識點的應用,A哥覺得值得專門寫篇文章來描述,敬請期待下文。
接下來再看看getParser()部分的實現(xiàn):

①:TemporalAccessorParser是個Parser

到此,整個Jsr310DateTimeFormatAnnotationFormatterFactory的源碼就分析完了,總結(jié)一下:
- 此工廠專為標注在JSR 310日期/時間類型的@DateTimeFormat注解服務(wù)
- 底層格式化器雙向均使用的是和上下文相關(guān)的的DateTimeFormatter,具有高度可定制化的特性。此特性雖小卻有四兩撥千斤的效果,后面會專文給出使用場景
- @DateTimeFormat注解的style和pattern屬性都是支持占位符形式書寫的,更富彈性
使用示例
它不像DateTimeFormatAnnotationFormatterFactory只提供了部分支持,而是提供了全部功能,感受一下。
case1:成員屬性 + LocalDate類型。輸入 + 輸出
- @Data
- @AllArgsConstructor
- class Father {
- @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
- private LocalDate birthday;
- }
測試代碼:
- @Test
- public void test4() throws NoSuchFieldException, ParseException {
- AnnotationFormatterFactory annotationFormatterFactory = new Jsr310DateTimeFormatAnnotationFormatterFactory();
- // 找到該field
- Field field = Father.class.getDeclaredField("birthday");
- DateTimeFormat annotation = field.getAnnotation(DateTimeFormat.class);
- Class<?> type = field.getType();
- // 輸出:
- System.out.println("輸出:LocalDate -> String====================");
- Printer printer = annotationFormatterFactory.getPrinter(annotation, type);
- Father father = new Father(LocalDate.now());
- System.out.println(printer.print(father.getBirthday(), Locale.US));
- // 輸入:
- System.out.println("輸入:String -> Date====================");
- Parser parser = annotationFormatterFactory.getParser(annotation, type);
- Object output = parser.parse("2021-02-07", Locale.US);
- father = new Father((LocalDate) output);
- System.out.println(father);
- }
運行程序,輸出:
- 輸出:LocalDate -> String====================
- 2021-02-07
- 輸入:String -> Date====================
- Father(birthday=2021-02-07)
完美。
case2:方法參數(shù) + LocalDate類型。輸入
- @Test
- public void test5() throws ParseException, NoSuchMethodException {
- AnnotationFormatterFactory annotationFormatterFactory = new Jsr310DateTimeFormatAnnotationFormatterFactory();
- // 拿到方法入?yún)?nbsp;
- Method method = this.getClass().getDeclaredMethod("methodJSR310", LocalDate.class);
- Parameter parameter = method.getParameters()[0];
- DateTimeFormat annotation = parameter.getAnnotation(DateTimeFormat.class);
- Class<?> type = parameter.getType();
- // 輸入:
- System.out.println("輸入:String -> LocalDate====================");
- Parser parser = annotationFormatterFactory.getParser(annotation, type);
- Object output = parser.parse("2021-02-06", Locale.US);
- // 給該方法傳入“轉(zhuǎn)換好的”參數(shù),表示輸入
- methodJSR310((LocalDate) output);
- }
- public void methodJSR310(@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate localDate) {
- System.out.println(localDate);
- }
運行程序,輸出:
- 輸入:String -> LocalDate====================
- 2021-02-06
case3:方法返回值 + LocalDate類型。輸入
- @Test
- public void test6() throws NoSuchMethodException {
- AnnotationFormatterFactory annotationFormatterFactory = new Jsr310DateTimeFormatAnnotationFormatterFactory();
- // 拿到方法返回值類型
- Method method = this.getClass().getDeclaredMethod("method1JSR310");
- DateTimeFormat annotation = method.getAnnotation(DateTimeFormat.class);
- Class<?> type = method.getReturnType();
- // 輸出:
- System.out.println("輸出:LocalDate -> 時間格式的String====================");
- Printer printer = annotationFormatterFactory.getPrinter(annotation, type);
- LocalDate returnValue = method1JSR310();
- System.out.println(printer.print(returnValue, Locale.US));
- }
- @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
- public LocalDate method1JSR310() {
- return LocalDate.now();
- }
完美。
NumberFormatAnnotationFormatterFactory
對應的格式化器API是:org.springframework.format.number.AbstractNumberFormatter的三個子類

@since 3.0。直奔主題,源碼嘍幾眼:

有了上面的“經(jīng)驗”,此part不用解釋了吧。
①:@NumberFormat可以標注在Number的子類型上,并生成對應的格式化器處理。
底層實現(xiàn):實際的格式化動作Printer/Parser如下圖所示,全權(quán)委托給前面已介紹過的格式化器來完成,就不做過多介紹啦。有知識盲區(qū)的可乘坐電梯前往本系列前面文章查看~

使用示例
@NumberFormat支持標注在多種類型上,如小數(shù)、百分數(shù)、錢幣等等,由于文上已做好了鋪墊,所以這里只給出個簡單使用案例即可,舉一反三。
- @Test
- public void test2() throws NoSuchMethodException, ParseException {
- AnnotationFormatterFactory annotationFormatterFactory = new NumberFormatAnnotationFormatterFactory();
- // 獲取待處理的目標類型(方法參數(shù)、字段屬性、方法返回值等等)
- Method method1 = this.getClass().getMethod("method2", double.class);
- Parameter parameter = method1.getParameters()[0];
- NumberFormat annotation = parameter.getAnnotation(NumberFormat.class);
- Class<?> fieldType = parameter.getType();
- // 1、根據(jù)注解和field類型生成一個解析器,完成String -> LocalDateTime
- Parser parser = annotationFormatterFactory.getParser(annotation, fieldType);
- // 2、模擬轉(zhuǎn)換動作,并輸出結(jié)果
- Object result = parser.parse("11%", Locale.US);
- System.out.println(result.getClass());
- System.out.println(result);
- }
- public void method2(@NumberFormat(style = NumberFormat.Style.PERCENT) double d) {
- }
運行程序,輸出:
- class java.math.BigDecimal
- 0.11
完美的將11%這種百分數(shù)數(shù)字轉(zhuǎn)換為BigDecimal了。至于為何是BigDecimal類型而不是double,那都在PercentStyleFormatter里了。
總結(jié)
這兩個注解更像是高層抽象:模糊掉開發(fā)者的使用成本,能夠達到的效果是:
- @DateTimeFormat:日期時間類型的格式化,找我就夠了
- @NumberFormat:數(shù)字類型的格式化,找我就夠了
這兩個由于過于常用Spring內(nèi)置提供了,若你有特殊需求,Spring也提供了鉤子,可以自定義注解 + 擴展AnnotationFormatterFactory接口來實現(xiàn)。注解 + 工廠類組合在一起像是一個分發(fā)器,模糊掉類型上的差異,讓使用者有統(tǒng)一感受。
有了本系列前面知識的鋪墊,本文一路讀下來毫不費力,底層基礎(chǔ)決定上層建筑。這些都是在Spring MVC場景下使用的這些注解的底層原理,本系列對其抽絲剝繭后,那些使用上的問題自當無師自通,迎刃而解。
當然嘍,在實際應用中不可能像本例一樣這樣編碼實現(xiàn),開發(fā)者應該只需知道注解使用在哪即可。既然要方便,那就需要整合。下篇文章將繼續(xù)了解Spring是如何將此功能整合進注冊中心,大大簡化使用方式的。
本文思考題
本文所屬專欄:Spring類型轉(zhuǎn)換,后臺回復專欄名即可獲取全部內(nèi)容,已被https://www.yourbatman.cn收錄。
看完了不一定懂,看懂了不一定會。來,文末3個思考題幫你復盤:
傳入Long類型時間戳,如何能支持自動封裝到Date類型?
@DateTimeFormat一般用于Controller層?那么它能用在Service層嗎?如何做?
為什么并不建議在Service/Dao層使用@DateTimeFormat等注解呢?