写点什么

6. 抹平差异,统一类型转换服务 ConversionService

用户头像
YourBatman
关注
发布于: 2020 年 12 月 28 日
6. 抹平差异,统一类型转换服务ConversionService

分享、成长,拒绝浅藏辄止。关注公众号【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
@Nullablepublic 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 接口使用起来应尽量的方便,重载多个是个有效途径。内部做适配、归口即可,用户至上


@Overridepublic void addConverter(Converter<?, ?> converter) {	// 获取泛型类型 -> 转为ConvertiblePair	ResolvableType[] typeInfo = getRequiredTypeInfo(converter.getClass(), Converter.class);	... 	// converter适配为GenericConverter添加	addConverter(new ConverterAdapter(converter, typeInfo[0], typeInfo[1]));}
@Overridepublic <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)));}
@Overridepublic void addConverter(GenericConverter converter) { this.converters.add(converter); invalidateCache();}
复制代码

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


查找

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

@Nullableprotected 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 接口的实现)。


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

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


转换
@Override@SuppressWarnings("unchecked")@Nullablepublic <T> T convert(@Nullable Object source, Class<T> targetType) {	return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType));}
@Override@Nullablepublic 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.1public 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 怎能会不支持呢?不过与其说这是类型转换,倒不如说是格式化更合适。所以放在该系列后几篇关于格式化章节中再做讲述

  1. 一般的 Converter 都见名之意,但 StreamConverter 有何作用呢?什么场景下会生效

1. 答:上文已讲述

  1. 对于兜底的转换器,有何含义?这种极具通用性的转换器作用为何

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),咱们下期见


发布于: 2020 年 12 月 28 日阅读数: 332
用户头像

YourBatman

关注

分享、成长,拒绝浅尝辄止。 2018.01.21 加入

分享、成长,拒绝浅尝辄止。公众号:BAT的乌托邦

评论

发布
暂无评论
6. 抹平差异,统一类型转换服务ConversionService