
前言
你好,我是A哥(YourBatman)。好久不见,甚是想念,女王节快乐!
回忆本系列前两篇的文章内容,主要讲解了@DateTimeFormat
和@NumberFormat
注解的实现原理细节,以及FormatterRegistrar格式化器注册员等相关内容。由于时间相隔稍微有点久了,所以链接贴在这方便你做回顾:
根据反馈,这两篇文章所讲“知识点”依然存在可能不全,或者没讲太明白的地方。我自己反复读了两遍,觉得会有至少这两点疑惑:
@DateTimeFormat
等注解到底是如何工作的?JSR 310日期时间注册员Registrar做了哪些工作?
以这两个问题为主线,此文将继续把这(两)部分内容补充完整。
本文提纲

版本约定
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上使用?
系列推荐
11. 春节礼物:Spring的Registrar倒排思想送给你 10. 原来是这么玩的,@DateTimeFormat和@NumberFormat 9. 细节见真章,Formatter注册中心的设计很讨巧

System.out.println("点个赞吧!");
print_r('关注【BAT的乌托邦】!');
var_dump('私聊A哥:fsx1056342982');
console.log("点个赞吧!");
NSLog(@"关注【BAT的乌托邦】!");
print("私聊A哥:fsx1056342982");
echo("点个赞吧!");
cout << "关注【BAT的乌托邦】!" << endl;
printf("私聊A哥:fsx1056342982");
Console.WriteLine("点个赞吧!");
fmt.Println("关注【BAT的乌托邦】!");
Response.Write("私聊A哥:fsx1056342982");
alert("点个赞吧!");
A哥(YourBatman)
:Spring Framework开源贡献者,Java架构师,领域专家。文章不标题党,不哗众取宠,每篇文章都成系列去系统的攻破一个知识点,每个系列可能是全网最佳/唯一。注重基本功修养,底层基础决定上层建筑。现有IDEA系列、Spring N多系列、Bean Validation系列、日期时间系列......关注免费获取




