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

你好,我是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接口的实现类,实现其所有接口方法。
FormatterRegistry是ConverterRegistry的子接口,而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());
}
转换步骤分为两步:
- 若源类型(实际类型)不是该Printer类型的泛型类型的子类型的话,那就尝试使用conversionService转一趟
- 例如:Printer处理的是Number类型,但是你传入的是Person类型,这个时候conversionService就会发挥作用了
- 交由目标格式化器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类型的转换。一般来说,我们有两种途径来达到此目的:
- 直接方式:写一个Person转String的转换器,专用
- 缺点明显:多写一套代码
- 组合方式(推荐):如果目前已经有
Person -> Integer的了,那我们就组合起来用就非常方便啦,下面这个例子将告诉你使用这种方式完成“需求”- 缺点不明显:转换器一般要求与业务数据无关,因此通用性强,应最大可能的复用
下面示例二将帮你解决通过复用已有能力方式达到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
完美。
针对本例,有如下关注点:
- 使用
addFormatterForFieldType()方法注册了IntegerPrinter,并且明确指定了处理的类型:只处理Person类型- 说明:IntegerPrinter是可以注册多次分别用于处理不同类型。比如你依旧可以保留
formatterRegistry.addPrinter(new IntegerPrinter());来处理Integer -> String是木问题的
- 说明:IntegerPrinter是可以注册多次分别用于处理不同类型。比如你依旧可以保留
- 因为IntegerPrinter 实际上 只能转换
Integer -> String,因此还必须注册一个转换器,用于Person -> Integer桥接一下,这样就串起来了Person -> Integer -> String。只是外部看起来这些都是IntegerPrinter做的一样,特别工整 - 强调:addConverter()注册转换器时请务必不要使用lambda表达式代替输入,否则会失去泛型类型,导致出错
- 若想用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;
}
转换步骤分为两步:
- 通过Parser将String转换为指定的类型结果result(若失败,则抛出异常)
- 判断若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)
完美。
针对本例,有如下关注点:
- 使用
addFormatterForFieldType()方法注册了IntegerParser,并且明确指定了处理的类型,用于处理Person类型- 也就是说此IntegerParser专门用于转换目标类型为Person的属性
- 因为IntegerParser 实际上 只能转换
String -> Integer,因此还必须注册一个转换器,用于Integer -> Person桥接一下,这样就串起来了String -> Integer -> Person。外面看起来这些都是IntegerParser做的一样,非常工整 - 同样强调: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个思考题帮你复盘:
- FormatterRegistry作为注册中心只有添加方法,why?
- 示例中为何强调:addConverter()注册转换器时请务必不要使用lambda表达式代替输入,会有什么问题?
- 这种功能组合/桥接的巧妙设计方式,你脑中还能想到其它案例吗?
推荐阅读
- ...
- 6. 抹平差异,统一类型转换服务ConversionService
- 7. JDK拍了拍你:字符串拼接一定记得用MessageFormat#format
- 8. 格式化器大一统 -- Spring的Formatter抽象
- ......
♚声明♚
本文所属专栏:Java进阶,公号后台回复专栏名即可获取全部内容。
分享、成长,拒绝浅藏辄止。关注【BAT的乌托邦】,回复关键字专栏有Spring技术栈、中间件等小而美的原创专栏供以免费学习。本文已被 https://www.yourbatman.cn 收录。
本文是 A哥(YourBatman)原创文章,未经作者允许/开白不得转载,谢谢合作。

