9. 细节见真章,Formatter注册中心的设计很讨巧(上)

简介: 9. 细节见真章,Formatter注册中心的设计很讨巧(上)

本文提纲


image.png


版本约定


Spring Framework:5.3.x

Spring Boot:2.4.x


✍正文


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


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


FormatterRegistry:格式化器注册中心


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


public interface FormatterRegistry extends ConverterRegistry {
  void addPrinter(Printer<?> printer);
  void addParser(Parser<?> parser);
  void addFormatter(Formatter<?> formatter);
  void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
  void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
  void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);
}



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


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


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


image.png


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


FormatterRegistry接口的继承树如下:

image.png


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


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


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


FormattingConversionService


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


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


image.png


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


FormattingConversionService:
  @Override
  public void addPrinter(Printer<?> printer) {
    Class<?> fieldType = getFieldType(printer, Printer.class);
    addConverter(new PrinterConverter(fieldType, printer, this));
  }
  @Override
  public void addParser(Parser<?> parser) {
    Class<?> fieldType = getFieldType(parser, Parser.class);
    addConverter(new ParserConverter(fieldType, parser, this));
  }
  @Override
  public void addFormatter(Formatter<?> formatter) {
    addFormatterForFieldType(getFieldType(formatter), formatter);
  }
  @Override
  public void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter) {
    addConverter(new PrinterConverter(fieldType, formatter, this));
    addConverter(new ParserConverter(fieldType, formatter, this));
  }
  @Override
  public void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser) {
    addConverter(new PrinterConverter(fieldType, printer, this));
    addConverter(new ParserConverter(fieldType, parser, this));
  }


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


image.png


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


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


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


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


PrinterConverter:Printer接口适配器


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


private static class PrinterConverter implements GenericConverter {
  private final Class<?> fieldType;
  // 从Printer<?>泛型里解析出来的类型,有可能和fieldType一样,有可能不一样
  private final TypeDescriptor printerObjectType;
  // 实际执行“转换”动作的组件
  private final Printer printer;
  private final ConversionService conversionService;
  public PrinterConverter(Class<?> fieldType, Printer<?> printer, ConversionService conversionService) {
    ...
    // 从类上解析出泛型类型,但不一定是实际类型
    this.printerObjectType = TypeDescriptor.valueOf(resolvePrinterObjectType(printer));
    ...
  }
  // fieldType -> String
  @Override
  public Set<ConvertiblePair> getConvertibleTypes() {
    return Collections.singleton(new ConvertiblePair(this.fieldType, String.class));
  }
}


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


PrinterConverter:
  @Override
  @SuppressWarnings("unchecked")
  public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
    // 若sourceType不是printerObjectType的子类型
    // 就尝试用conversionService转一下类型试试
    // (也就是说:若是子类型是可直接处理的,无需转换一趟)
    if (!sourceType.isAssignableTo(this.printerObjectType)) {
      source = this.conversionService.convert(source, sourceType, this.printerObjectType);
    }
    if (source == null) {
      return "";
    }
    // 执行实际转换逻辑
    return this.printer.print(source, LocaleContextHolder.getLocale());
  }


换步骤分为两步:


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

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

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


image.png


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


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


准备一个Java Bean:


@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
    private Integer id;
    private String name;
}


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


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


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


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


运行程序,输出:

true
false
11


完美。


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


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

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


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

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


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

相关文章
|
3月前
|
前端开发 数据库 JavaScript
基于Flowable的流程挂接自定义业务表单的设计与实践
文章讨论了如何在Flowable流程引擎中挂接自定义业务表单,以及相关设计和实践的步骤。文章中包含了一些前后端代码示例,如Vue组件的模板和脚本部分,这些代码用于实现与Flowable流程引擎交互的界面。例如,有一个按钮组件用于提交申请,点击后会触发applySubmit方法,该方法会与后端API进行交互,处理流程启动、查询关联流程等逻辑。
48937 11
|
4月前
|
存储
注册中心是如何工作的
【2月更文挑战第8天】
|
存储 缓存 监控
聊聊消息中心的设计与实现逻辑
消息通知的流程设计,在各个业务线中通过消息中心提供的接口方法,将不同场景下的消息内容提交到消息中心,消息中心进行统一维护管理,并根据消息的来源和去向,适配相应的推送逻辑。
1141 0
聊聊消息中心的设计与实现逻辑
|
消息中间件 存储 运维
NBF事件中心架构设计与实现
本文介绍了事件驱动架构在供应链执行链路的应用背景和实践过程,并介绍了NBF事件中心产品的设计和部分实现。目前事件中心每日事件发送量峰值在千万级别,平稳度过了双11、双12、年货节等流量高峰。
796 2
NBF事件中心架构设计与实现
|
Java 测试技术 计算机视觉
9. 细节见真章,Formatter注册中心的设计很讨巧(下)
9. 细节见真章,Formatter注册中心的设计很讨巧(下)
9. 细节见真章,Formatter注册中心的设计很讨巧(下)
|
负载均衡 网络协议 JavaScript
Nacos配置中心交互模型是 push 还是 pull ?你应该这么回答
对于Nacos大家应该都不太陌生,出身阿里名声在外,能做动态服务发现、配置管理,非常好用的一个工具。然而这样的技术用的人越多面试被问的概率也就越大,如果只停留在使用层面,那面试可能要吃大亏。
Nacos配置中心交互模型是 push 还是 pull ?你应该这么回答
|
XML Dubbo Java
服务注册流程分析01
在填充该 ServiceBean 的时候会将对应的那个声明了注解的 bean 设置到 ServiceBean 中。 剩下的流程放置到下一篇文章中
118 0
|
Dubbo Java 应用服务中间件
服务注册流程分析02
上一篇文章中、我们已经知道 Dubbo 会额外注册 ServiceBean 到 Spring 容器中、因为需要借助这个 ServiceBean 注册到服务中心
145 0
|
容灾 数据中心
核心服务-注册中心(下)
核心服务-注册中心(下)
242 0
核心服务-注册中心(下)
|
存储
核心服务-注册中心(中)
核心服务-注册中心(中)
178 0
核心服务-注册中心(中)