前言大家好,我是A哥(你的蝙蝠侠)。至此本系列(Spring类型转换)大部分理论基础已经尘埃落定,非常抽象,甚至枯燥乏味。幸运的是,终于要结束了,我应该给自己一秒钟的掌声,跟随“学习”。后面的内容会更偏向于应用,比如在SpringMVC中的应用,在IoC容器中的应用,在JPA中的应用等,后面的内容是否比前面的基础更重要无法一概而论,但我相信大多数学生会更感兴趣。毕竟具体的东西更容易接受,更符合人性,而且很多都是可以在工作中使用,考试中考证,面试中问到的知识点。热情自然会高很多。作为“两者”之间的纽带,本文将介绍自定义ConversionService类型转换服务的高手FormattingConversionServiceFactoryBean,以及很少有人会关注但在设计思路上非常重要的DateTimeContext和DateTimeContextHolder。值得一看。本文概述版本协议SpringFramework:5.3.xSpringBoot:2.4.xTextConversionService是Spring从3.0开始提出的一种全新的统一类型转换服务。它有两个在Spring框架下可以用于生产的主要实现:DefaultConversionService:Default注册了很多常规的类型转换器,比如Number->String,String->Collection...,但是它没有组件DefaultFormattingConversionService对于日期/时间和数字格式化:在DefaultConversionService的基础上进行了增强(但不是继承自它),增加了格式化相关的内容。如支持:Date、JSR310、数字货币百分比等。格式化相关内容虽然Spring内置的converter/formatter可以“应付”大部分场景,但有时候我们还是需要DIY。通过前面的学习,我们知道在注册中心注册formatter/converter的方式有很多种。能否通过降低用户使用门槛,提供更统一的编程体验?没错,就是今天的主角:FormattingConversionServiceFactoryBean。FormattingConversionServiceFactoryBean是用于生成FormattingConversionService实例的工厂类。它旨在促进集中配置。在此之前,稍微回顾一下:FormattingConversionService实现了FormatterRegistry接口,继承自GenericConversionService,因此在功能上是DefaultConversionService的超集。一般来说,我们常说的ConversionService转换服务的底层实现都会用到它(子类),区分以下几种情况:在SpringFramework环境下,它的子类只有DefaultFormattingConversionService(默认有很多格式化器/转换器,支持JSR310、数字格式化、格式化注解等)在SpringBoot环境下,其子类包括ApplicationConversionService和WebConversionService。ApplicationConversionService没有继承自DefaultFormattingConversionService但比它更强大:现在添加了更多转换器,并且可以从容器中自动检索Converter/Formatter类型的Bean,然后进行注册。WebConversionService继承自DefaultFormattingConversionService,增强了对JSR310更强的支持。在SpringBoot的web环境中,该实例替代了注解@EnableWebMvc/@EnableWebFlux指定的默认转换服务实例。另外,请记住,作为一个基础组件,ConversionService并不是全局只有一个。在SpringFramework和SpringBoot环境下有不同的表现,本系列下半篇会详细分析。为什么需要它?根据本系列前几篇文章,虽然格式化器/转换器的底层形式是xxxConverter,但其“上层”注册方式并不单一,它提供了多种方法,表现出极大的灵活性,易于使用和扩展.以FormatterRegistry(继承自ConverterRegistry)注册中心为例,它提供了很多方法让你向注册中心注册formatter/converter,API如下://===========1.直接注册转换器==========voidaddConverter(Converter,?>converter);voidaddConverter(ClasssourceType,ClasstargetType,Converterconverter);voidaddConverter(GenericConverterconverter);voidaddConverterFactory(ConverterFactory,?>factory);//===========2.注册Formatter格式化器(底层适配Converter转换器)==========voidaddPrinter(Printer>printer);voidaddParser(Parser>parser);voidaddFormatter(Formatter>formatter);voidaddFormatterForFieldType(Class>fieldType,Formatter>formatter);//===========3.通过注解工厂为某些标有自定义注解的格式注册格式化器/转换器===========voidaddFormatterForFieldAnnotation(AnnotationFormatterFactory<?extendsAnnotation>annotationFormatterFactory);除了这些直接用于注册的接口API来完成注册之外,Spring还提供了一些批量注册的方式。虽然底层仍然依赖于这些API接口,但这种聚合方式极大地改善了其治理并简化了注册流程。比如专文中重点介绍的FormatterRegistrar注册器就是一个典型代表。关于formatter/converter的注册方式,A哥试着画了一张图来说明:由此可见,注册formatter/converter的方式有很多种。因此,为了方便,Spring设计了FormattingConversionServiceFactoryBean,将ConversionService实例集中提供给容器,尽量提供统一的编程体验,屏蔽更多细节,方便用户使用。如何实现?知道了这个FactoryBean的功能定位,实现起来其实还是比较简单的。无非是将各种“手段”整合在一起,进行集中定制和管理。从这些成员变量中可以看出,注册转换器的所有方式都包含在内。细心的你可能会疑惑:为什么没看到传入注解工厂AnnotationFormatterFactory的方式???其实归类为Setformatters(Set的泛型是什么?),下面的源码可以“证明”:①:负责注册所有的converter。包括Converter、ConverterFactory、GenericConverter三种类型,涵盖1:1、N:1、N:N所有场景②:负责注册Formatter和注解工厂方法。这里有两点值得你特别注意:不支持单独注册Printer/Parser,因为Spring认为任何类型的formatter都应该是双向的AnnotationFormatterFactory放在Setformatters中,和Formatter一起③:负责处理Batch的注册动作注册商xxxRegistrar.比如DateTimeFormatterRegistrar和DateFormatterRegistrar等。关于注册器FormatterRegistrar的详细介绍可以参考这篇文章:11.春节礼物:Spring的Registrar倒置思路送给你最后,从上面还有一点值得你注意图片:工厂生产的ConversionService实例是一个固定的DefaultFormattingConversionService,这就是为什么我说在SpringFramework环境下默认使用的ConversionService实例是它的原因,无论是web还是非web场景。直接使用FormattingConversionServiceFactoryBean的场景确实不多。除非你非常了解这个机制并且想完全取代它,否则建议你使用它。例如:在SpringFramework环境下,如果要启用SpringMVC模块,就会使用@EnableWebMvc注解来启用。这个时候SpringMVC默认往容器里面放了一个ConversionService实例:WebMvcConfigurationSupport:@BeanpublicFormattingConversionServicemvcConversionService(){FormattingConversionServiceconversionService=newDefaultFormattingConversionService();addFormatters(conversionService);returnconversionService;}protectedvoidaddFormatters(FormatterRegistryregistry){}暴露addFormatters()扩展点.一般来说,如果要自定义formatter/converter,建议通过重写这个方法来添加的方式。?说明:这里只代表SpringFramework环境。如果是在SpringBoot下,会有不同的表现,不同的定制方式。?另外,从这部分源码中我们可以看出,并没有FormattingConversionServiceFactoryBean构造的类型转换服务实例。而是通过直接new的方式。其实这里如果使用FormattingConversionServiceFactoryBean来构建,我觉得还是比较方便的,留下扩展点也比较方便。你怎么认为?DateTimeContext:细粒度的个性化定制Spring从4.0开始提供了DateTimeContextHolder,用于线程BindDateTimeContext。而DateTimeContext提供了:Chronology(Java中的日历系统)、ZoneId(JSR310中的时区)、DateTimeFormatter(JSR310格式器)等上下文数据,如果你需要这些上下文信息,可以使用这个API进行绑定。publicclassDateTimeContext{@NullableprivateChronologychronology;@NullableprivateZoneIdtimeZone;...//省略get/set}如果需要自定义,可以将这两个值(日历和时区)设置到context实例中。当然,最重要的是从context中获取Aformatter,这也是最终目的:①:如果设置了timeZone时区,则以其为准。否则,转到步骤②②:如果未设置时区,则尝试从LocaleContext上下文中获取时区。关于格式化,注意:这个方法是实例方法而不是静态方法,所以必须先自己新建一个DateTimeContext。再看DateTimeContextHolder,它使用ThreadLocal绑定DateTimeContext和线程,方便用户获取上下文数据:privatestaticfinalThreadLocaldateTimeContextHolder=newNamedThreadLocal<>("DateTimeContext");该类除了维护DateTimeContext外,还提供了更直接的Method:根据当前上下文,直接获取DateTimeFormatterformatter实例:①:将Locale属性绑定到调用者传入的formatter,如果存在②:获取当前上下文对象DateTimeContext,然后根据当前上下文(如果存在)获取处理后的DateTimeFormatter实例。这个静态方法可以认为是对DateTimeContext#getFormatter()的封装,也可以自定义扩展的Locale参数。用户可以一步获取上下文相关的DateTimeFormatter实例。大多数时候我们直接使用这个方法会更方便。?问题:为什么Locale参数没有放在LocalDateContext上下文属性中?你能猜出Spring是如何设计和考虑的吗??使用场景和其他xxxContext一样,结合使用场景理解可以更深刻。毕竟,一切学习都是关于应用的。上下文的概念在编程世界中非常普遍。无论是业务开发、中间件开发,还是基础设施开发,我觉得都有应用它的理由。由于DateTimeFormatter是线程安全的,为了方便开发,通常会设置一个(已经配置好的)全局通用实例,如下所示:/***Globalgeneraldate-timeformatter(当然也可以有一个日期专用的,特定时间的...)*/publicstaticfinalDateTimeFormatterGLOBAL_DATETIME_FORMATTER=DateTimeFormatter.ofPattern("yyyy-MM-ddHH:mm:ss").withLocale(Locale.CHINA).withZone(ZoneId.of("Asia/Shanghai")).withChronology(IsoChronology.INSTANCE);这样子项目中所有需要使用格式化程序DateTimeFormatter的地方都可以从这里获取,方便且统一管理,可谓一举两得。但是,但是,但是,难免有时候会有个性化的格式化需求,个性化的粒度还是很细的。比如在SpringMVC场景下,不同接口的返回值想自定义Locale,自定义ZoneId时区等返回不同的数据格式,又想复用全局设置尽量保持统一(毕竟,个性化参数一般只有1~2个而已)。听过不同的接口,敏感的人可以发现,这是一个典型的可以用Context解决的场景:既不影响全局,又实现了线程级别的定制。对于这个场景,我使用代码示例来模拟Demo。代码示例@Testpublicvoidtest1()throwsInterruptedException{//模拟请求参数(同一个参数,不同接口表现不同)Instantstart=Instant.now();//模拟Controller接口1:zoneId不同newThread(()->{DateTimeContextcontext=newDateTimeContext();context.setTimeZone(ZoneId.of("America/New_York"));DateTimeContextHolder.setDateTimeContext(context);//根据全局格式化器+自己的上下文格式化器自定义一个专用接口DateTimeFormatterprimaryFormatter=DateTimeContextHolder.getFormatter(GLOBAL_DATETIME_FORMATTER,null);System.out.printf("北京时间%s界面1时间%s\n",GLOBAL_DATETIME_FORMATTER.format(start),primaryFormatter.format(start));}).start();//模拟Controller界面2:Locale不同newThread(()->{//基于globalformatter+自己的context自定义一个专用于这个接口的formatterDateTimeFormatterprimaryFormatter=DateTimeContextHolder.getFormatter(GLOBAL_DATETIME_FORMATTER,Locale.US);System.out.printf("北京时间%s接口2时间%s\n",GLOBAL_DATETIME_FORMATTER.format(start),primaryFormatter.format(start));}).start();TimeUnit.SECONDS.sleep(2);}运行程序,输出:北京时间2021-03-15T07:29:37.8+08:00[Asia/Shanghai]接口1次2021-03-14T19:29:37.8-04:00[America/New_York]北京时间2021-03-15T07:29:37.8+08:00[Asia/Shanghai]接口2次2021-03-15T07:29:37.8+08:00[Asia/Shanghai]通过这种操作上下文的方式完美的达到了重用和个性化的目的:重用globalformatter的配置个性化只是局部个性化,globalformatter没有影响,风险可控,但实现了非常自由的个性化需求。有的同学可能会问,如果要自定义Pattern怎么办呢?答案是:我不能。Java的DateTimeFormatter和Pattern属于强绑定关系。如果更改了Pattern,则必须使用新的DateTimeFormatter实例,并且不能(内部)复制其他属性。至于原因,A哥在解释JDK日期和时间的时候提到过。具体可以关注我,参考JDK日期时间系列。?说明:一般来说,对于一个项目来说,Pattern不太可能需要个性化。如果是这种情况,请完全自定义一个DateTimeFormatter进行处理。?总结本文介绍了Spring的两个组件:FormattingConversionServiceFactoryBean:类型转换服务工厂,注册管理格式化器/转换器推荐方案DateTimeContext:因为自定义日期时间格式化器是比较常见的需求,Spring在4.0推出了这个API,方便用户实现更精细-粒度控制。还是那句话,使用它会事半功倍而且代码优雅易维护的思维模式,要知道它还有很大的应用空间。以后本系列会更偏向于应用层面的案例分析,SpringMVC场景的使用将首当其冲。欢迎大家一起讨论、交流、学习。本文思考题本文所属专栏:Spring类型转换,后台回复栏目名即可获取全部内容,已收录在https://yourbatman.cn。看了可能不明白,看懂了也未必明白。来,文末的3个问题帮你复习一下:如何使用FormattingConversionServiceFactoryBean自定义类型转换服务?Spring设计DateTimeContext和DateTimeContextHolder是为了解决什么问题?为什么DateTimeContextHolder#getFormatter方法的第二个参数Locale没有放在DateTimeContext中?显然可以这样做。系列推荐12.@DateTimeFormat做了什么检查间隙?