细节见真章,Formatter注册中心的设计很讨巧

开发 前端
本文以介绍FormatterRegistry接口为中心,重点研究了此接口的实现方式,发现即使小小的一枚注册中心实现,也蕴藏有丰富亮点供以学习、CV。

[[374345]]

 你好,我是A哥(YourBatman)。

Spring设计了org.springframework.format.Formatter格式化器接口抽象,对格式化器进行了大一统,让你只需要关心统一的API,而无需关注具体实现,相关议题上篇文章 有详细介绍。

Spring内建有不少格式化器实现,同时对它们的管理、调度使用也有专门的组件负责,可谓泾渭分明,职责清晰。本文将围绕Formatter注册中心FormatterRegistry展开,为你介绍Spring是如何优雅,巧妙的实现注册管理的。

学习编码是个模仿的过程,绝大多数时候你并不需要创造东西。当然这里指的模仿并非普通的CV模式,而是取精华为己所用,本文所述巧妙设计便是精华所在,任君提取。

本文提纲


版本约定

  • Spring Framework:5.3.x
  • Spring Boot:2.4.x

正文

对Spring的源码阅读、分析这么多了,会发现对于组件管理大体思想都一样,离不开这几个组件:注册中心(注册员) + 分发器

一龙生九子,九子各不同。虽然大体思路保持一致,但每个实现在其场景下都有自己的发挥空间,值得我们向而往之。

FormatterRegistry:格式化器注册中心

field属性格式化器的注册表(注册中心)。请注意:这里强调了field的存在,先混个眼熟,后面你将能有较深体会。

  1. public interface FormatterRegistry extends ConverterRegistry { 
  2.  
  3.  void addPrinter(Printer<?> printer); 
  4.  void addParser(Parser<?> parser); 
  5.  void addFormatter(Formatter<?> formatter); 
  6.  void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter); 
  7.  void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser); 
  8.  
  9.  void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory); 

此接口继承自类型转换器注册中心ConverterRegistry,所以格式化注册中心是转换器注册中心的加强版,是其超集,功能更多更强大。

❝关于类型转换器注册中心ConverterRegistry的详细介绍,可翻阅本系列的这篇文章,看完后门清❞

虽然FormatterRegistry提供的添加方法挺多,但其实基本都是在描述同一个事:为指定类型fieldType添加格式化器(printer或parser),绘制成图如下所示:


❝说明:最后一个接口方法除外,addFormatterForFieldAnnotation()和格式化注解相关,因为它非常重要,因此放在下文专门撰文讲解❞

FormatterRegistry接口的继承树如下:


有了学过ConverterRegistry的经验,这种设计套路很容易被看穿。这两个实现类按层级进行分工:

  • FormattingConversionService:实现所有接口方法
  • DefaultFormattingConversionService:继承自上面的FormattingConversionService,在其基础上注册默认的格式化器

事实上,功能分类确实如此。本文重点介绍FormattingConversionService,这个类的设计实现上有很多讨巧之处,只要你来,要你好看。

FormattingConversionService

它是FormatterRegistry接口的实现类,实现其所有接口方法。

FormatterRegistry是ConverterRegistry的子接口,而ConverterRegistry接口的所有方法均已由GenericConversionService全部实现了,所以可以通过继承它来间接完成 ConverterRegistry接口方法的实现,因此本类的继承结构是这样子的(请细品这个结构):


FormattingConversionService通过继承GenericConversionService搞定“左半边”(父接口ConverterRegistry);只剩“右半边”待处理,也就是FormatterRegistry新增的接口方法。

  1. FormattingConversionService: 
  2.  
  3.  @Override 
  4.  public void addPrinter(Printer<?> printer) { 
  5.   Class<?> fieldType = getFieldType(printer, Printer.class); 
  6.   addConverter(new PrinterConverter(fieldType, printer, this)); 
  7.  } 
  8.  @Override 
  9.  public void addParser(Parser<?> parser) { 
  10.   Class<?> fieldType = getFieldType(parser, Parser.class); 
  11.   addConverter(new ParserConverter(fieldType, parser, this)); 
  12.  } 
  13.  @Override 
  14.  public void addFormatter(Formatter<?> formatter) { 
  15.   addFormatterForFieldType(getFieldType(formatter), formatter); 
  16.  } 
  17.  @Override 
  18.  public void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter) { 
  19.   addConverter(new PrinterConverter(fieldType, formatter, this)); 
  20.   addConverter(new ParserConverter(fieldType, formatter, this)); 
  21.  } 
  22.  @Override 
  23.  public void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser) { 
  24.   addConverter(new PrinterConverter(fieldType, printer, this)); 
  25.   addConverter(new ParserConverter(fieldType, parser, this)); 
  26.  } 

