查漏补缺@DateTimeFormat到底干了些啥

开发 前端
Spring中的转换器、格式化器是整个Spring技术栈体系中非常重要的一份子,是众多高级特性的基础支撑。

[[385892]]

本文转载自微信公众号「BAT的乌托邦」,作者YourBatman。转载本文请联系BAT的乌托邦公众号。

本文提纲

版本约定

  • 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的请求实体形如这样:

  1. @Data 
  2. public class Person{ 
  3.   
  4.  @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss"
  5.  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基础上做二次开发的小伙伴才会用到,用个简单示例了解一下用法:

  1. @Test 
  2. public void test1() { 
  3.     FormattingConversionService conversionService = new FormattingConversionService(); 
  4.     // 注册员负责添加格式化器以支持Date系列的转换 
  5.     new DateTimeFormatterRegistrar().registerFormatters((FormatterRegistry) conversionService); 
  6.  
  7.     // 1、普通使用(API方式) 
  8.     LocalDateTime now = LocalDateTime.now(); 
  9.     System.out.println("当前时间:" + now); 
  10.     System.out.println("LocalDateTime转为LocalDate:" + conversionService.convert(now, LocalDate.class)); 
  11.     System.out.println("LocalDateTime转为LocalTime:" + conversionService.convert(now, LocalTime.class)); 
  12.  
  13.     // 时间戳转Instant 
  14.     long currMills = System.currentTimeMillis(); 
  15.     System.out.println("当前时间戳:" + currMills); 
  16.     System.out.println("时间戳转Instant:" + conversionService.convert(currMills, Instant.class)); 

运行程序,输出:

  1. 当前时间:2021-03-07T21:19:39.752 
  2. LocalDateTime转为LocalDate:2021-03-07 
  3. LocalDateTime转为LocalTime:21:19:39.752 
  4. 当前时间戳:1615123179763 
  5. 时间戳转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上使用?

 

责任编辑:武晓燕 来源: BAT的乌托邦
相关推荐

2020-06-02 16:30:20

Redis数据库字符串

2024-02-26 09:36:10

toggleAPIweb

2021-03-04 08:26:17

synchronizeReentrantLojava

2018-12-05 10:44:41

Redis缺点程序员

2021-03-17 13:44:14

隐私信息安全手机

2023-10-09 08:31:19

2021-11-04 12:42:55

RocketMQ启动消费

2019-09-24 09:46:35

Tomcat连接器Lifecycle

2022-02-17 10:56:33

Redis数据系统

2018-04-04 09:00:00

区块链X即服务微软

2022-09-21 16:25:17

Redis性能

2020-01-09 13:31:50

AI 数据人工智能

2021-03-17 10:20:14

网络安全网络安全技术周刊

2015-07-07 17:21:46

2021-02-27 11:03:26

算法职责ICBU

2015-03-02 10:35:25

百度专利经费

2020-04-07 16:21:38

疫情科技企业

2009-06-03 09:08:36

2022-02-23 08:18:06

nginx前端location

2020-11-24 08:05:18

5G卫星通信
点赞
收藏

51CTO技术栈公众号