本文转载自微信公众号「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的请求实体形如这样:
- @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上使用?