关于spring:9-细节见真章Formatter注册中心的设计很讨巧

9次阅读

共计 12310 个字符,预计需要花费 31 分钟才能阅读完成。

你好,我是 A 哥(YourBatman)。

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

Spring 内建有不少格式化器实现,同时对它们的治理、调度应用也有专门的组件负责,堪称若明若暗,职责清晰。本文将围绕 Formatter 注册核心 FormatterRegistry 开展,为你介绍 Spring 是如何优雅,奇妙的实现注册治理的。

学习编码是个 模拟 的过程,绝大多数时候你并不需要发明货色。当然这里指的模拟并非一般的CV 模式,而是取精髓为己所用,本文所述奇妙设计便是精髓所在,任君提取。

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

本文提纲

版本约定

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

阐明:最初一个接口办法除外,addFormatterForFieldAnnotation()和格式化注解相干,因为它十分重要,因而放在下文专门撰文解说

FormatterRegistry 接口的继承树如下:

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

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

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

FormattingConversionService

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

FormatterRegistryConverterRegistry 的子接口,而 ConverterRegistry 接口的所有办法均已由 GenericConversionService 全副实现了,所以能够通过继承它来 间接实现 ConverterRegistry 接口办法的实现,因而本类的继承构造是这样子的(请细品这个构造):

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

格式化器的注册治理 远没有 转换器那么简单,因为它是基于 下层适配 的思维,最终适配为 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 执行 理论的 转换逻辑

能够说 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 转换一把

能够看到它和 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,期待可能被解决喽:

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

// 第二个参数不传 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 援用,从而到这样的成果:

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

✍总结

本文以介绍 FormatterRegistry 接口为核心,重点钻研了此接口的实现形式,发现即便小小的一枚注册核心实现,也蕴藏有丰盛亮点供以学习、CV。

一般来说 ConversionService 天生具备 十分强悍的转换能力,因而理论状况是你若须要自定义一个 Printer/Parser 的话是大概率不须要本人再额定加个 Converter 转换器的,也就是说底层机制让你未然站在了“伟人”肩膀上。

♨本文思考题♨

看完了不肯定懂,看懂了不肯定会。来,文末 3 个思考题帮你复盘:

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

☀举荐浏览☀

  • 6. 抹平差别,对立类型转换服务 ConversionService
  • 7. JDK 拍了拍你:字符串拼接肯定记得用 MessageFormat#format
  • 8. 格式化器大一统 — Spring 的 Formatter 形象
  • ……

♚申明♚

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

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

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

正文完
 0