原来是这么玩的,@DateTimeFormat和@NumberFormat

开发 前端
在本系列中间,用几篇文章彻彻底底的把JDK日期时间系列深入讲解了一遍,此系列有可能是把JDK日期时间讲得最好最全的,强烈建议你前往看它一看。

[[381166]]

 前言你好,我是A哥(YourBatman)。

在本系列中间,用几篇文章彻彻底底的把JDK日期时间系列深入讲解了一遍,此系列有可能是把JDK日期时间讲得最好最全的,强烈建议你前往看它一看。

本系列的上篇文章 对格式化器Formatter进行了剖析,Spring对日期时间、数字、钱币等常用类型都内置了相应的格式化器实现,开发者可拿来就用。但是,这在使用上依旧有一定门槛:开发者需要知道对应API的细节。比如若需要对Date、LocalDate进行格式化操作的话,就需要分别了解处理他俩的正确API,这在使用上是存在一定“难度”的。

另外,在面向元数编程大行其道的今天,硬编码往往是被吐槽甚至被拒绝的,声明式才会受到欢迎。Spring自3.0起大量的引入了“更为时尚”的元数据编程支持,从而稳固了其“江湖地位”。@DateTimeFormat和@NumberFormat两个注解是Spring在类型转换/格式化方面的元编程代表,本文一起来探讨下。

本文提纲


版本约定

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

正文

据我了解,@DateTimeFormat是开发中出镜率很高的一个注解,其作用见名之意很好理解:对日期时间格式化。但使用起来常常迷糊。比如:使用它还是com.fasterxml.jackson.annotation.JsonFormat注解呢?能否使用在Long类型上?能否使用在JSR 310日期时间类型上?

有这些问号其实蛮正常,但切忌囫囵吞枣,也不建议强记这些问题的答案,而是通过规律在原理层面去理解,不仅能更牢靠而且更轻松,这或许是学习编程最重要的必备技巧之一。

@DateTimeFormat和@NumberFormat

在类型转换/格式化方面注解,Spring提供了两个:

  • @DateTimeFormat:将Field/方法参数格式化为日期/时间类型
  • @NumberFormat:将Field/方法参数格式化为数字类型

