本文转载自微信公众号「BAT的乌托邦」,作者YourBatman 。转载本文请联系BAT的乌托邦公众号。
请人吃饭不如请人出汗,请人出汗不如送人以渔。A哥春节继续营业,这个时候还能看得下去这种技术文章的同学我猜有三类:
- 要么孤独了
- 要么喝醉了
- 要么喝醉后觉得孤独了
现实情况往往挺扎心,所以牢记使命,砥砺前行是个好办法。
上篇文章 把@DateTimeFormat和@NumberFormat注解的实现原理搞清楚了,通过面向元数据编程屏蔽了理解层面、实施层面上的差异化。同时,通过手敲代码案例,扎扎实实、彻彻底底搞明白了@DateTimeFormat等注解有何用以及如何用,从此不再虚。
像AnnotationFormatterFactory、xxxConverter这种均属于low-level底层API,上手起来一般颇具难度。一个良好的、流行的框架最起码应该是上手简单的,所以开发者应该是最多关心到FormattingConversionService/ConversionService层面即止。本文带你看看Spring是如何做到酱紫的~
本文提纲
版本约定
- Spring Framework:5.3.x
- Spring Boot:2.4.x
正文
上文是通过手动调用API的方式实现元数据的解析从而达到数据格式化(转换)的目的,而在实际应用场景中,作为业务开发者是不可能去直接去操纵API的,毕竟说到底那对开发者太不友好,使用门槛过高。
因此,本文将介绍的是一种更为“高级”的使用方案,看看Spring是如何做到兼具高扩展性的整合,从而对开发者十分友好,相信这便也是Spring最有魅力的地方,一起来学习学习吧。
FormatterRegistry:注册中心
对于多组件的管理,注册中心是个很好的解决方案。
FormatterRegistry其实在:9. 细节见真章,Formatter注册中心的设计很讨巧 这篇文章已经有过很详细的分析,学到了它那非常巧妙的设计,这里也顺道推荐你花几分钟前往看看。在这篇文章的末尾,A哥故意留下了一个小尾巴没讲:注册中心对注解工厂AnnotationFormatterFactory的支持,也就是这个接口方法:
- FormatterRegistry:
- void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);
现在时机成熟,本文就来重点关照它。
该接口方法的唯一实现在FormattingConversionService里:
①:从AnnotationFormatterFactory的泛型类型中提取到注解类型。注意:若没有指定泛型(没有指定注解类型)就抛出异常②:该工厂类支持的类型们③:对于支持的每个类型,均注册一个Printer/Parser
重点在于步骤③,AnnotationPrinterConverter和AnnotationParserConverter均是一个ConditionalGenericConverter转换器,底层实现实际委托给AnnotationFormatterFactory去完成,所以说对AnnotationFormatterFactory的理解格外的重要,还好上篇文章对它已经做了详尽分析,点击这里电梯直达。
下面以AnnotationPrinterConverter为例观其源码:
①:该转换器只负责将fieldType类型转换为String类型②:只有fieldType上标注有指定的这个注解,此转换器才会生效③:转换逻辑。这种缓存式处理逻辑很是常见,其实最核心的代码往往只有一句,本处就是它:this.annotationFormatterFactory.getPrinter(...)。获取到合适的Printer,然后适配为PrinterConverter从而完成最终的convert转换动作
❝说明:PrinterConverter和ParserConverter在本系列前面文章已介绍,相关内容可出门左拐在本系列内很容易找到❞AnnotationParserConverter的实现逻辑如出一辙,这里就不再啰嗦了。
FormattingConversionService它实现了FormatterRegistry接口的所有接口方法,但是它并未提供一些默认行为。换句话讲:实现了所有的组件注册/管理的能力,但并没有“帮你”注册任何组件,所以还不具备能够直接提供服务的条件,若要使用还需“人工干预”放些组件进去才行。
一般来讲,对于这种情况一般在外部再包一层 DefaultXXX来提供默认服务是一种对开发者十分友好的解决方案,Spring也是这么干的,下面来看看DefaultFormattingConversionService为我们默认注册了哪些基础组件,提供了哪些能力呢。
DefaultFormattingConversionService
默认的格式化器转换服务,该默认行为适用于大多数应用程序对格式化器、转换器的需求。
继承自FormattingConversionService,这个默认行为是为该实例而设计的,但为了方便使用,它对外暴露了其static静态方法addDefaultFormatters(),这个设计方式同DefaultConversionService暴露了静态方法addDefaultConverters()如出一辙。
默认注册了哪些组件?
对于一个默认的Service服务,最关心的当属它提供了哪些能力。换句话讲:它默认帮我们注册了哪些组件呢?
要回答这个问题可不能靠“背答案”,方式方法其实非常的简单,爬进去它的源码处一看便知:
①:虽然说本类(其实是父类)实现了EmbeddedValueResolverAware接口,但构造时依旧可以指定占位符处理器StringValueResolver,当然一般情况下传入null即可②:调用DefaultConversionService的静态方法,把默认的转换器们都注册进来。那么,默认到底注册了哪些转换器呢?DefaultConversionService.addDefaultConverters(this)该静态方法其实是本系列前面文章所讲的内容,这里A哥顺道也贴在这吧:
③:若registerDefaultFormatters为true就添加默认的格式化器们,一般来讲,此值都为true。那么,默认到底注册了哪些格式化器呢?
①:对@NumberFormat注解提供支持,格式化数字(Currency、数字、百分数等)
②:对JSR 354钱币类型javax.money.CurrencyUnit、Monetary等类型提供支持。一般情况下,用不着,所以此part不会被真的注册
③:对JSR-310日期时间的格式化提供支持。这里使用到了其专用的注册器DateTimeFormatterRegistrar统一操作
④、⑤:第4、5步是互斥操作,若有Jota-Time就提供对它的支持而不触发java.util.Date的注册器,否则使用后者注册器。
注意:你以为④、⑤是真的互斥吗?难道导入了joda-time的包后java.util.Date相关模块就失效了?很明显不是这样的,让你“放心”的地方在于JodaTimeFormatterRegistrar注册器内部包含了java.util.Date格式化器的注册关系,因此一切都还得到xxxRegistrar里去看才能揭晓。
总之,DefaultFormattingConversionService作为默认的格式化转换服务,它是DefaultConversionService的超集,在其基础上扩展了格式化器,格式化注解支持等相关能力。在Spring环境下,大多数情况使用都是它而非DefaultConversionService。
现在,对FormatterRegistry类一个笼统的认识,知道它默认给注册了哪些组件,支持哪些功能,但是细节部分还不清晰。比如说:支持哪些数据类型?支持哪些格式?这些都藏在相应的xxxRegistrar里~
FormatterRegistrar:注册员
registrar:登记员;注册主任。
xxxRegistrar它是一种“倒排”思想的设计体现,能达到高内聚的效果。Spring、Spring Boot惯用的“伎俩”,譬如你随便一搜就能看能看到很多很多:
FormatterRegistrar代表的是格式化器注册员接口,接口定义:
- public interface FormatterRegistrar {
- void registerFormatters(FormatterRegistry registry);
- }
接口方法含义:将Converter和Formatter注册进FormatterRegistry注册中心里,至于注册哪些组件由各子类自行管理和负责,而非Registry注册中心主动去编排。这是一种倒排设计思想,能够很好的达到高内聚的目的。
❝注意:虽然存在ConverterRegistry和FormatterRegistry两个接口,但只有FormatterRegistrar而 没有 ConverterRegistrar哦❞该接口有三个实现类:
见名之意,每个实现子类都维护着自己分内之事,边界十分清晰。
DateFormatterRegistrar:Date注册员
提供对java.util.Date、java.util.Calendar、long类型的日期时间的注册支持。
接口方法实现如下:
①:添加常规转换器,支持DateToLong、DateToCalendar、LongToCalendar等基础转换能力②:若有个性化指定格式化器,那就给Calendar专门使用。当然,大多数情况下并不会这么做,这步逻辑是为了向后兼容性而考虑而已,一般可忽略③:添加@DateTimeFormat注解的解析支持
代码示例
下面介绍DateFormatterRegistrar注册员的使用示例。
普通使用方式
最常规的转换,Date、Long、Calendar等日期时间类型似乎是可以互转的。
- @Test
- public void test1() {
- FormattingConversionService conversionService = new FormattingConversionService();
- // 注册员负责添加格式化器以支持Date系列的转换
- new DateFormatterRegistrar().registerFormatters((FormatterRegistry) conversionService);
- // 1、普通使用
- long currMills = System.currentTimeMillis();
- System.out.println("当前时间戳:" + currMills);
- // Date -> Calendar
- System.out.println(conversionService.convert(new Date(currMills), Calendar.class));
- // Long -> Date
- System.out.println(conversionService.convert(currMills, Date.class));
- // Calendar -> Long
- Calendar calendar = Calendar.getInstance(TimeZone.getDefault());
- calendar.setTimeInMillis(currMills);
- System.out.println(conversionService.convert(calendar, Long.class));
- }
运行程序,输出:
- 当前时间戳:1612741385457
- java.util.GregorianCalendar[time=1612741385457 ...
- Mon Feb 08 07:43:05 CST 2021
- 1612741385457
完美。
注解使用方式
使用更高级的注解方式,如@DateTimeFormat
- // 准备一个Java Bean:
- @Data
- @AllArgsConstructor
- class Son {
- @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
- private Date birthday;
- }
测试代码:
- @Test
- public void test1() {
- FormattingConversionService conversionService = new FormattingConversionService();
- // 重要:重要:重要:注册基础的转换能力
- DefaultConversionService.addDefaultConverters((ConverterRegistry) conversionService);
- // 注册员负责添加格式化器以支持Date系列的转换
- new DateFormatterRegistrar().registerFormatters((FormatterRegistry) conversionService);
- // 1、注解使用
- Son son = new Son(new Date());
- // 输出:将Date类型输出为Long类型
- System.out.println(conversionService.convert(son.getBirthday(), Long.class));
- // 输出:将String烈性输入为Date类型
- // System.out.println(conversionService.convert("2021-02-12", Date.class)); // 报错
- System.out.println(conversionService.convert(1613034123709L, Date.class));
- }
运行程序,输出:
- 1613034230018
- Thu Feb 11 17:02:03 CST 2021
完美。实现了Long类型 <-> Date类型的互转。
可能有同学会问了,为毛"2021-02-12"就不能convert到Date类型呢?这个原因,额,嗯,哼,若你看了上篇文章 的话,这将不会是个问题。
当然,在实际使用中,更多的情况是String -> Date的转换case,怎么破?有两个办法:
回味本系列前面文章,因为前面有讲了不止一次
关注后面文章。因为此case过于常见,后面(特别是在Spring MVC下使用)依旧会重点提及
总结
本文重点是想经由FormatterRegistry注册中心,引述出Spring常用的Registrar注册员设计思想,它是一种面向对象编程思想的体现,是不是比面向过程优雅很多呢?本文以DateTimeFormatterRegistrar为示例进行了打样,可以看到Spring在API抽象这块着实是非常优秀的,扩展性和方便性兼具,这个度把握得绝佳,或许这也算是设计美学吧。