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

✍前言

你好,我是YourBatman。

通过前两篇文章的介绍已经非常熟悉Spirng 3.0全新一代的类型转换机制了,它提供的三种类型转换器(Converter、ConverterFactory、GenericConverter),分别可处理1:1、1:N、N:N的类型转换。按照Spring的设计习惯,必有一个注册中心来统一管理,负责它们的注册、删除等,它就是ConverterRegistry

对于ConverterRegistry在文首多说一句:我翻阅了很多博客文章介绍它时几乎无一例外的提到有查找的功能,但实际上是没有的。Spring设计此API接口并没有暴露其查找功能,选择把最为复杂的查找匹配逻辑私有化,目的是让开发者使可无需关心,细节之处充分体现了Spring团队API设计的卓越能力。

另外,内建的绝大多数转换器访问权限都是default/private,那么如何使用它们,以及屏蔽各种转换器的差异化呢?为此,Spring提供了一个统一类型转换服务,它就是ConversionService

版本约定

  • Spring Framework:5.3.1
  • Spring Boot:2.4.0

✍正文

ConverterRegistry和ConversionService的关系密不可分,前者为后者提供转换器管理支撑,后者面向使用者提供服务。本文涉及到的接口/类有:

  • ConverterRegistry:转换器注册中心。负责转换器的注册、删除
  • ConversionService统一的类型转换服务。属于面向开发者使用的门面接口
  • ConfigurableConversionService:上两个接口的组合接口
  • GenericConversionService:上个接口的实现,实现了注册管理、转换服务的几乎所有功能,是个实现类而非抽象类
  • DefaultConversionService:继承自GenericConversionService,在其基础上注册了一批默认转换器(Spring内建),从而具备基础转换能力,能解决日常绝大部分场景

ConverterRegistry

Spring 3.0引入的转换器注册中心,用于管理新一套的转换器们。

public interface ConverterRegistry {

	void addConverter(Converter<?, ?> converter);
<S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter);
void addConverter(GenericConverter converter);
void addConverterFactory(ConverterFactory<?, ?> factory); // 唯一移除方法:按照转换pair对来移除
void removeConvertible(Class<?> sourceType, Class<?> targetType);
}

它的继承树如下:

ConverterRegistry有子接口FormatterRegistry,它属于格式化器的范畴,故不放在本文讨论。但仍旧属于本系列专题内容,会在接下来的几篇内容里介入,敬请关注。

ConversionService

面向使用者的统一类型转换服务。换句话说:站在使用层面,你只需要知道ConversionService接口API的使用方式即可,并不需要关心其内部实现机制,可谓对使用者非常友好。

public interface ConversionService {

	boolean canConvert(Class<?> sourceType, Class<?> targetType);
boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType); <T> T convert(Object source, Class<T> targetType);
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

它的继承树如下:

可以看到ConversionService和ConverterRegistry的继承树殊途同归,都直接指向了ConfigurableConversionService这个分支,下面就对它进行介绍。

ConfigurableConversionService

ConversionServiceConverterRegistry的组合接口,自己并未新增任何接口方法。

public interface ConfigurableConversionService extends ConversionService, ConverterRegistry {

}

它的继承树可参考上图。接下来就来到此接口的直接实现类GenericConversionService。

GenericConversionService

ConfigurableConversionService接口提供了完整实现的实现类。换句话说:ConversionService和ConverterRegistry接口的功能均通过此类得到了实现,所以它是本文重点。

该类很有些值得学习的地方,可以细品,在我们自己设计程序时加以借鉴。

public class GenericConversionService implements ConfigurableConversionService {

	private final Converters converters = new Converters();
private final Map<ConverterCacheKey, GenericConverter> converterCache = new ConcurrentReferenceHashMap<ConverterCacheKey, GenericConverter>(64);
}

它用两个成员变量来管理转换器们,其中converterCache是缓存用于加速查找,因此更为重要的便是Converters喽。

Converters是GenericConversionService的内部类,用于管理(添加、删除、查找)转换器们。也就说对ConverterRegistry接口的实现最终是委托给它去完成的,它是整个转换服务正常work的内核,下面我们对它展开详细叙述。

1、内部类Converters

它管理所有转换器,包括添加、删除、查找。

GenericConversionService:

	// 内部类
private static class Converters {
private final Set<GenericConverter> globalConverters = new LinkedHashSet<GenericConverter>();
private final Map<ConvertiblePair, ConvertersForPair> converters = new LinkedHashMap<ConvertiblePair, ConvertersForPair>(36);
}

