写点什么

4. 上新了 Spring,全新一代类型转换机制

用户头像
YourBatman
关注
发布于: 2020 年 12 月 21 日
4. 上新了Spring,全新一代类型转换机制

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


✍前言

你好,我是 YourBatman。


上篇文章 介绍完了 Spring 类型转换早期使用的 PropertyEditor 详细介绍,关于 PropertyEditor 现存的资料其实还蛮少的,希望这几篇文章能弥补这块空白,贡献一份微薄之力。


如果你也吐槽过 PropertyEditor 不好用,那么本文将对会有帮助。Spring 自 3.0 版本开始自建了一套全新类型转换接口,这就是本文的主要内容,接下来逐步展开。

说明:Spring 自 3.0 后笑傲群雄,进入大一统。Java 从此步入 Spring 的时代


版本约定

  • Spring Framework:5.3.1

  • Spring Boot:2.4.0



✍正文

在了解新一代的转换接口之前,先思考一个问题:Spring 为何要自己造一套轮子呢? 一向秉承不重复造轮子原则的 Spring,不是迫不得已的话是不会去动他人奶酪的,毕竟互利共生才能长久。类型转换,作为 Spring 框架的基石,扮演着异常重要的角色,因此对其可扩展性、可维护性、高效性均有很高要求。


基于此,我们先来了解下 PropertyEditor 设计上到底有哪些缺陷/不足(不能满足现代化需求),让 Spring“被迫”走上了自建道路。


PropertyEditor 设计缺陷

前提说明:本文指出它的设计缺陷,只讨论把它当做类型转换器在转换场景下存在的一些缺陷。


  1. 职责不单一:该接口有非常多的方法,但只用到 2 个而已

  2. 类型不安全:setValue()方法入参是 Object,getValue()返回值是 Object,依赖于约定好的类型强转,不安全

  3. 线程不安全:依赖于 setValue()后 getValue(),实例是线程不安全的

  4. 语义不清晰:从语义上根本不能知道它是用于类型转换的组件

  5. 只能用于 String 类型:它只能进行 String <-> 其它类型的转换,而非更灵活的 Object <-> Object


PropertyEditor 存在这五宗“罪”,让 Spring 决定自己设计一套全新 API 用于专门服务于类型转换,这就是本文标题所述:新一代类型转换 Converter、ConverterFactory、GenericConverter。


关于 PropertyEditor 在 Spring 中的详情介绍,请参见文章:3. 搞定收工,PropertyEditor就到这


新一代类型转换

为了解决 PropertyEditor 作为类型转换方式的设计缺陷,Spring 3.0 版本重新设计了一套类型转换接口,有 3 个核心接口:

  1. Converter<S, T>:Source -> Target 类型转换接口,适用于 1:1 转换

  2. ConverterFactory<S, R>:Source -> R 类型转换接口,适用于 1:N 转换

  3. GenericConverter:更为通用的类型转换接口,适用于 N:N 转换

1. 注意:就它没有泛型约束,因为是通用


另外,还有一个条件接口ConditionalConverter,可跟上面 3 个接口搭配组合使用,提供前置条件判断验证。


这套接口,解决了 PropertyEditor 做类型转换存在的所有缺陷,且具有非常高的灵活性和可扩展性。下面进入详细了解。


Converter

将源类型 S 转换为目标类型 T。

@FunctionalInterfacepublic interface Converter<S, T> {	T convert(S source);}
复制代码

它是个函数式接口,接口定义非常简单。适合 1:1 转换场景:可以将任意类型 转换为 任意类型。它的实现类非常多,部分截图如下:



值得注意的是:几乎所有实现类的访问权限都是default/private,只有少数几个是 public 公开的,下面我用代码示例来“近距离”感受一下。


代码示例

