关于spring:4-上新了Spring全新一代类型转换机制

45次阅读

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

分享、成长,回绝浅藏辄止。关注公众号【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 转换
  4. 留神:就它没有泛型束缚,因为是通用

另外,还有一个条件接口 ConditionalConverter,可跟下面 3 个接口搭配组合应用,提供前置条件判断验证。
这套接口,解决了 PropertyEditor 做类型转换存在的 所有缺点,且具备十分高的灵活性和可扩展性。上面进入具体理解。

Converter

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

@FunctionalInterface
public interface Converter<S, T> {T convert(S source);}

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

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

代码示例
/**
 * Converter:1:1
 */@Test
public 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---------------
true
true
false
false
null
----------------StringToCharsetConverter---------------
UTF-8
UTF-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
 */@Test
public 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.Integer
class java.lang.Double
class 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. 若源汇合为空,或者指标汇合 没指定泛型,也不须要做转换动作
    3. 源汇合为空,还转换个啥
    4. 指标汇合没指定泛型,那就是 Object,因而能够接收所有,还转换个啥
  2. 若没有触发疾速返回。给指标创立一个 新汇合 ,而后把 source 的元素 一个一个的 放进新汇合里去,这里又分为两种解决 case

    1. 若新汇合(指标汇合)没有指定泛型类型(那就是 Object),就间接 putAll 即可,并不需要做类型转换
    1. 若新汇合(指标汇合指定了泛型类型),就 遍历 源汇合委托 conversionService.convert() 对元素一个一个的转
代码示例

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

@Test
public 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]
true
class 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 类型转换】系列:

  • 1. 揭秘 Spring 类型转换 – 框架设计的基石
  • 2. Spring 晚期类型转换,基于 PropertyEditor 实现
  • 3. 搞定出工,PropertyEditor 就到这

【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 的乌托邦 回复 专栏 二字即可全副获取,分享、成长,回绝浅藏辄止。

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

正文完
 0