说明:这里使用的集合/Map均为LinkedHashXXX,都是有序的(存入顺序和遍历取出顺序保持一致)

用这两个集合/Map存储着注册进来的转换器们,他们的作用分别是:

  • globalConverters:存取通用的转换器,并不限定转换类型,一般用于兜底
  • converters:指定了类型对,对应的转换器的映射关系。
    • ConvertiblePair:表示一对,包含sourceType和targetType
    • ConvertersForPair:这一对对应的转换器(因为能处理一对的可能存在多个转换器),内部使用一个双端队列Deque来存储,保证顺序
      • 小细节:Spring 5之前使用LinkedList,之后使用Deque(实际为ArrayDeque)存储
final class ConvertiblePair {
private final Class<?> sourceType;
private final Class<?> targetType;
}
private static class ConvertersForPair {
private final Deque<GenericConverter> converters = new ArrayDeque<>(1);
}
添加add
public void add(GenericConverter converter) {
Set<ConvertiblePair> convertibleTypes = converter.getConvertibleTypes();
if (convertibleTypes == null) {
... // 放进globalConverters里
} else {
... // 放进converters里(若支持多组pair就放多个key)
}
}

在此之前需要了解个前提:对于三种转换器Converter、ConverterFactory、GenericConverter在添加到Converters之前都统一被适配为了GenericConverter,这样做的目的是方便统一管理。对应的两个适配器是ConverterAdapter和ConverterFactoryAdapter,它俩都是ConditionalGenericConverter的内部类。

添加的逻辑被我用伪代码简化后其实非常简单,无非就是一个非此即彼的关系而已:

  • 若转换器没有指定处理的类型对,就放进全局转换器列表里,用于兜底
  • 若转换器有指定处理的类型对(可能还是多个),就放进converters里,后面查找时使用
删除remove
public void remove(Class<?> sourceType, Class<?> targetType) {
this.converters.remove(new ConvertiblePair(sourceType, targetType));
}

移除逻辑非常非常的简单,这得益于添加时候做了统一适配的抽象

查找find
@Nullable
public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) {
// 找到该类型的类层次接口(父类 + 接口),注意:结果是有序列表
List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType());
List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType()); // 双重遍历
for (Class<?> sourceCandidate : sourceCandidates) {
for (Class<?> targetCandidate : targetCandidates) {
ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate);
... // 从converters、globalConverters里匹配到一个合适转换器后立马返回
}
}
return null;
}

查找逻辑也并不复杂,有两个关键点需要关注:

  • getClassHierarchy(class):获取该类型的类层次(父类 + 接口),注意:结果List是有序的List

    • 也就是说转换器支持的类型若是父类/接口,那么也能够处理器子类
  • 根据convertiblePair匹配转换器:优先匹配专用的converters,然后才是globalConverters。若都没匹配上返回null

2、管理转换器(ConverterRegistry)

了解了Converters之后再来看GenericConversionService是如何管理转换器,就如鱼得水,一目了然了。

添加

为了方便使用者调用,ConverterRegistry接口提供了三个添加方法,这里一一给与实现。

说明:暴露给调用者使用的API接口使用起来应尽量的方便,重载多个是个有效途径。内部做适配、归口即可,用户至上

@Override
public void addConverter(Converter<?, ?> converter) {
// 获取泛型类型 -> 转为ConvertiblePair
ResolvableType[] typeInfo = getRequiredTypeInfo(converter.getClass(), Converter.class);
...
// converter适配为GenericConverter添加
addConverter(new ConverterAdapter(converter, typeInfo[0], typeInfo[1]));
} @Override
public <S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter) {
addConverter(new ConverterAdapter(converter, ResolvableType.forClass(sourceType), ResolvableType.forClass(targetType)));
} @Override
public void addConverter(GenericConverter converter) {
this.converters.add(converter);
invalidateCache();
}

前两个方法都会调用到第三个方法上,每调用一次addConverter()方法都会清空缓存,也就是converterCache.clear()。所以动态添加转换器对性能是有损的,因此使用时候需稍加注意一些。

查找

ConverterRegistry接口并未直接提供查找方法,而只是在实现类内部做了实现。提供一个钩子方法用于查找给定sourceType/targetType对的转换器。

@Nullable
protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType); // 1、查缓存
GenericConverter converter = this.converterCache.get(key);
if (converter != null) {
... // 返回结果
} // 2、去converters里查找
converter = this.converters.find(sourceType, targetType);
if (converter == null) {
// 若还没有匹配的,就返回默认结果
// 默认结果是NoOpConverter -> 什么都不做
converter = getDefaultConverter(sourceType, targetType);
} ... // 把结果装进缓存converterCache里
return null;
}

有了对Converters查找逻辑的分析,这个步骤就很简单了。绘制成图如下:

3、转换功能(ConversionService)

上半部分介绍完GenericConversionService对转换器管理部分的实现(对ConverterRegistry接口的实现),接下来就看看它是如何实现转换功能的(对ConversionService接口的实现)。

判断
@Override
public boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType) {
return canConvert((sourceType != null ? TypeDescriptor.valueOf(sourceType) : null), TypeDescriptor.valueOf(targetType));
} @Override
public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
if (sourceType == null) {
return true;
} // 查找/匹配对应的转换器
GenericConverter converter = getConverter(sourceType, targetType);
return (converter != null);
}

能否执行转换判断的唯一标准:能否匹配到可用于转换的转换器。而这个查找匹配逻辑,稍稍抬头往上就能看到。

转换
@Override
@SuppressWarnings("unchecked")
@Nullable
public <T> T convert(@Nullable Object source, Class<T> targetType) {
return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType));
} @Override
@Nullable
public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
if (sourceType == null) {
return handleResult(null, targetType, convertNullSource(null, targetType));
}
// 校验:source必须是sourceType的实例
if (source != null && !sourceType.getObjectType().isInstance(source)) {
throw new IllegalArgumentException("Source to convert from must be an instance of [" + sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
} // ============拿到转换器,执行转换============
GenericConverter converter = getConverter(sourceType, targetType);
if (converter != null) {
Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
return handleResult(sourceType, targetType, result);
}
// 若没进行canConvert的判断直接调动,可能出现此种状况:一般抛出ConverterNotFoundException异常
return handleConverterNotFound(source, sourceType, targetType);
}

同样的,执行转换的逻辑很简单,非常好理解的两个步骤:

  1. 查找匹配到一个合适的转换器(查找匹配的逻辑同上)
  2. 拿到此转换器执行转换converter.convert(...)

说明:其余代码均为一些判断、校验、容错,并非核心,本文给与适当忽略。

GenericConversionService实现了转换器管理、转换服务的所有功能,是可以直接面向开发者使用的。但是开发者使用时可能并不知道需要注册哪些转换器来保证程序正常运转,Spring并不能要求开发者知晓其内建实现。基于此,Spring在3.1又提供了一个默认实现DefaultConversionService,它对使用者更友好。

DefaultConversionService

Spirng容器默认使用的转换服务实现,继承自GenericConversionService,在其基础行只做了一件事:构造时添加内建的默认转换器。从而天然具备有了基本的类型转换能力,适用于不同的环境。如:xml解析、@Value解析、http协议参数自动转换等等。

小细节:它并非Spring 3.0就有,而是Spring 3.1新推出的API

// @since 3.1
public class DefaultConversionService extends GenericConversionService { // 唯一构造器
public DefaultConversionService() {
addDefaultConverters(this);
} }

本类核心代码就这一个构造器,构造器内就这一句代码:addDefaultConverters(this)。接下来需要关注Spring默认情况下给我们“安装”了哪些转换器呢?也就是了解下addDefaultConverters(this)这个静态方法

默认注册的转换器们

// public的静态方法,注意是public的访问权限
public static void addDefaultConverters(ConverterRegistry converterRegistry) {
addScalarConverters(converterRegistry);
addCollectionConverters(converterRegistry); converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new StringToTimeZoneConverter());
converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter()); converterRegistry.addConverter(new ObjectToObjectConverter());
converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new FallbackObjectToStringConverter());
converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
}

该静态方法用于注册全局的、默认的转换器们,从而让Spring有了基础的转换能力,进而完成绝大部分转换工作。为了方便记忆这个注册流程,我把它绘制成图供以你保存:

特别强调:转换器的注册顺序非常重要,这决定了通用转换器的匹配结果(谁在前,优先匹配谁,first win)。