/** * Converter:1:1 */@Testpublic void test() {    System.out.println("----------------StringToBooleanConverter---------------");    Converter<String, Boolean> converter = new StringToBooleanConverter();
// trueValues.add("true"); // trueValues.add("on"); // trueValues.add("yes"); // trueValues.add("1"); System.out.println(converter.convert("true")); System.out.println(converter.convert("1"));
// falseValues.add("false"); // falseValues.add("off"); // falseValues.add("no"); // falseValues.add("0"); System.out.println(converter.convert("FalSe")); System.out.println(converter.convert("off")); // 注意:空串返回的是null System.out.println(converter.convert(""));

System.out.println("----------------StringToCharsetConverter---------------"); Converter<String, Charset> converter2 = new StringToCharsetConverter(); // 中间横杠非必须,但强烈建议写上 不区分大小写 System.out.println(converter2.convert("uTf-8")); System.out.println(converter2.convert("utF8"));}
复制代码

运行程序,正常输出:

----------------StringToBooleanConverter---------------truetruefalsefalsenull----------------StringToCharsetConverter---------------UTF-8UTF-8
复制代码

说明:StringToBooleanConverter/StringToCharsetConverter 访问权限都是 default,外部不可直接使用。此处为了做示例用到一个小技巧 -> 将 Demo 的报名调整为和转换器的一样,这样就可以直接访问


关注点:true/on/yes/1 都能被正确转换为true的,且对于英文字母来说一般都不区分大小写,增加了容错性(包括 Charset 的转换)。


不足

Converter 用于解决 1:1 的任意类型转换,因此它必然存在一个不足:解决 1:N 转换问题需要写 N 遍,造成重复冗余代码。


譬如:输入是字符串,它可以转为任意数字类型,包括 byte、short、int、long、double 等等,如果用 Converter 来转换的话每个类型都得写个转换器,想想都麻烦有木有。


Spring 早早就考虑到了该场景,提供了相应的接口来处理,它就是ConverterFactory<S, R>


ConverterFactory

从名称上看它代表一个转换工厂:可以将对象 S 转换为 R 的所有子类型,从而形成 1:N 的关系。

该接口描述为 xxxFactory 是非常合适的,很好的表达了 1:N 的关系


public interface ConverterFactory<S, R> {	<T extends R> Converter<S, T> getConverter(Class<T> targetType);}
复制代码

它同样也是个函数式接口。该接口的实现类并不多,Spring Framework 共提供了 5 个内建实现(访问权限全部为 default):



以 StringToNumberConverterFactory 为例看看实现的套路:

final class StringToNumberConverterFactory implements ConverterFactory<String, Number> {
@Override public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) { return new StringToNumber<T>(targetType); }

// 私有内部类:实现Converter接口。用泛型边界约束一类类型 private static final class StringToNumber<T extends Number> implements Converter<String, T> {
private final Class<T> targetType; public StringToNumber(Class<T> targetType) { this.targetType = targetType; }
@Override public T convert(String source) { if (source.isEmpty()) { return null; } return NumberUtils.parseNumber(source, this.targetType); } }
}
复制代码

由点知面,ConverterFactory 作为 Converter 的工厂,对 Converter 进行包装,从而达到屏蔽内部实现的目的,对使用者友好,这不正是工厂模式的优点么,符合 xxxFactory 的语义。但你需要清除的是,工厂内部实现其实也是通过众多 if else 之类的去完成的,本质上并无差异。


代码示例