值得关注的是:这里所说的日期/时间类型有很多,如最古老的java.util.Date类型、JSR 310的LocalDate类型甚至时间戳Long类型都可称作日期时间类型;同样的,数字类型也是个泛概念,如Number类型、百分数类型、钱币类型也都属此范畴。

  • ❝话外音:这两个注解能够作用的类型很广很广❞分别看看这两个注解定义,不可谓不简单:
  1. @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) 
  2. public @interface DateTimeFormat { 
  3.  
  4.  String style() default "SS"
  5.  ISO iso() default ISO.NONE; 
  6.  String pattern() default ""
  7.   

  1. @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) 
  2. public @interface NumberFormat { 
  3.  
  4.  Style style() default Style.DEFAULT
  5.  String pattern() default ""

哥俩有两个共通的点:

  1. 都支持标注在方法Method、字段Field、方法参数Parameter上
  2. 均支持灵活性极大的pattern属性,此属性支持Spring占位符书写形式
  3. 对于pattern属性你既可以用字面量写死,也可以用形如${xxx.xxx.pattern}占位符形式这种更富弹性的写法

咱们在使用这两个注解时,最最最常用的是pattern这个属性没有之一,理由是它非常的灵活强大,能满足各式各样格式化需求。从这一点也侧面看出,咱们在日期/时间、数字方面的格式化,并不遵循国际标准(如ISO),而普遍使用的“中国标准”。

由于这两个注解几乎所有同学都在Spring MVC上使用过,那么本文就先原理再示例。在知晓了其背后原理后再去使用,别有一番体会。

AnnotationFormatterFactory

说到格式化注解,就不得不提该工厂类,它是实现原理的核心所在。

字面含义:注解格式化工厂。用大白话解释:该工厂用于为标注在Field字段上的注解创建对应的格式化器进而对值进行格式化处理。从这句话里可提取出几个关键因素:


  1. 注解
  2. 字段Field
  3. 这里Field并不只表示java.lang.reflect.Field,像方法返回类型、参数类型都属此范畴,下面使用示例会有所体会
  4. 格式化器Formatter

接口定义:

 

  1. public interface AnnotationFormatterFactory<A extends Annotation> { 
  2.   
  3.  Set<Class<?>> getFieldTypes(); 
  4.   
  5.  Printer<?> getPrinter(A annotation, Class<?> fieldType); 
  6.  Parser<?> getParser(A annotation, Class<?> fieldType); 

 

接口共定义了三个方法:

此接口Spring内建有如下实现:

 


虽然实现有5个之多,但其实只有两类,也就是说面向使用者而言只需做两种区分即可,分别对应上面所讲的两个注解。这里A哥把它绘制成图所示:


红色框框部分(以及其处理的Field类型)是咱们需要关注的重点,其它的留个印象即可。

关于日期时间类型,我在多篇文章里不再推荐使用java.util.Date类型(更不建议使用Long类型喽),而是使用Java 8提供的JSR 310日期时间类型100%代替(包括代替joda-time)。但是呢,在当下阶段java.util.Date类型依旧不可忽略(庞大存量市场,庞大“存量”程序员的存在),因此决定把DateTimeFormatAnnotationFormatterFactory依旧还是抬到桌面上来叙述叙述,但求做得更全面些。

❝关于JDK的日期时间我写了一个非常全的系列,详情点击这里直达:日期时间系列,建议先行了解❞

DateTimeFormatAnnotationFormatterFactory

对应的格式化器API是:org.springframework.format.datetime.DateFormatter。

@since 3.2版本就已存在,专用于对java.util.Date体系 + @DateTimeFormat的支持:创建出相应的Printer/Parser。下面解读其源码:


①:该工厂类专为@DateTimeFormat注解服务②:借助Spring的StringValueResolver对占位符(若存在)做替换


这部分源码告诉我们:@DateTimeFormat注解标注在如图的这些类型上时才有效,才能被该工厂处理从而完成相应创建工作。

  • ❝注意:除了Date和Calendar类型外,还有Long类型哦,请不要忽略了❞

核心处理逻辑也比较好理解:不管是Printer还是Parser最终均委托给DateFormatter去完成,而此API在本系列前面文章已做了详细讲解。电梯直达

值得注意的是:DateFormatter 只能 处理Date类型。换句话讲getFormatter()方法的第二个参数fieldType在此方法里并没有被使用,也就是说缺省情况下@DateTimeFormat注解并不能正常处理其标注在Calendar、Long类型的Case。若要得到支持,需自行重写其getPrinter/getParser等方法。

使用示例

由于@DateTimeFormat可以标注在成员属性、方法参数、方法(返回值)上,且当其标注在Date、Calendar、Long等类型上时方可交给本工厂类来处理生成相应的处理类,本文共用三个案例case进行覆盖。

case1:成员属性 + Date类型。输入 + 输出

准备一个标注有@DateTimeFormat注解的Field属性,为Date类型

  1. @Data 
  2. @AllArgsConstructor 
  3. class Person { 
  4.  
  5.     @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss"
  6.     private Date birthday; 
  7.      

书写测试程序:

  1. @Test 
  2. public void test1() throws Exception { 
  3.     AnnotationFormatterFactory annotationFormatterFactory = new DateTimeFormatAnnotationFormatterFactory(); 
  4.  
  5.     // 找到该field 
  6.     Field field = Person.class.getDeclaredField("birthday"); 
  7.     DateTimeFormat annotation = field.getAnnotation(DateTimeFormat.class); 
  8.     Class<?> type = field.getType(); 
  9.  
  10.     // 输出: 
  11.     System.out.println("输出:Date -> String===================="); 
  12.     Printer printer = annotationFormatterFactory.getPrinter(annotation, type); 
  13.     Person person = new Person(new Date()); 
  14.     System.out.println(printer.print(person.getBirthday(), Locale.US)); 
  15.  
  16.     // 输入: 
  17.     System.out.println("输入:String -> Date===================="); 
  18.     Parser parser = annotationFormatterFactory.getParser(annotation, type); 
  19.     Object output = parser.parse("2021-02-06 19:00:00", Locale.US); 
  20.     person = new Person((Dateoutput); 
  21.     System.out.println(person); 

运行程序,输出:

  1. 输出:Date -> String==================== 
  2. 2021-02-06 22:21:56 
  3. 输入:String -> Date==================== 
  4. Person(birthday=Sat Feb 06 19:00:00 CST 2021) 

完美。

case2:方法参数 + Calendar。输入

  1. @Test 
  2. public void test2() throws NoSuchMethodException, ParseException { 
  3.     AnnotationFormatterFactory annotationFormatterFactory = new DateTimeFormatAnnotationFormatterFactory(); 
  4.  
  5.     // 拿到方法入参 
  6.     Method method = this.getClass().getDeclaredMethod("method", Calendar.class); 
  7.     Parameter parameter = method.getParameters()[0]; 
  8.     DateTimeFormat annotation = parameter.getAnnotation(DateTimeFormat.class); 
  9.     Class<?> type = parameter.getType(); 
  10.  
  11.     // 输入: 
  12.     System.out.println("输入:String -> Calendar===================="); 
  13.     Parser parser = annotationFormatterFactory.getParser(annotation, type); 
  14.     Object output = parser.parse("2021-02-06 19:00:00", Locale.US); 
  15.  // 给该方法传入“转换好的”参数,表示输入 
  16.     method((Calendar) output); 
  17.  
  18. public void method(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Calendar calendar) { 
  19.     System.out.println(calendar); 
  20.     System.out.println(calendar.getTime()); 

运行程序,报错:

  1. 输入:String -> Calendar==================== 
  2.  
  3. java.lang.ClassCastException: java.util.Date cannot be cast to java.util.Calendar 
  4.  ... 

通过文上的阐述,这个错误是在意料之中的。下面通过自定义一个增强实现来达到目的:

  1. class MyDateTimeFormatAnnotationFormatterFactory extends DateTimeFormatAnnotationFormatterFactory { 
  2.  
  3.     @Override 
  4.     public Parser<?> getParser(DateTimeFormat annotation, Class<?> fieldType) { 
  5.         if (fieldType.isAssignableFrom(Calendar.class)) { 
  6.             return new Parser<Calendar>() { 
  7.                 @Override 
  8.                 public Calendar parse(String text, Locale locale) throws ParseException { 
  9.                     // 先翻译为Date 
  10.                     Formatter<Date> formatter = getFormatter(annotation, fieldType); 
  11.                     Date date = formatter.parse(text, locale); 
  12.  
  13.                     // 再翻译为Calendar 
  14.                     Calendar calendar = Calendar.getInstance(TimeZone.getDefault()); 
  15.                     calendar.setTime(date); 
  16.                     return calendar; 
  17.                 } 
  18.  
  19.             }; 
  20.         } 
  21.         return super.getParser(annotation, fieldType); 
  22.     } 

将测试程序中的工厂类换为自定义的增强实现:

  1. AnnotationFormatterFactory annotationFormatterFactory = new MyDateTimeFormatAnnotationFormatterFactory(); 

再次运行程序,输出:

  1. 输入:String -> Calendar==================== 
  2. java.util.GregorianCalendar[time=1612609200000, ... 
  3. Sat Feb 06 19:00:00 CST 2021 

完美。

case3:方法返回值 + Long。输出 建议自行实现,略

❝时间戳被经常用来做时间传递,那么传输中的Long类型如何被自动封装 为Date类型(输入)呢?动动手巩固下吧~❞

Jsr310DateTimeFormatAnnotationFormatterFactory

对应的格式化器API是:Spring的org.springframework.format.datetime.standard.DateTimeFormatterFactory以及JDK的java.time.format.DateTimeFormatter。

@since 4.0。JSR 310时间是伴随着Java 8的出现而出现的,Spring自4.0 开始支持 Java 8,自5.0至少基于 Java 8,因此此类since 4.0就不好奇喽。

从类名能读出它用于处理JSR 310日期时间。下面解读一下它的部分源码,透过现象看其本质:


①:该工厂专为@DateTimeFormat注解服务②:借助Spring的StringValueResolver对占位符(若存在)做替换


@DateTimeFormat注解标注在这些类型上时,就会交给此工厂类来负责其格式化器的创建工作。


①:得到一个JDK的java.time.format.DateTimeFormatter,由它负责将 日期/时间 -> String类型的格式化。由于JSR 310日期/时间的格式化JDK自己实现得已经非常完善,Spring只需要将它整合进来就成。但是呢,DateTimeFormatter它是线程安全的无法同时设置iso、pattern等个性化参数,于是Spring就造了DateTimeFormatterFactory工厂类,用它用来抹平使用上的差异,达到(和java.util.Date)一致的使用体验。当然喽,这个知识点属于上篇文章的内容,欲回顾详情可点击这里电梯直达。

回到本处,getFormatter()方法得到格式化器实例是关键,具体代码如下:


使用Spring的工厂类DateTimeFormatterFactory构建出一个JSR 310的日期时间格式化器DateTimeFormatter来处理。有了上篇文章的铺垫,相信这个逻辑无需再多费一言解释了哈。

②:这一大块是对LocalXXX(含LocalDate/Time)标准格式化器做的特殊处理:将ISO_XXX格式化模版适配为更加适用的ISO_Local_XXX格式化模版,更加精确。③:TemporalAccessorPrinter它就是个Printer,实际的格式化器依旧是DateTimeFormatter,只不过它的作用是兼容到了上下文级别(和当前线程绑定)的格式化器,从而有能力用到上下文级别的格式化参数,具有更好的可定制性,如下图所示(源码来自TemporalAccessorPrinter):


强调:别看这个特性很小,但非常有用,有四两拨千斤的功效。因为它和我们业务系统息息相关,掌握这个点可轻松实现事半功倍的效果,别人加班你加薪。关于此知识点的应用,A哥觉得值得专门写篇文章来描述,敬请期待下文。

接下来再看看getParser()部分的实现:


①:TemporalAccessorParser是个Parser,同样的也是利用了具有Context上下文特性的DateTimeFormatter来完成String -> TemporalAccessor工作的。熟悉这个方向的转换逻辑的同学就知道,因为都是静态方法调用,所以必须用“枚举”的方式一一处理,截图如下(源码来自TemporalAccessorParser):


到此,整个Jsr310DateTimeFormatAnnotationFormatterFactory的源码就分析完了,总结一下:

  1. 此工厂专为标注在JSR 310日期/时间类型的@DateTimeFormat注解服务
  2. 底层格式化器双向均使用的是和上下文相关的的DateTimeFormatter,具有高度可定制化的特性。此特性虽小却有四两拨千斤的效果,后面会专文给出使用场景
  3. @DateTimeFormat注解的style和pattern属性都是支持占位符形式书写的,更富弹性

使用示例

它不像DateTimeFormatAnnotationFormatterFactory只提供了部分支持,而是提供了全部功能,感受一下。

case1:成员属性 + LocalDate类型。输入 + 输出

  1. @Data 
  2. @AllArgsConstructor 
  3. class Father { 
  4.     @DateTimeFormat(iso = DateTimeFormat.ISO.DATE
  5.     private LocalDate birthday; 

测试代码:

  1. @Test 
  2. public void test4() throws NoSuchFieldException, ParseException { 
  3.     AnnotationFormatterFactory annotationFormatterFactory = new Jsr310DateTimeFormatAnnotationFormatterFactory(); 
  4.  
  5.     // 找到该field 
  6.     Field field = Father.class.getDeclaredField("birthday"); 
  7.     DateTimeFormat annotation = field.getAnnotation(DateTimeFormat.class); 
  8.     Class<?> type = field.getType(); 
  9.  
  10.     // 输出: 
  11.     System.out.println("输出:LocalDate -> String===================="); 
  12.     Printer printer = annotationFormatterFactory.getPrinter(annotation, type); 
  13.     Father father = new Father(LocalDate.now()); 
  14.     System.out.println(printer.print(father.getBirthday(), Locale.US)); 
  15.  
  16.     // 输入: 
  17.     System.out.println("输入:String -> Date===================="); 
  18.     Parser parser = annotationFormatterFactory.getParser(annotation, type); 
  19.     Object output = parser.parse("2021-02-07", Locale.US); 
  20.     father = new Father((LocalDate) output); 
  21.     System.out.println(father); 

运行程序,输出:

  1. 输出:LocalDate -> String==================== 
  2. 2021-02-07 
  3. 输入:String -> Date==================== 
  4. Father(birthday=2021-02-07) 

完美。

case2:方法参数 + LocalDate类型。输入

  1. @Test 
  2. public void test5() throws ParseException, NoSuchMethodException { 
  3.     AnnotationFormatterFactory annotationFormatterFactory = new Jsr310DateTimeFormatAnnotationFormatterFactory(); 
  4.  
  5.     // 拿到方法入参 
  6.     Method method = this.getClass().getDeclaredMethod("methodJSR310", LocalDate.class); 
  7.     Parameter parameter = method.getParameters()[0]; 
  8.     DateTimeFormat annotation = parameter.getAnnotation(DateTimeFormat.class); 
  9.     Class<?> type = parameter.getType(); 
  10.  
  11.  
  12.     // 输入: 
  13.     System.out.println("输入:String -> LocalDate===================="); 
  14.     Parser parser = annotationFormatterFactory.getParser(annotation, type); 
  15.     Object output = parser.parse("2021-02-06", Locale.US); 
  16.     // 给该方法传入“转换好的”参数,表示输入 
  17.     methodJSR310((LocalDate) output); 
  18.  
  19. public void methodJSR310(@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate localDate) { 
  20.     System.out.println(localDate); 

运行程序,输出:

  1. 输入:String -> LocalDate==================== 
  2. 2021-02-06 

case3:方法返回值 + LocalDate类型。输入

  1. @Test 
  2. public void test6() throws NoSuchMethodException { 
  3.     AnnotationFormatterFactory annotationFormatterFactory = new Jsr310DateTimeFormatAnnotationFormatterFactory(); 
  4.  
  5.     // 拿到方法返回值类型 
  6.     Method method = this.getClass().getDeclaredMethod("method1JSR310"); 
  7.     DateTimeFormat annotation = method.getAnnotation(DateTimeFormat.class); 
  8.     Class<?> type = method.getReturnType(); 
  9.  
  10.  
  11.     // 输出: 
  12.     System.out.println("输出:LocalDate -> 时间格式的String===================="); 
  13.     Printer printer = annotationFormatterFactory.getPrinter(annotation, type); 
  14.  
  15.     LocalDate returnValue = method1JSR310(); 
  16.     System.out.println(printer.print(returnValue, Locale.US)); 
  17.  
  18. @DateTimeFormat(iso = DateTimeFormat.ISO.DATE
  19. public LocalDate method1JSR310() { 
  20.     return LocalDate.now(); 

完美。

NumberFormatAnnotationFormatterFactory

对应的格式化器API是:org.springframework.format.number.AbstractNumberFormatter的三个子类


@since 3.0。直奔主题,源码喽几眼:


有了上面的“经验”,此part不用解释了吧。

①:@NumberFormat可以标注在Number的子类型上,并生成对应的格式化器处理。

底层实现:实际的格式化动作Printer/Parser如下图所示,全权委托给前面已介绍过的格式化器来完成,就不做过多介绍啦。有知识盲区的可乘坐电梯前往本系列前面文章查看~


使用示例

@NumberFormat支持标注在多种类型上,如小数、百分数、钱币等等,由于文上已做好了铺垫,所以这里只给出个简单使用案例即可,举一反三。

  1. @Test 
  2. public void test2() throws NoSuchMethodException, ParseException { 
  3.     AnnotationFormatterFactory annotationFormatterFactory = new NumberFormatAnnotationFormatterFactory(); 
  4.  
  5.     // 获取待处理的目标类型(方法参数、字段属性、方法返回值等等) 
  6.     Method method1 = this.getClass().getMethod("method2"double.class); 
  7.     Parameter parameter = method1.getParameters()[0]; 
  8.     NumberFormat annotation = parameter.getAnnotation(NumberFormat.class); 
  9.     Class<?> fieldType = parameter.getType(); 
  10.  
  11.     // 1、根据注解和field类型生成一个解析器,完成String -> LocalDateTime 
  12.     Parser parser = annotationFormatterFactory.getParser(annotation, fieldType); 
  13.     // 2、模拟转换动作,并输出结果 
  14.     Object result = parser.parse("11%", Locale.US); 
  15.     System.out.println(result.getClass()); 
  16.     System.out.println(result); 
  17.  
  18.  
  19. public void method2(@NumberFormat(style = NumberFormat.Style.PERCENT) double d) { 

运行程序,输出:

  1. class java.math.BigDecimal 
  2. 0.11 

完美的将11%这种百分数数字转换为BigDecimal了。至于为何是BigDecimal类型而不是double,那都在PercentStyleFormatter里了。

总结

这两个注解更像是高层抽象:模糊掉开发者的使用成本,能够达到的效果是:

  • @DateTimeFormat:日期时间类型的格式化,找我就够了
  • @NumberFormat:数字类型的格式化,找我就够了

这两个由于过于常用Spring内置提供了,若你有特殊需求,Spring也提供了钩子,可以自定义注解 + 扩展AnnotationFormatterFactory接口来实现。注解 + 工厂类组合在一起像是一个分发器,模糊掉类型上的差异,让使用者有统一感受。

有了本系列前面知识的铺垫,本文一路读下来毫不费力,底层基础决定上层建筑。这些都是在Spring MVC场景下使用的这些注解的底层原理,本系列对其抽丝剥茧后,那些使用上的问题自当无师自通,迎刃而解。

当然喽,在实际应用中不可能像本例一样这样编码实现,开发者应该只需知道注解使用在哪即可。既然要方便,那就需要整合。下篇文章将继续了解Spring是如何将此功能整合进注册中心,大大简化使用方式的。

本文思考题

本文所属专栏:Spring类型转换,后台回复专栏名即可获取全部内容,已被https://www.yourbatman.cn收录。

看完了不一定懂,看懂了不一定会。来,文末3个思考题帮你复盘:

传入Long类型时间戳,如何能支持自动封装到Date类型?

@DateTimeFormat一般用于Controller层?那么它能用在Service层吗?如何做?

为什么并不建议在Service/Dao层使用@DateTimeFormat等注解呢?

 

责任编辑:姜华 来源: BAT的乌托邦
相关推荐

2014-03-31 14:59:08

大数据

2020-02-23 15:55:00

疫情AI人工智能

2022-01-14 14:19:38

ReactTS前端

2022-12-06 17:30:04

2021-07-29 16:56:59

微信腾讯注册

2020-06-30 08:12:32

VMwareKVMDocker

2022-08-15 08:01:00

三色标记JVM算法

2017-06-06 15:13:07

2018-10-28 17:54:00

分布式事务数据

2022-12-14 07:32:40

InnoDBMySQL引擎

2023-04-09 23:25:30

Java注解元注解

2023-09-11 13:27:00

数据训练

2022-01-04 08:00:48

前端技术Esbuild

2017-11-27 12:24:02

命令行代码指令

2023-05-22 15:58:11

2022-05-05 08:55:12

工业物联网IIoT

2024-02-06 09:30:25

Figma矩形矩形物理属性

2017-08-16 10:55:07

QQ斗图

2023-02-15 08:17:38

2018-04-02 15:13:21

网络
点赞
收藏

51CTO技术栈公众号