11

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

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzI0MTUwOTgyOQ%3D%3D&%3Bmid=2247491641&%3Bidx=1&%3Bsn=794c1fadf5d5144df83079a549d3b15f
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
iYfuMj.png!mobile

你好,我是A哥(YourBatman)。

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

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

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

这几天进入 小寒 天气,北京迎来最低-20℃,最高-11℃的冰点温度,外出注意保暖

本文提纲

IVBbyiY.png!mobile

版本约定

  • 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),绘制成图如下所示:

y63i6vf.png!mobile

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

FormatterRegistry接口的继承树如下:

jENrAb2.png!mobile

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

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

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

FormattingConversionService

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

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

rqQJZ32.png!mobile

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

jqi6Bz.png!mobile

格式化器的注册管理 远没有 转换器那么复杂,因为它是基于 上层适配 的思想,最终适配为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转一趟

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

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

UFNV7vA.png!mobile

可以说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 的目的。

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

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

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

@Test
public void test2() {
FormattingConversionService formattingConversionService = new FormattingConversionService();
FormatterRegistry formatterRegistry = formattingConversionService;
// 说明:这里不使用DefaultConversionService是为了避免默认注册的那些转换器对结果的“干扰”,不方便看效果
// ConversionService conversionService = new DefaultConversionService();
ConversionService conversionService = formattingConversionService;

// 注册格式化器
formatterRegistry.addFormatterForFieldType(Person.class, new IntegerPrinter(), null);
// 强调:此处绝不能使用lambda表达式代替,否则泛型类型丢失,结果将出错
formatterRegistry.addConverter(new Converter<Person, Integer>() {
@Override
public Integer convert(Person source) {
return source.getId();
}
});

// 最终均使用ConversionService统一提供服务转换
System.out.println(conversionService.canConvert(Person.class, String.class));
System.out.println(conversionService.convert(new Person(1, "YourBatman"), String.class));
}

运行程序,输出:

true
11

完美。

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

  1. 使用 addFormatterForFieldType() 方法注册了IntegerPrinter,并且 明确指定 了处理的类型:只处理Person类型
    1. 说明:IntegerPrinter是可以注册多次分别用于处理不同类型。比如你依旧可以保留 formatterRegistry.addPrinter(new IntegerPrinter()); 来处理Integer -> String是木问题的
  2. 因为IntegerPrinter 实际上 只能转换 Integer -> String ,因此还必须注册一个转换器,用于 Person -> Integer 桥接一下,这样就串起来了 Person -> Integer -> String 。只是外部 看起来 这些都是IntegerPrinter做的一样,特别工整
  3. 强调:addConverter()注册转换器时请务必不要使用lambda表达式代替输入,否则会失去泛型类型,导致出错

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

ParserConverter:Parser接口适配器

Parser<?> 适配为转换器,转换目标为 String -> fieldType

private static class ParserConverter implements GenericConverter {

private final Class<?> fieldType;
private final Parser<?> parser;
private final ConversionService conversionService;

... // 省略构造器

// String -> fieldType
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(String.class, this.fieldType));
}

}

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

ParserConverter:

@Override
@Nullable
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
// 空串当null处理
String text = (String) source;
if (!StringUtils.hasText(text)) {
return null;
}

...
Object result = this.parser.parse(text, LocaleContextHolder.getLocale());
...

// 解读/转换结果
TypeDescriptor resultType = TypeDescriptor.valueOf(result.getClass());
if (!resultType.isAssignableTo(targetType)) {
result = this.conversionService.convert(result, resultType, targetType);
}
return result;
}

转换步骤分为两步:

  1. 通过Parser将String转换为指定的类型结果result(若失败,则抛出异常)

  2. 判断若result属于 目标类型 的子类型,直接返回,否则调用ConversionService转换一把

JnM3Ybn.png!mobile

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

private static class IntegerParser implements Parser<Integer> {

@Override
public Integer parse(String text, Locale locale) throws ParseException {
return NumberUtils.parseNumber(text, Integer.class);
}
}

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

书写测试用例:

@Test
public void test3() {
FormattingConversionService formattingConversionService = new FormattingConversionService();
FormatterRegistry formatterRegistry = formattingConversionService;
ConversionService conversionService = formattingConversionService;

// 注册格式化器
formatterRegistry.addParser(new IntegerParser());

System.out.println(conversionService.canConvert(String.class, Integer.class));
System.out.println(conversionService.convert("1", Integer.class));
}

运行程序,输出:

true
1

完美。

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

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

@Test
public void test4() {
FormattingConversionService formattingConversionService = new FormattingConversionService();
FormatterRegistry formatterRegistry = formattingConversionService;
ConversionService conversionService = formattingConversionService;

// 注册格式化器
formatterRegistry.addFormatterForFieldType(Person.class, null, new IntegerParser());
formatterRegistry.addConverter(new Converter<Integer, Person>() {
@Override
public Person convert(Integer source) {
return new Person(source, "YourBatman");
}
});

System.out.println(conversionService.canConvert(String.class, Person.class));
System.out.println(conversionService.convert("1", Person.class));
}

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

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

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

IfyUv2n.png!mobile

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

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

再次运行程序,输出:

true
Person(id=1, name=YourBatman)

完美。

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

  1. 使用 addFormatterForFieldType() 方法注册了IntegerParser,并且 明确指定 了处理的类型,用于处理Person类型
    1. 也就是说此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(或T的子类型) -> String
  • ParserConverter:入口必须是String类型,出口类型也已确定,即 Parser<T> 的泛型类型,只能处理 String -> T(或T的子类型)

按既定“规则”,它俩的能力范围还是蛮受限的。Spring厉害的地方就在于此,可以巧妙的通过组合的方式,扩大现有组件的能力边界。比如本利中它就在PrinterConverter/ParserConverter里分别放入了ConversionService引用,从而到这样的效果:

ua22eqM.png!mobileConversionService

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

✍总结

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

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

☾本文思考题☽

看完了不一定看懂了,看懂了不一定记住了,记住了不一定掌握了。建议独立完成思考 or 文末留言讨论。本文思考/进阶内容:

  1. FormatterRegistry作为注册中心只有添加方法,why?

  2. 示例中为何强调:addConverter()注册转换器时请务必不要使用lambda表达式代替输入,会有什么问题?

  3. 这种功能组合/桥接的巧妙设计方式,你脑中还能想到其它案例吗?

本文所属专栏: Spring类型转换 ,公号后台回复专栏名即可获取全部内容。

分享、成长,拒绝浅藏辄止。关注公众号【 BAT的乌托邦 】,回复关键字 专栏 有Spring技术栈、中间件等小而美的 原创专栏 供以免费学习。本文已被 https://www.yourbatman.cn 收录。

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

✔推荐阅读✔

7rem63.gif!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK