关于spring:6-抹平差异统一类型转换服务ConversionService

57次阅读

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

分享、成长,回绝浅藏辄止。关注公众号【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 类型转换】系列:

  • 1. 揭秘 Spring 类型转换 – 框架设计的基石
  • 2. Spring 晚期类型转换,基于 PropertyEditor 实现
  • 3. 搞定出工,PropertyEditor 就到这
  • 4. 上新了 Spring,全新一代类型转换机制
  • 5. 穿过拥挤的人潮,Spring 已为你制作好高级赛道

【Jackson】系列:

  • 1. 初识 Jackson — 世界上最好的 JSON 库
  • 2. 妈呀,Jackson 原来是这样写 JSON 的
  • 3. 懂了这些,方敢在简历上说会用 Jackson 写 JSON
  • 4. JSON 字符串是如何被解析的?JsonParser 理解一下
  • 5. JsonFactory 工厂而已,还蛮有料,这是我没想到的
  • 6. 二十不惑,ObjectMapper 应用也不再蛊惑
  • 7. Jackson 用树模型解决 JSON 是必备技能,不信你看

【数据校验 Bean Validation】系列:

  • 1. 不吹不擂,第一篇就能晋升你对 Bean Validation 数据校验的认知
  • 2. Bean Validation 申明式校验办法的参数、返回值
  • 3. 站在应用层面,Bean Validation 这些标准接口你须要烂熟于胸
  • 4. Validator 校验器的五大外围组件,一个都不能少
  • 5. Bean Validation 申明式验证四大级别:字段、属性、容器元素、类
  • 6. 自定义容器类型元素验证,类级别验证(多字段联结验证)

【新个性】系列:

  • IntelliJ IDEA 2020.3 正式公布,年度最初一个版本很讲武德
  • IntelliJ IDEA 2020.2 正式公布,诸多亮点总有几款能助你提效
  • [IntelliJ IDEA 2020.1 正式公布,你要的 Almost 都在这!]()
  • Spring Framework 5.3.0 正式公布,在云原生路上持续发力
  • Spring Boot 2.4.0 正式公布,全新的配置文件加载机制(不向下兼容)
  • Spring 扭转版本号命名规定:此举对非英语国家很敌对
  • JDK15 正式公布,划时代的 ZGC 同时发表转正

【程序人生】系列:

  • 蚂蚁金服上市了,我不想致力了
  • 如果程序员和产品经理都用凡尔赛文学对话 ……
  • 程序人生 | 春风得意马蹄疾,一日看尽长安花

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

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

正文完
 0