查漏补缺@DateTimeFormat@DateTimeFormat做了什么?转载本文请联系BAT的乌托邦公众号。本文概述了版本约定SpringFramework:5.3.xSpringBoot:2.4.xSpring中的TextConverters和formatters是整个Spring技术栈体系中非常重要的一部分,也是很多高级特性的基础支持。作为Spring的用户,也许你已经工作了几年,只是接触到了@DateTimeFormat注解,才意识到Spring具有格式化功能;可能你不知道使用xml配置和SpringMVC自动封装的过程。无法感知Converter转换模块的存在;也许你还不确定@DateTimeFormat可以标记在哪些类型上,每次使用都得谷歌百度……作为一个Spring开发者,以上应该都不是问题了。相反,他很会说话,会背瓜。下面将本文的补充内容传递给大家,坐稳开始吧。@DateTimeFormat注解究竟做了什么?不用猜,很多程序员都知道/在SpringMVC场景下使用@DateTimeFormat注解,甚至只是在这个场景下:前端传递一个日期时间格式的值,后端使用Date/LocalDateTime接收这个值被使用。Request的请求实体是这样的:@DatapublicclassPerson{@DateTimeFormat(pattern="yyyy-MM-ddHH:mm:ss")privateLocalDateTimearriveTime;}这样前端传入"2021-03-0721:00:00"这种格式的字符串可以自动封装成arriveTime。解释:String->LocalDateTimearriveTime属于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管理底部的Converters分为三类。1:1、1:N、N:N?在注册中心注册后,转换服务就有能力转换AnnotationFormatterFactory支持的FieldType<->String。当然,它执行转换动作是有前提的。AnnotationFormatterFactory指定的注解类型必须标注在FieldType上。这时候@DateTimeFormat就会发挥作用。从这个角度来看,@DateTimeFormat注解实际上并没有做任何事情,只是纯粹作为Field上的元数据,指定参与判断和格式化所需要的参数。此注释适用于开发人员。真正做“很多事情”的是AnnotationFormatterFactory和FormatterRegistry等底层核心API。它们都是在初始化阶段默默完成的,所有这些(复杂的)逻辑对开发者来说是完全透明的。JSR310日期时间注册器上一篇文章介绍了Springformatter反转的思想,具体体现在FormatterRegistrar接口的设计上。支持java.util.Date类型的“较旧”DateFormatterRegistrar用于制作示例。体验倒置设计的好处。我们知道,Java领域的日期时间类型分为三大类:旧的Date系统、JSR310系统、Joda-time系统。这不对应FormatterRegistrar接口的继承体系的三个实现类:A哥在开发中出于任何原因不建议使用Date类型,而是用JSR310代替。那么接下来我们看看DateTimeFormatterRegistrar注册器是干什么的为了我们。DateTimeFormatterRegistrar:自4.0起的JSR310注册器。在Spring下用于支持JSR310日期时间的格式化/转换。我们知道,JSR310中对日期时间的格式化其实是非常完善的,体现在Java原生APIjava.time.format.DateTimeFormatter中。Spring的JSR310日期时间格式在DateTimeFormatter的基础上进行了简单的封装和适配,使其使用姿势尽可能与Date/JodaTime保持一致,从而对开发者和代码结构设计更加友好。可以接近统一。本系列前几篇文章中介绍的DateTimeFormatterFactory是DateTimeFormatter的简单包装器,DateTimeFormatter是一个用于生成格式化程序实例的工厂。这里的DateTimeFormatterRegistrar就是利用它们来进行一系列的注册动作,所以可以理解为一种更高级的封装形式。源码分析让我们从源码开始一探究竟。屏幕截图显示了实现类支持的类型示例。这里使用一个自定义的枚举类,以更抽象的方式定义了三种类型,分别是date、time和datetime。这三个类其实包含了JSR310类型的主要API,包括:LocalDate、LocalTime、LocalDateTime、ZonedDateTime、OffsetDateTime、OffsetTime,共6个API。将此与Jsr310DateTimeFormatAnnotationFormatterFactory支持的六种类型进行比较,如下截图所示:注意:该截图显示@DateTimeFormat只能对这6种类型的JSR310日期和时间有效。其实任何时候Spring都不推荐你直接使用原生的DateTimeFormatterAPI,而是使用它封装的org.springframework.format.datetime.standard.DateTimeFormatterFactory来获取一个DateTimeFormatter实例,这样使用起来更统一一致.灵活性。这不是DateTimeFormatterRegistrar,而是它的作用:这是唯一的构造函数:三种类型对应的DateTimeFormatters是由Spring封装的DateTimeFormatterFactory工厂“动态”生成的,而不是直接绑定的。由于DateTimeFormatter被设计成不可变的,如果在初始化的时候绑定了,后面就不能自定义了。这也是引入DateTimeFormatterFactory做自定义参数“缓存”的另一个功能~由于直接使用DateTimeFormatterFactory代替了DateTimeFormatter,自定义不同类型的参数非常方便。在DateTimeFormatterFactory上使用了以下方法,以保证多种条件并存:当然,最重要的是FormatterRegistrar接口方法的实现逻辑:①:这一步类似于调用它的publicstatic方法addDateConverters(registry)上文介绍DateFormatterRegistrar时,它作为注册基础转换Converter(如Date->Calendar,Date->LongConverter),从而提供基本的转换能力。值得注意的是:DateTimeConverters.registerConverters(registry)内部调用了DateFormatterRegistrar.addDateConverters(registry),还额外调用了LocalDate、Calendar、Long、Instant等Converter(如ZonedDateTimeToLocalDateConverter、LongToInstantConverter等),后者是A前者的超集。巧合的是:jodaTime的JodaTimeConverters.registerConverters(registry)内部也必须调用DateFormatterRegistrar.addDateConverters(registry)。有兴趣的可以自己查一下。②:生成每种类型对应的formatter。简单来说就是通过DateTimeFormatterFactory创建对应的formatterDateTimeFormatter③:这一步的作用在源码的注释部分解释的很清楚。这段大段代码的作用是使用ISO_LOCAL_*variantformatter代替Execution,效果是性能提升了2倍?说明:在上面提到的Jsr310DateTimeFormatAnnotationFormatterFactory中getPrinter()生成formatter的时候也用了这个方法,使转换性能翻倍?④:对于不需要特殊加速的类型,只需在绑定上注册专用的格式化程序org.springframework.format.Formatter。如PeriodFormatter、DurationFormatter等。⑤:让@DateTimeFormat注解提供对JSR310日期和时间的支持。关于格式化注释的知识,请爬上2层楼或点击文首/文末的推荐链接进入文章详细了解,加深记忆。代码示例下面介绍DateTimeFormatterRegistrar注册器的使用示例,包括API使用和面向注解的使用。API使用方式这种使用方式一般门槛较高,需要对底层API有很好的了解才能使用自如。一般需要在Spring基础上做二次开发的小伙伴使用。用一个简单的例子来理解用法:@Testpublicvoidtest1(){FormattingConversionServiceconversionService=newFormattingConversionService();//registrar负责添加formatters支持Date系列的转换newDateTimeFormatterRegistrar().registerFormatters((FormatterRegistry)conversionService);//1.常用(API方式)LocalDateTimenow=LocalDateTime.now();System.out.println("当前时间:"+now);System.out.println("LocalDateTime转换为LocalDate:"+conversionService.convert(now,LocalDate。类系统。out.println("LocalDateTime转换为LocalTime:"+conversionService.convert(now,LocalTime.class));//时间戳转为InstantlongcurrMillis=System.currentTimeMillis();System.out.println("当前时间戳:"+currMills);System.out.println("TimestamptoInstant:"+conversionService.convert(currMills,Instant.class));}运行程序,输出:当前时间:2021-03-07T21:19:39.752LocalDateTime转换为LocalDate:2021-03-07LocalDateTime转换为LocalTime:21:19:39.752当前时间戳:1615123179763时间戳转换为Instant:2021-03-07T13:19:39。763Z完美的通过了这个例子。现在知道为什么前端传一个时间戳,后端不用Long而是用Instant来“捕捉”不报错了~注解的用法在SpringMVC的集成使用一章有说明。JodaTimeFormatterRegistrar:joda-timeregistrar@deprecatedasof5.3,pleaseuseJavastandardJSR310datetimeinsteadTips:JodaDateTimeFormatAnnotationFormatterFactoryy在5.3版本中也被标记为过期?jodaTime曾经是绝对的王者,在水与火中拯救Java日期时间,直到JSR310系统的出现。同样的一句话是给你的:建议不要在(新)项目中以任何理由使用jodaTime,而是像Date一样彻底放弃,使用JSR310就够了。说明:不建议再使用JodaTime了,不是杀驴,而是JSR310是jodaTime的作者/组织捐给Java的(看语法,多么相似!),所以叫它更合适现在退休了吗?由于jodaTime没有Date那么重的历史包袱(关键Date还是JDK内置的核心类),而且与JSR310一脉相承,在可预见的未来将告别Java舞台而逐渐消亡。所以,我个人认为已经没有必要去学习jodaTime(包括外设)了,所以这部分暂时略过。综上所述,作为“失传”已久的“第一篇”文章,本文并没有多少新内容,主要是收下前两篇文章,为下一篇做铺垫。这篇文章虽然是补充内容,但还是有一些“含金量”的。希望对您有所帮助。请期待本系列接下来的精彩内容。本文思考题本文所属专栏:Spring类型转换,后台回复栏目名即可获取全部内容,已收录在https://yourbatman.cn。看了可能不明白,看懂了也未必明白。来来来,文末的三个问题帮你复习一下:@DateTimeFormat可以在LocalDateTime上标注吗?JSR310日期和时间的常用API是什么?@DateTimeFormat注解如何用在普通的JavaBean上?