针对这幅图,你可能还会有如下疑问:

  1. JSR310转换器只看到TimeZone、ZoneId等转换,怎么没看见更为常用的LocalDate、LocalDateTime等这些类型转换呢?难道Spring默认是不支持的?

    1. 答:当然不是。 这么常见的场景Spring怎能会不支持呢?不过与其说这是类型转换,倒不如说是格式化更合适。所以放在该系列后几篇关于格式化章节中再做讲述
  2. 一般的Converter都见名之意,但StreamConverter有何作用呢?什么场景下会生效
    1. 答:上文已讲述
  3. 对于兜底的转换器,有何含义?这种极具通用性的转换器作用为何
    1. 答:上文已讲述

最后,需要特别强调的是:它是一个静态方法,并且还是public的访问权限,且不仅仅只有本类调用。实际上,DefaultConversionService仅仅只做了这一件事,所以任何地方只要调用了该静态方法都能达到前者相同的效果,使用上可谓给与了较大的灵活性。比如Spring Boot环境下不是使用DefaultConversionService而是ApplicationConversionService,后者是对FormattingConversionService扩展,这个话题放在后面详解。

Spring Boot在web环境默认向容易注册了一个WebConversionService,因此你有需要可直接@Autowired使用

ConversionServiceFactoryBean

顾名思义,它是用于产生ConversionService类型转换服务的工厂Bean,为了方便和Spring容器整合而使用。

public class ConversionServiceFactoryBean implements FactoryBean<ConversionService>, InitializingBean {

	@Nullable
private Set<?> converters;
@Nullable
private GenericConversionService conversionService; public void setConverters(Set<?> converters) {
this.converters = converters;
}
@Override
public void afterPropertiesSet() {
// 使用的是默认实现哦
this.conversionService = new DefaultConversionService();
ConversionServiceFactory.registerConverters(this.converters, this.conversionService);
} @Override
@Nullable
public ConversionService getObject() {
return this.conversionService;
}
...
}

这里只有两个信息量需要关注:

  1. 使用的是DefaultConversionService,因此那一大串的内建转换器们都会被添加进来的
  2. 自定义转换器可以通过setConverters()方法添加进来
    1. 值得注意的是方法入参是Set<?>并没有明确泛型类型,因此那三种转换器(1:1/1:N/N:N)你是都可以添加.

✍总结

通读本文过后,相信能够给与你这个感觉:曾经望而却步的Spring类型转换服务ConversionService,其实也不过如此嘛。通篇我用了多个简单字眼来说明,因为拆开之后,无一高复杂度知识点。

迎难而上是积攒涨薪底气和勇气的途径,况且某些知识点其实并不难,所以我觉得从性价比角度来看这类内容是非常划算的,你pick到了麽?

正所谓类型转换和格式化属于两组近义词,在Spring体系中也经常交织在一起使用,有种傻傻分不清楚之感。从下篇文章起进入到本系列关于Formatter格式化器知识的梳理,什么日期格式化、@DateTimeFormat、@NumberFormat都将帮你捋清楚喽,有兴趣者可保持持续关注。


推荐阅读

【Spring类型转换】系列:

【Jackson】系列:

【数据校验Bean Validation】系列:

【新特性】系列:

【程序人生】系列:

还有诸如【Spring配置类】【Spring-static】【Spring数据绑定】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】...更多原创专栏,关注BAT的乌托邦回复专栏二字即可全部获取,也可加我fsx1056342982,交个朋友。

有些已完结,有些连载中。我是A哥(YourBatman),咱们下期见