从接口的实现可以看到这个“惊天大秘密”:所有的格式化器(含Printer、Parser、Formatter)都是被当作Converter注册的,也就是说真正的注册中心只有一个,那就是ConverterRegistry。


格式化器的注册管理远没有转换器那么复杂,因为它是基于上层适配的思想,最终适配为Converter来完成注册的。所以最终注册进去的实际是个经由格式化器适配来的转换器,完美复用了那套复杂的转换器管理逻辑。

❝这种设计思路,完全可以“CV”到我们自己的编程思维里吧❞

甭管是Printer还是Parser,都会被适配为GenericConverter从而被添加到ConverterRegistry里面去,被当作转换器管理起来。现在你应该知道为何FormatterRegistry接口仅需提供添加方法而无需提供删除方法了吧。

当然喽,关于Printer/Parser的适配实现亦是本文本文关注的焦点,里面大有文章可为,let's go!

PrinterConverter:Printer接口适配器

把Printer适配为转换器,转换目标为fieldType -> String。

  1. private static class PrinterConverter implements GenericConverter { 
  2.   
  3.  private final Class<?> fieldType; 
  4.  // 从Printer<?>泛型里解析出来的类型,有可能和fieldType一样,有可能不一样 
  5.  private final TypeDescriptor printerObjectType; 
  6.  // 实际执行“转换”动作的组件 
  7.  private final Printer printer; 
  8.  private final ConversionService conversionService; 
  9.  
  10.  public PrinterConverter(Class<?> fieldType, Printer<?> printer, ConversionService conversionService) { 
  11.   ... 
  12.   // 从类上解析出泛型类型,但不一定是实际类型 
  13.   this.printerObjectType = TypeDescriptor.valueOf(resolvePrinterObjectType(printer)); 
  14.   ... 
  15.  } 
  16.  
  17.  // fieldType -> String 
  18.  @Override 
  19.  public Set<ConvertiblePair> getConvertibleTypes() { 
  20.   return Collections.singleton(new ConvertiblePair(this.fieldType, String.class)); 
  21.  } 
  22.  

既然是转换器,重点当然是它的convert转换方法:

  1. PrinterConverter: 
  2.  
  3.  @Override 
  4.  @SuppressWarnings("unchecked"
  5.  public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { 
  6.   // 若sourceType不是printerObjectType的子类型 
  7.   // 就尝试用conversionService转一下类型试试 
  8.   // (也就是说:若是子类型是可直接处理的,无需转换一趟) 
  9.   if (!sourceType.isAssignableTo(this.printerObjectType)) { 
  10.    source = this.conversionService.convert(source, sourceType, this.printerObjectType); 
  11.   } 
  12.   if (source == null) { 
  13.    return ""
  14.   } 
  15.  
  16.   // 执行实际转换逻辑 
  17.   return this.printer.print(source, LocaleContextHolder.getLocale()); 
  18.  } 

转换步骤分为两步:

1.若源类型(实际类型)不是该Printer类型的泛型类型的子类型的话,那就尝试使用conversionService转一趟

    a.例如:Printer处理的是Number类型,但是你传入的是Person类型,这个时候conversionService就会发挥作用了

2.交由目标格式化器Printer执行实际的转换逻辑


可以说Printer它可以直接转,也可以是构建在conversionService 之上 的一个转换器:只要源类型是我能处理的,或者经过conversionService后能成为我能处理的类型,都能进行转换。有一次完美的能力复用。

说到这我估计有些小伙伴还不能理解啥意思,能解决什么问题,那么下面我分别给你用代码举例,加深你的了解。

准备一个Java Bean:

  1. @Data 
  2. @NoArgsConstructor 
  3. @AllArgsConstructor 
  4. public class Person { 
  5.  
  6.     private Integer id; 
  7.     private String name

准备一个Printer:将Integer类型加10后,再转为String类型

  1. private static class IntegerPrinter implements Printer<Integer> { 
  2.  
  3.     @Override 
  4.     public String print(Integer object, Locale locale) { 
  5.         object += 10; 
  6.         return object.toString(); 
  7.     } 

示例一:使用Printer,无中间转换

测试用例:

  1. @Test 
  2. public void test2() { 
  3.     FormattingConversionService formattingConversionService = new FormattingConversionService(); 
  4.     FormatterRegistry formatterRegistry = formattingConversionService; 
  5.     // 说明:这里不使用DefaultConversionService是为了避免默认注册的那些转换器对结果的“干扰”,不方便看效果 
  6.     // ConversionService conversionService = new DefaultConversionService(); 
  7.     ConversionService conversionService = formattingConversionService; 
  8.  
  9.     // 注册格式化器 
  10.     formatterRegistry.addPrinter(new IntegerPrinter()); 
  11.  
  12.     // 最终均使用ConversionService统一提供服务转换 
  13.     System.out.println(conversionService.canConvert(Integer.class, String.class)); 
  14.     System.out.println(conversionService.canConvert(Person.class, String.class)); 
  15.  
  16.     System.out.println(conversionService.convert(1, String.class)); 
  17.     // 报错:No converter found capable of converting from type [cn.yourbatman.bean.Person] to type [java.lang.String] 
  18.     // System.out.println(conversionService.convert(new Person(1, "YourBatman"), String.class)); 

运行程序,输出:

  1. true 
  2. false 
  3. 11 

完美。

但是,它不能完成Person -> String类型的转换。一般来说,我们有两种途径来达到此目的:

1.直接方式:写一个Person转String的转换器,专用

   a.缺点明显:多写一套代码

2.组合方式(推荐):如果目前已经有Person -> Integer的了,那我们就组合起来用就非常方便啦,下面这个例子将告诉你使用这种方式完成“需求”

   a.缺点不明显:转换器一般要求与业务数据无关,因此通用性强,应最大可能的复用

下面示例二将帮你解决通过复用已有能力方式达到Person -> String的目的。

示例二:使用Printer,有中间转换

基于示例一,若要实现Person -> String的话,只需再给写一个Person -> Integer的转换器放进ConversionService里即可。

❝说明:一般来说ConversionService已经具备很多“能力”了的,拿来就用即可。本例为了帮你说明底层原理,所以用的是一个“干净的”ConversionService实例❞

  1. @Test 
  2. public void test2() { 
  3.     FormattingConversionService formattingConversionService = new FormattingConversionService(); 
  4.     FormatterRegistry formatterRegistry = formattingConversionService; 
  5.     // 说明:这里不使用DefaultConversionService是为了避免默认注册的那些转换器对结果的“干扰”,不方便看效果 
  6.     // ConversionService conversionService = new DefaultConversionService(); 
  7.     ConversionService conversionService = formattingConversionService; 
  8.  
  9.     // 注册格式化器 
  10.     formatterRegistry.addFormatterForFieldType(Person.class, new IntegerPrinter(), null); 
  11.     // 强调:此处绝不能使用lambda表达式代替,否则泛型类型丢失,结果将出错 
  12.     formatterRegistry.addConverter(new Converter<Person, Integer>() { 
  13.         @Override 
  14.         public Integer convert(Person source) { 
  15.             return source.getId(); 
  16.         } 
  17.     }); 
  18.  
  19.     // 最终均使用ConversionService统一提供服务转换 
  20.     System.out.println(conversionService.canConvert(Person.class, String.class)); 
  21.     System.out.println(conversionService.convert(new Person(1, "YourBatman"), String.class)); 

运行程序,输出:

  1. true 
  2. 11 

完美。

针对本例,有如下关注点:

1.使用addFormatterForFieldType()方法注册了IntegerPrinter,并且明确指定了处理的类型:只处理Person类型
    a.说明:IntegerPrinter是可以注册多次分别用于处理不同类型。比如你依旧可以保留formatterRegistry.addPrinter(new IntegerPrinter());来处理Integer -> String是木问题的

2.因为IntegerPrinter 实际上 只能转换 Integer -> String,因此还必须注册一个转换器,用于Person -> Integer桥接一下,这样就串起来了Person -> Integer -> String。只是外部看起来这些都是IntegerPrinter做的一样,特别工整

3.强调:addConverter()注册转换器时请务必不要使用lambda表达式代替输入,否则会失去泛型类型,导致出错

   a.若想用lambda表达式,请使用addConverter(Class,Class,Converter)这个重载方法完成注册

ParserConverter:Parser接口适配器

把Parser适配为转换器,转换目标为String -> fieldType。

  1. private static class ParserConverter implements GenericConverter { 
  2.  
  3.  private final Class<?> fieldType; 
  4.  private final Parser<?> parser; 
  5.  private final ConversionService conversionService; 
  6.  
  7.  ... // 省略构造器 
  8.  
  9.  // String -> fieldType 
  10.  @Override 
  11.  public Set<ConvertiblePair> getConvertibleTypes() { 
  12.   return Collections.singleton(new ConvertiblePair(String.class, this.fieldType)); 
  13.  } 
  14.   

既然是转换器,重点当然是它的convert转换方法:

  1. ParserConverter: 
  2.  
  3.  @Override 
  4.  @Nullable 
  5.  public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { 
  6.   // 空串当null处理 
  7.   String text = (String) source; 
  8.   if (!StringUtils.hasText(text)) { 
  9.    return null
  10.   } 
  11.    
  12.   ... 
  13.   Object result = this.parser.parse(text, LocaleContextHolder.getLocale()); 
  14.   ... 
  15.    
  16.   // 解读/转换结果 
  17.   TypeDescriptor resultType = TypeDescriptor.valueOf(result.getClass()); 
  18.   if (!resultType.isAssignableTo(targetType)) { 
  19.    result = this.conversionService.convert(result, resultType, targetType); 
  20.   } 
  21.   return result; 
  22.  } 

转换步骤分为两步:

  1. 通过Parser将String转换为指定的类型结果result(若失败,则抛出异常)
  2. 判断若result属于目标类型的子类型,直接返回,否则调用ConversionService转换一把

可以看到它和Printer的“顺序”是相反的,在返回值上做文章。同样的,下面将用两个例子来加深理解。

  1. private static class IntegerParser implements Parser<Integer> { 
  2.  
  3.     @Override 
  4.     public Integer parse(String text, Locale locale) throws ParseException { 
  5.         return NumberUtils.parseNumber(text, Integer.class); 
  6.     } 

示例一:使用Parser,无中间转换

书写测试用例:

  1. @Test 
  2. public void test3() { 
  3.     FormattingConversionService formattingConversionService = new FormattingConversionService(); 
  4.     FormatterRegistry formatterRegistry = formattingConversionService; 
  5.     ConversionService conversionService = formattingConversionService; 
  6.  
  7.     // 注册格式化器 
  8.     formatterRegistry.addParser(new IntegerParser()); 
  9.  
  10.     System.out.println(conversionService.canConvert(String.class, Integer.class)); 
  11.     System.out.println(conversionService.convert("1"Integer.class)); 

运行程序,输出:

  1. true 

完美。

示例二:使用Parser,有中间转换

下面示例输入一个“1”字符串,出来一个Person对象(因为有了上面例子的铺垫,这里就“直抒胸臆”了哈)。

  1. @Test 
  2. public void test4() { 
  3.     FormattingConversionService formattingConversionService = new FormattingConversionService(); 
  4.     FormatterRegistry formatterRegistry = formattingConversionService; 
  5.     ConversionService conversionService = formattingConversionService; 
  6.  
  7.     // 注册格式化器 
  8.     formatterRegistry.addFormatterForFieldType(Person.class, null, new IntegerParser()); 
  9.     formatterRegistry.addConverter(new Converter<Integer, Person>() { 
  10.         @Override 
  11.         public Person convert(Integer source) { 
  12.             return new Person(source, "YourBatman"); 
  13.         } 
  14.     }); 
  15.  
  16.     System.out.println(conversionService.canConvert(String.class, Person.class)); 
  17.     System.out.println(conversionService.convert("1", Person.class)); 

运行程序,啪,空指针了:

  1. java.lang.NullPointerException 
  2.  at org.springframework.format.support.FormattingConversionService$PrinterConverter.resolvePrinterObjectType(FormattingConversionService.java:179) 
  3.  at org.springframework.format.support.FormattingConversionService$PrinterConverter.<init>(FormattingConversionService.java:155) 
  4.  at org.springframework.format.support.FormattingConversionService.addFormatterForFieldType(FormattingConversionService.java:95) 
  5.  at cn.yourbatman.formatter.Demo.test4(Demo.java:86) 
  6.  ... 

根据异常栈信息,可明确原因为:addFormatterForFieldType()方法的第二个参数不能传null,否则空指针。这其实是Spring Framework的bug,我已向社区提了issue,期待能够被解决喽:


为了正常运行本例,这么改一下:

  1. // 第二个参数不传null,用IntegerPrinter占位 
  2. formatterRegistry.addFormatterForFieldType(Person.class, new IntegerPrinter(), new IntegerParser()); 

再次运行程序,输出:

  1. true 
  2. Person(id=1, name=YourBatman) 

完美。

针对本例,有如下关注点:

1.使用addFormatterForFieldType()方法注册了IntegerParser,并且明确指定了处理的类型,用于处理Person类型

   a.也就是说此IntegerParser专门用于转换目标类型为Person的属性

2.因为IntegerParser 实际上 只能转换 String -> Integer,因此还必须注册一个转换器,用于Integer -> Person桥接一下,这样就串起来了String -> Integer -> Person。外面看起来这些都是IntegerParser做的一样,非常工整

3.同样强调:addConverter()注册转换器时请务必不要使用lambda表达式代替输入,否则会失去泛型类型,导致出错

二者均持有ConversionService带来哪些增强?

❝说明:关于如此重要的ConversionService你懂的,遗忘了的可乘坐电梯到这复习

对于PrinterConverter和ParserConverter来讲,它们的源目的是实现 String <-> Object,特点是:

  • PrinterConverter:出口必须是String类型,入口类型也已确定,即Printer的泛型类型,只能处理 T(或T的子类型) -> StringParserConverter:入口必须是String类型,出口类型也已确定,即Parser的泛型类型,只能处理 String -> T(或T的子类型)按既定“规则”,它俩的能力范围还是蛮受限的。Spring厉害的地方就在于此,可以巧妙的通过组合的方式,扩大现有组件的能力边界。比如本利中它就在PrinterConverter/ParserConverter里分别放入了ConversionService引用,从而到这样的效果:

ConversionService

通过能力组合协作,起到串联作用,从而扩大输入/输出“范围”,感觉就像起到了放大镜的效果一样,这个设计还是很讨巧的。

总结

本文以介绍FormatterRegistry接口为中心,重点研究了此接口的实现方式,发现即使小小的一枚注册中心实现,也蕴藏有丰富亮点供以学习、CV。

一般来说ConversionService 天生具备非常强悍的转换能力,因此实际情况是你若需要自定义一个Printer/Parser的话是大概率不需要自己再额外加个Converter转换器的,也就是说底层机制让你已然站在了“巨人”肩膀上。

☾本文思考题☽

看完了不一定看懂了,看懂了不一定记住了,记住了不一定掌握了。本文思考/进阶内容:

  1. FormatterRegistry作为注册中心只有添加方法,why?
  2. 示例中为何强调:addConverter()注册转换器时请务必不要使用lambda表达式代替输入,会有什么问题?
  3. 这种功能组合/桥接的巧妙设计方式,你脑中还能想到其它案例吗?

本文是 A哥(YourBatman) 原创文章,未经作者允许不得转载,谢谢合作。

 

责任编辑:姜华 来源: BAT的乌托邦
相关推荐

2021-08-17 09:46:57

设计细节产品体验用户

2012-06-27 14:19:33

2010-05-18 08:53:08

VB.NET

2013-03-15 14:47:56

ARM服务器评测ARM

2021-08-04 11:54:25

Nacos注册中心设计

2019-02-19 15:57:07

华为云

2018-07-25 11:08:38

新华三鹰视网络

2009-03-04 06:08:00

扫描服务器人性化设计服务器应用

2010-01-12 11:45:12

微软Linux嵌入式开发

2010-09-14 11:29:43

谷歌

2021-05-27 11:10:23

注册中心业务

2011-10-19 09:27:03

数据中心照明设计

2013-06-05 15:56:43

华为OceanStor

2015-10-14 11:29:17

数据中心细节

2023-01-12 12:21:36

AI自动驾驶

2011-12-01 14:32:13

Facebook数据中基础架构

2011-09-15 09:42:35

2014-11-19 14:25:43

DNSPOD域名注册

2011-04-21 17:28:19

点赞
收藏

51CTO技术栈公众号