/** * ConverterFactory:1:N */@Testpublic void test2() {    System.out.println("----------------StringToNumberConverterFactory---------------");    ConverterFactory<String, Number> converterFactory = new StringToNumberConverterFactory();    // 注意:这里不能写基本数据类型。如int.class将抛错    System.out.println(converterFactory.getConverter(Integer.class).convert("1").getClass());    System.out.println(converterFactory.getConverter(Double.class).convert("1.1").getClass());    System.out.println(converterFactory.getConverter(Byte.class).convert("0x11").getClass());}
复制代码

运行程序,正常输出:

----------------StringToNumberConverterFactory---------------class java.lang.Integerclass java.lang.Doubleclass java.lang.Byte
复制代码

关注点:数字类型的字符串,是可以被转换为任意 Java 中的数字类型的,String(1) -> Number(N)。这便就是 ConverterFactory 的功劳,它能处理这一类转换问题。


不足

既然有了 1:1、1:N,自然就有 N:N。比如集合转换、数组转换、Map 到 Map 的转换等等,这些 N:N 的场景,就需要借助下一个接口 GenericConverter 来实现。


GenericConverter

它是一个通用的转换接口,用于在两个或多个类型之间进行转换。相较于前两个,这是最灵活的 SPI 转换器接口,但也是最复杂的。

public interface GenericConverter {
Set<ConvertiblePair> getConvertibleTypes(); Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); // 普通POJO final class ConvertiblePair { private final Class<?> sourceType; private final Class<?> targetType; }}
复制代码

该接口并非函数式接口,虽然方法不多但稍显复杂。现对出现的几个类型做简单介绍:

  • ConvertiblePair:维护 sourceType 和 targetType 的 POJO

- getConvertibleTypes()方法返回此 Pair 的 Set 集合。由此也能看出该转换器是可以支持 N:N 的(大多数情况下只写一对值而已,也有写多对的)

  • TypeDescriptor:类型描述。该类专用于 Spring 的类型转换场景,用于描述 from or to 的类型

- 比单独的 Type 类型强大,内部借助了 ResolvableType 来解决泛型议题


GenericConverter 的内置实现也比较多,部分截图如下:



ConditionalGenericConverter是 GenericConverter 和条件接口 ConditionalConverter 的组合,作用是在执行 GenericConverter 转换时增加一个前置条件判断方法。


转换器 | 描述 | 示例

-------- | ----- | -----

ArrayToArrayConverter | 数组转数组 Object[] -> Object[] | ["1","2"] -> [1,2]

ArrayToCollectionConverter | 数组转集合 Object[] -> Collection | 同上

CollectionToCollectionConverter | 数组转集合 Collection -> Collection | 同上

StringToCollectionConverter | 字符串转集合 String -> Collection | 1,2 -> [1,2]

StringToArrayConverter | 字符串转数组 String -> Array | 同上

MapToMapConverter | Map -> Map(需特别注意:key 和 value 都支持转换才行) | 略

CollectionToStringConverter | 集合转字符串 Collection -> String | [1,2] -> 1,2

ArrayToStringConverter | 委托给 CollectionToStringConverter 完成 | 同上

-- | -- | --

StreamConverter | 集合/数组 <-> Stream 互转 | 集合/数组类型 -> Stream 类型

IdToEntityConverter | ID->Entity 的转换 | 传入任意类型 ID -> 一个 Entity 实例

ObjectToObjectConverter | 很复杂的对象转换,任意对象之间 | obj -> obj

FallbackObjectToStringConverter | 上个转换器的兜底,调用 Obj.toString()转换 | obj -> String


说明:分割线下面的 4 个转换器比较特殊,字面上不好理解其实际作用,比较“高级”。它们如果能被运用在日常工作中可以事半功弎,因此放在在下篇文章专门给你介绍


下面以 CollectionToCollectionConverter 为例分析此转换器的“复杂”之处:

final class CollectionToCollectionConverter implements ConditionalGenericConverter {
private final ConversionService conversionService; public CollectionToCollectionConverter(ConversionService conversionService) { this.conversionService = conversionService; }
// 集合转集合:如String集合转为Integer集合 @Override public Set<ConvertiblePair> getConvertibleTypes() { return Collections.singleton(new ConvertiblePair(Collection.class, Collection.class)); }}
复制代码

这是唯一构造器,必须传入 ConversionService:元素与元素之间的转换是依赖于 conversionService 转换服务去完成的,最终完成集合到集合的转换。

CollectionToCollectionConverter:
@Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { return ConversionUtils.canConvertElements(sourceType.getElementTypeDescriptor(), targetType.getElementTypeDescriptor(), this.conversionService); }
复制代码

判断能否转换的依据:集合里的元素与元素之间是否能够转换,底层依赖于ConversionService#canConvert()这个 API 去完成判断。


接下来再看最复杂的转换方法:

CollectionToCollectionConverter:
@Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } Collection<?> sourceCollection = (Collection<?>) source;
// 判断:这些情况下,将不用执行后续转换动作了,直接返回即可 boolean copyRequired = !targetType.getType().isInstance(source); if (!copyRequired && sourceCollection.isEmpty()) { return source; } TypeDescriptor elementDesc = targetType.getElementTypeDescriptor(); if (elementDesc == null && !copyRequired) { return source; }
Collection<Object> target = CollectionFactory.createCollection(targetType.getType(), (elementDesc != null ? elementDesc.getType() : null), sourceCollection.size()); // 若目标类型没有指定泛型(没指定就是Object),不用遍历直接添加全部即可 if (elementDesc == null) { target.addAll(sourceCollection); } else { // 遍历:一个一个元素的转,时间复杂度还是蛮高的 // 元素转元素委托给conversionService去完成 for (Object sourceElement : sourceCollection) { Object targetElement = this.conversionService.convert(sourceElement, sourceType.elementTypeDescriptor(sourceElement), elementDesc); target.add(targetElement); if (sourceElement != targetElement) { copyRequired = true; } } }
return (copyRequired ? target : source); }
复制代码