9. 细节见真章,Formatter注册中心的设计很讨巧的更多相关文章
- Dubbo-聊聊注册中心的设计
前言 Dubbo源码阅读分享系列文章,欢迎大家关注点赞 SPI实现部分 Dubbo-SPI机制 Dubbo-Adaptive实现原理 Dubbo-Activate实现原理 Dubbo SPI-Wrap ...
- Netty高性能组件——FastThreadLocal源码解析(细微处见真章)
1. 前言 netty自行封装了FastThreadLocal以替换jdk提供的ThreadLocal,结合封装的FastThreadLocalThread,在多线程环境下的变量提高了ThreadLo ...
- 使用Spring Cloud搭建服务注册中心
我们在之前的博客中已经介绍过阿里的分布式服务框架dubbo[Linux上安装Zookeeper以及一些注意事项][一个简单的案例带你入门Dubbo分布式框架],但是小伙伴们应该也看到了,阿里的dubb ...
- springboot+cloud 学习(一)高可用服务注册中心(Eureka)
先说说Eureka Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的.SpringClo ...
- springcloud搭建服务注册中心与服务发现
1.创建服务注册中心 创建一个普通的Spring Boot工程 首先我们需要创建一个普通的Spring Boot工程,命名为eureka-server,普通到什么程度呢?就是一个starter都不需要 ...
- rpc中的注册中心
使用模板模式,对注册中心进行设计,可以方便后续添加注册中心 模板抽象类,提供注册中心必要的方法. public abstract class ServiceRegistry { //这是一个模板的抽象 ...
- 注册中心ZooKeeper,Eureka,Consul,Nacos对比
简介 服务注册中心本质上是为了解耦服务提供者和服务消费者.对于任何一个微服务,原则上都应存在或者支持多个提供者,这是由微服务的分布式属性决定的.更进一步,为了支持弹性扩缩容特性,一个微服务的提供者 ...
- 从零开始实现简单 RPC 框架 4:注册中心
RPC 中服务消费端(Consumer) 需要请求服务提供方(Provider)的接口,必须要知道 Provider 的地址才能请求到. 那么,Consumer 要从哪里获取 Provider 的地址 ...
- 几个你不知道的dubbo注册中心细节
你会正确配置backup地址吗? 在配置dubbo注册中心时,一般会这样写 dubbo.registry.protocol=zookeeper dubbo.registry.address=127.0 ...
随机推荐
- windows隐藏文件
attrib命令用来显示或更改文件属性. ATTRIB [+R | -R] [+A | -A ] [+S | -S] [+H | -H] [[drive:] [path] filename] [/S ...
- 【Python】 requests 各种参数请求的方式
Python使用requests发送post请求 1.我们使用postman进行接口测试的时候,发现POST请求方式的编码有3种,具体的编码方式如下: A:application/x-www-form ...
- Spring @Scheduled Annotation
1.Overview 这里我们将会学习Spring @Scheduled 标签,了解它是如何配置,如何设置定时任务. 关于它的使用,有两点简单的规则需要记住: ※它的方法应该是一个void返回值类型 ...
- 2020年3月16日第一天,今天计划学习:K8S Kubeadm 1.14的完美部署
------------恢复内容开始------------ 一.部署docker 1. 部署docker容器虚拟化平台并配置docker的环境 下载新的yum配置文件 wget http://m ...
- 使用docker-maven-plugin打包
今天在部署的时候遇到点问题,总结一下,docker部署的步骤,如果对您有帮助,关注一下,就是对我最大的肯定, 谢谢! 微服务部署有两种方法: (1)手动部署:首先基于源码打包生成jar包(或war包) ...
- vue封装API接口
第一步: 首先引入axios 然后创建两个文件夹api和http http.js 里面的 1 import axios from 'axios';//引入axios 2 3 //环境的切换 开发环境( ...
- 设计模式——责任链(结合Tomcat中Filter机制)
设计模式:责任链模式 说责任链之前,先引入一个场景,假如规定学生请假小于或等于 2 天,班主任可以批准:小于或等于 7 天,系主任可以批准:小于或等于 10 天,院长可以批准:其他情况不予批准:以此为 ...
- PHP简单的计算位数的函数
一个简单的PHP计算位数的函数: 1 <?php 2 //一个简单的计算字符串有长度的函数 3 #开始定义函数 4 function count_digit($number){ 5 $count ...
- Maven大全
Maven 命令 mvn clean compile -Dmaven.test.skip=true 编译代码,检查代码安全性 Maven 注解 用maven管理库依赖,有个好处就是连同库的依赖的全部j ...
- python三大流程
一.三大流程 1. 顺序:按照顺序依次逐行执行代码的过程.自左向右,自上而下 2. 分支:程序按照不同的条件执行不同的处理代码的过程. 分支分为单分支,双分支,多分支 经常用到的分支结构是if语句 i ...