6. 抹平差异,统一类型转换服务ConversionService的更多相关文章

  1. 奔跑吧,OpenStack现场分享:超融合架构如何抹平物理硬件差异?

    转自:https://www.ustack.com/blog/moping/ “通过引入OpenStack这一中间层,实现了云平台统一的管理调度支配向上交付,解决了业务的灵活性问题.但是在抹平下层物理 ...

  2. AMQ学习笔记 - 15. 实践方案:基于ActiveMQ的统一日志服务

    概述 以ActiveMQ + Log4j + Spring的技术组合,实现基于消息队列的统一日志服务. 参考:Spring+Log4j+ActiveMQ实现远程记录日志——实战+分析 与参考文章的比较 ...

  3. 基于ActiveMQ的统一日志服务

    概述 以ActiveMQ + Log4j + Spring的技术组合,实现基于消息队列的统一日志服务. 参考:Spring+Log4j+ActiveMQ实现远程记录日志——实战+分析 与参考文章的比较 ...

  4. CoSky-Mirror 就像一个镜子放在 Nacos、CoSky 中间,构建一个统一的服务发现平台

    CoSky 基于 Redis 的服务治理平台(服务注册/发现 & 配置中心) Consul + Sky = CoSky CoSky 是一个轻量级.低成本的服务注册.服务发现. 配置服务 SDK ...

  5. 【SpringCloud构建微服务系列】使用Spring Cloud Config统一管理服务配置

    一.为什么要统一管理微服务配置 对于传统的单体应用而言,常使用配置文件来管理所有配置,比如SpringBoot的application.yml文件,但是在微服务架构中全部手动修改的话很麻烦而且不易维护 ...

  6. 2 weekend110的zookeeper的原理、特性、数据模型、节点、角色、顺序号、读写机制、保证、API接口、ACL、选举、 + 应用场景:统一命名服务、配置管理、集群管理、共享锁、队列管理

    在hadoop生态圈里,很多地方都需zookeeper. 启动的时候,都是普通的server,但在启动过程中,通过一个特定的选举机制,选出一个leader. 只运行在一台服务器上,适合测试环境:Zoo ...

  7. 统一ID服务

    代码已经修改 调用方式 为restful请求  或者 feign请求  请参考 wiki:   http://192.168.120.46:8090/display/peixun/akucun+Gui ...

  8. 在centos7.6上部署前后端分离项目Nginx反向代理vue.js2.6+Tornado5.1.1,使用supervisor统一管理服务

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_102 这一次使用vue.js+tornado的组合来部署前后端分离的web项目,vue.js不用说了,前端当红炸子鸡,泛用性非常广 ...

  9. Spring Cloud 2020.0.0正式发布,再见了Netflix

    目录 ✍前言 版本约定 ✍正文 Spring Cloud版本管理 与Spring Boot版本对应关系 当前支持的版本 阻断式升级(不向下兼容) 1.再见了,Netflix Netflix组件替代方案 ...

随机推荐

  1. 一万三千字的HashMap面试必问知识点详解

    目录 概论 Hasmap 的继承关系 hashmap 的原理 解决Hash冲突的方法 开放定址法 再哈希法 链地址法 建立公共溢出区 hashmap 最终的形态 Hashmap 的返回值 HashMa ...

  2. DevOps Workshop | 代码管理入门:基于代码扫描实现团队效率提升

    CODING「DevOps Workshop 学习营地」持续火热进行中! 在这里,你可以轻松实践 DevOps 全流程.体验高效的云端开发.赢取精美礼品--第二期大奖「戴尔 U2718Q 显示器」将于 ...

  3. eNSP VLAN划分基础配置及Trunk接口

    跨交换机实现VLAN通信拓扑图: 一.配置PC机 ip 并测试相互能否ping通 PC名称 IP 子网掩码 网关 PC1 10.1.1.1 255.255.255.0 10.1.1.254 PC2 1 ...

  4. LeetCode 037 Sudoku Solver

    题目要求:Sudoku Solver Write a program to solve a Sudoku puzzle by filling the empty cells. Empty cells ...

  5. markdown详细

    在写博客时,想设置字体 颜色参照[(https://blog.csdn.net/wo919191/article/details/84249531)] 图片参照 背景色: gray

  6. tensorflow 小记——如何对张量做任意行求和,得到新tensor(一种方法:列表生成式)

    希望实现图片上的功能 import tensorflow as tfa = tf.range(10,dtype=float)b = aa = tf.reshape(a,[-1,1])a = tf.ti ...

  7. 2020武汉dotNET俱乐部分享交流圆满结束

    经过长达2个多月的准备,终于在12月5日圆满的举行了武汉首届dotNET俱乐部线下分享交流活动.我们一共精心准备了3个目前比较热门的主题,分别如下: Jason分享的<ABP开发框架的扩展应用& ...

  8. 性能测试平台nGrinder

    ngrinder简介 ngrinder是NAVER(韩国大型互联网公司)开源的性能测试工具平台,直接部署成web服务,平台化,支持多用户使用,可扩展性好,可自定义plugin插件 开源地址:http: ...

  9. 深入解析volatile关键字

    前言 很高兴遇见你~ 欢迎阅读我的文章. volatile关键字在Java多线程编程编程中起的作用是很大的,合理使用可以减少很多的线程安全问题.但其实可以发现使用这个关键字的开发者其实很少,包括我自己 ...

  10. Codeforces Edu Round 59 A-D

    A. Digits Sequence Dividing 注意特殊情况,在\(n = 2\)时除非\(str[1] >= str[2]\),否则可以把第一个数划分下来,剩下的数直接当成一组,一定满 ...