该转换步骤稍微有点复杂,我帮你屡清楚后有这几个关键步骤:

  1. 快速返回:对于特殊情况,做快速返回处理

1. 若目标元素类型是元素类型的子类型(或相同),就没有转换的必要了(copyRequired = false)

2. 若源集合为空,或者目标集合没指定泛型,也不需要做转换动作

1. 源集合为空,还转换个啥

2. 目标集合没指定泛型,那就是 Object,因此可以接纳一切,还转换个啥

  1. 若没有触发快速返回。给目标创建一个新集合,然后把 source 的元素一个一个的放进新集合里去,这里又分为两种处理 case

1. 若新集合(目标集合)没有指定泛型类型(那就是 Object),就直接 putAll 即可,并不需要做类型转换

2. 若新集合(目标集合指定了泛型类型),就遍历源集合委托conversionService.convert()对元素一个一个的转


代码示例

以 CollectionToCollectionConverter 做示范:List<String> -> Set<Integer>

@Testpublic void test3() {    System.out.println("----------------CollectionToCollectionConverter---------------");    ConditionalGenericConverter conditionalGenericConverter = new CollectionToCollectionConverter(new DefaultConversionService());    // 将Collection转为Collection(注意:没有指定泛型类型哦)    System.out.println(conditionalGenericConverter.getConvertibleTypes());
List<String> sourceList = Arrays.asList("1", "2", "2", "3", "4"); TypeDescriptor sourceTypeDesp = TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)); TypeDescriptor targetTypeDesp = TypeDescriptor.collection(Set.class, TypeDescriptor.valueOf(Integer.class));
System.out.println(conditionalGenericConverter.matches(sourceTypeDesp, targetTypeDesp)); Object convert = conditionalGenericConverter.convert(sourceList, sourceTypeDesp, targetTypeDesp); System.out.println(convert.getClass()); System.out.println(convert);}
复制代码

运行程序,正常输出:

[java.util.Collection -> java.util.Collection]trueclass java.util.LinkedHashSet[1, 2, 3, 4]
复制代码

关注点:target 最终使用的是 LinkedHashSet 来存储,这结果和CollectionFactory#createCollection该 API 的实现逻辑是相关(Set 类型默认创建的是 LinkedHashSet 实例)。


不足

如果说它的优点是功能强大,能够处理复杂类型的转换(PropertyEditor 和前 2 个接口都只能转换单元素类型),那么缺点就是使用、自定义实现起来比较复杂。这不官方也给出了使用指导意见:在 Converter/ConverterFactory 接口能够满足条件的情况下,可不使用此接口就不使用。


ConditionalConverter

条件接口,@since 3.2。它可以为 Converter、GenericConverter、ConverterFactory 转换增加一个前置判断条件

public interface ConditionalConverter {	boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);}
复制代码

该接口的实现,截图如下:



可以看到,只有通用转换器 GenericConverter 和它进行了合体。这也很容易理解,作为通用的转换器,加个前置判断将更加严谨和更安全。对于专用的转换器如 Converter,它已明确规定了转换的类型,自然就不需要做前置判断喽。


✍总结

本文详细介绍了 Spring 新一代的类型转换接口,类型转换作为 Spring 的基石,其重要性可见一斑。


PropertyEditor 作为 Spring 早期使用“转换器”,因存在众多设计缺陷自 Spring 3.0 起被新一代转换接口所取代,主要有:

  1. Converter<S, T>:Source -> Target 类型转换接口,适用于 1:1 转换

  2. ConverterFactory<S, R>:Source -> R 类型转换接口,适用于 1:N 转换

  3. GenericConverter:更为通用的类型转换接口,适用于 N:N 转换


下篇文章将针对于 GenericConverter 的几个特殊实现撰专文为你讲解,你也知道做难事必有所得,做难事才有可能破局、破圈,欢迎保持关注。



✔✔✔推荐阅读✔✔✔

【Spring 类型转换】系列:


【Jackson】系列:


【数据校验 Bean Validation】系列:


【新特性】系列:


【程序人生】系列:


还有诸如【Spring 配置类】【Spring-static】【Spring 数据绑定】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】...更多原创专栏,关注BAT的乌托邦回复专栏二字即可全部获取,分享、成长,拒绝浅藏辄止。

有些专栏已完结,有些正在连载中,期待你的关注、共同进步


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

YourBatman

关注

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

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

评论

发布
暂无评论
4. 上新了Spring,全新一代类型转换机制