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

简介: 6. 抹平差异,统一类型转换服务ConversionService(上)

✍前言


你好,我是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



image.png


✍正文


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


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



image.png


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);
}


它的继承树如下:


image.png


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);
}


它的继承树如下:


image.png


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


ConfigurableConversionService


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

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查找逻辑的分析,这个步骤就很简单了。绘制成图如下:


image.png



相关文章
|
2月前
|
安全 编译器 C++
【C/C++ 类型转换规则】一文了解C/C++ 中的类型转换规则,帮助你更好的编程
【C/C++ 类型转换规则】一文了解C/C++ 中的类型转换规则,帮助你更好的编程
19 0
|
7天前
|
JavaScript 前端开发 编译器
TypeScript中的高级类型:联合类型、交叉类型与条件类型深入解析
【4月更文挑战第23天】探索TypeScript的高级类型。这些特性增强类型系统的灵活性,提升代码质量和维护性。
|
3月前
|
JSON 前端开发 Java
统一数据返回格式 及 可能遇到的问题;统一异常处理
统一数据返回格式需要创建一个新类并使其实现ResponseBodyAdvice 接口并重写里面的方法,然后给当前类加上@ControllerAdvice注解。 实际应用时还有几个问题: 问题一:重复打包 问题二:ClassCastException: com.example.Spring_demo.Resp cannot be cast to java.lang.String 统一异常处理需要使用两个注解@ExceptionHandler@ControllerAdvice
38 2
统一数据返回格式 及 可能遇到的问题;统一异常处理
|
11月前
|
JavaScript 索引
数据类型及相互规则转换,我被问麻了!!!
数据类型及相互规则转换,我被问麻了!!!
87 2
|
存储 SQL BI
数据字典标准与统一的重要性(码表&枚举值)
关于“公共代码、编码、码表、枚举值”的含义,以及在软件开发中的应用场景。
286 0
数据字典标准与统一的重要性(码表&枚举值)
|
前端开发 程序员 C#
【C#】通过扩展对象的方式,对字符串等数据类型进行数据进一步处理
在本篇文章中,我们讲一起了解下对象扩展的使用 在实际项目开发中,对象扩展使用的场景还是挺多的,比如:需要对时间值进行再处理,或者字符串中的斜杠(/)转为反斜杠(\)
90 0
|
JavaScript 前端开发 算法
从规范的角度解析对象 — 原始值转换
从规范的角度解析对象 — 原始值转换
105 0
从规范的角度解析对象 — 原始值转换
typescript50-交叉类型和接口之间的类型说明
typescript50-交叉类型和接口之间的类型说明
65 0
typescript50-交叉类型和接口之间的类型说明
|
SQL Oracle 关系型数据库
ETL(九):同构关联(源限定符转换组件的使用)(一)
ETL(九):同构关联(源限定符转换组件的使用)(一)
ETL(九):同构关联(源限定符转换组件的使用)(一)
ETL(九):同构关联(源限定符转换组件的使用)(三)
ETL(九):同构关联(源限定符转换组件的使用)(三)
ETL(九):同构关联(源限定符转换组件的使用)(三)