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设计缺陷
前提说明:本文指出它的设计缺陷,只讨论把它当做类型转换器在转换场景下存在的一些缺陷。
- 职责不单一:该接口有非常多的方法,但只用到2个而已
- 类型不安全:setValue()方法入参是Object,getValue()返回值是Object,依赖于约定好的类型强转,不安全
- 线程不安全:依赖于setValue()后getValue(),实例是线程不安全的
- 语义不清晰:从语义上根本不能知道它是用于类型转换的组件
- 只能用于String类型:它只能进行String <-> 其它类型的转换,而非更灵活的Object <-> Object
PropertyEditor存在这五宗“罪”,让Spring决定自己设计一套全新API用于专门服务于类型转换,这就是本文标题所述:新一代类型转换Converter、ConverterFactory、GenericConverter。
关于PropertyEditor在Spring中的详情介绍,请参见文章:3. 搞定收工,PropertyEditor就到这
新一代类型转换
为了解决PropertyEditor作为类型转换方式的设计缺陷,Spring 3.0版本重新设计了一套类型转换接口,有3个核心接口:
Converter<S, T>
:Source -> Target类型转换接口,适用于1:1转换ConverterFactory<S, R>
:Source -> R类型转换接口,适用于1:N转换GenericConverter
:更为通用的类型转换接口,适用于N:N转换- 注意:就它没有泛型约束,因为是通用
另外,还有一个条件接口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);
}
该转换步骤稍微有点复杂,我帮你屡清楚后有这几个关键步骤:
- 快速返回:对于特殊情况,做快速返回处理
- 若目标元素类型是源元素类型的子类型(或相同),就没有转换的必要了(copyRequired = false)
- 若源集合为空,或者目标集合没指定泛型,也不需要做转换动作
- 源集合为空,还转换个啥
- 目标集合没指定泛型,那就是Object,因此可以接纳一切,还转换个啥
- 若没有触发快速返回。给目标创建一个新集合,然后把source的元素一个一个的放进新集合里去,这里又分为两种处理case
- 若新集合(目标集合)没有指定泛型类型(那就是Object),就直接putAll即可,并不需要做类型转换
- 若新集合(目标集合指定了泛型类型),就遍历源集合委托
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起被新一代转换接口所取代,主要有:
Converter<S, T>
:Source -> Target类型转换接口,适用于1:1转换ConverterFactory<S, R>
:Source -> R类型转换接口,适用于1:N转换GenericConverter
:更为通用的类型转换接口,适用于N:N转换
下篇文章将针对于GenericConverter的几个特殊实现撰专文为你讲解,你也知道做难事必有所得,做难事才有可能破局、破圈,欢迎保持关注。
推荐阅读
【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的乌托邦
回复专栏
二字即可全部获取,分享、成长,拒绝浅藏辄止。
有些专栏已完结,有些正在连载中,期待你的关注、共同进步
4. 上新了Spring,全新一代类型转换机制的更多相关文章
- IntelliJ IDEA上创建maven Spring MVC项目
IntelliJ IDEA上创建Maven Spring MVC项目 各软件版本 利用maven骨架建立一个webapp 建立相应的目录 配置Maven和SpringMVC 配置Maven的pom.x ...
- Spring4.1新特性——Spring缓存框架增强(转)
目录 Spring4.1新特性——综述 Spring4.1新特性——Spring核心部分及其他 Spring4.1新特性——Spring缓存框架增强 Spring4.1新特性——异步调用和事件机制的异 ...
- 文件上传和下载(可批量上传)——Spring(三)
在文件上传和下载(可批量上传)——Spring(二)的基础上,发现了文件下载时,只有在Chrome浏览器下文件名正常显示,还有发布到服务器后,不能上传到指定的文件夹目录,如上传20160310.txt ...
- Spring4.1新特性——Spring MVC增强
目录 Spring4.1新特性——综述 Spring4.1新特性——Spring核心部分及其他 Spring4.1新特性——Spring缓存框架增强 Spring4.1新特性——异步调用和事件机制的异 ...
- 3.0.0 alpha 重磅发布!九大新功能、全新 UI 解锁调度系统新能力
2022 年 4 月 22 日,Apache DolphinScheduler 正式宣布 3.0.0 alpha 版本发布!此次版本升级迎来了自发版以来的最大变化,众多全新功能和特性为用户带来新的体验 ...
- 新建一个新的spring boot项目
简单几步,在Eclipse中创建一个新的spring Boot项目: 1.Eclipse中安装STS插件: Help -> Eclipse Marketplace... Search或选择&qu ...
- 在IIS上新发布的网站,样式与js资源文件加载不到(资源文件和网页同一个域名下)
在IIS上新发布的网站,网站能打开,但样式与js资源文件加载不到(资源文件和网页是同一个域名下,例如:网页www.xxx.com/index.aspx,图片www.xxx.com/pic.png). ...
- 思路 一般创建展示类时候 例如page类 会在网页上新增多个对应字段的隐藏域 用于存储值
思路 一般创建展示类时候 例如page类 会在网页上新增多个对应字段的隐藏域 用于存储值
- 【spring cloud】子模块module -->导入一个新的spring boot项目作为spring cloud的一个子模块微服务,怎么做/或者 每次导入一个新的spring boot项目,IDEA不识别子module,启动类无法启动/右下角没有蓝色图标
如题:导入一个新的spring boot项目作为spring cloud的一个子模块微服务,怎么做 或者说每次导入一个新的spring boot项目,IDEA不识别,启动类无法启动,怎么解决 下面分别 ...
随机推荐
- 关于iOS路径变化的解决方案
问题描述: 使用沙盒存储文件的时候,我们会保存文件的绝对路劲以便下次读取,但是发现一个现象,我们保存的文件,在第二次打开App去查找的时候,发现找不到了...... 查找原因: iOS8之后,苹果添加 ...
- 网络拓扑实例之RRPP单环(五)
组网图形 RRPP简介 在城域网和企业网的网络规划以及实际组网应用中大多会采用环网结构来提高网络的可靠性.采用环网结构的好处是:当环上任意一个节点或节点之间的链路发生故障,都可以将数据流量切换到备份链 ...
- yii2.0 实现城市联动效果
<script type="text/javascript"> function getcitytext(){ citytext = ''; $(".city ...
- Java8 Stream:2万字20个实例,玩转集合的筛选、归约、分组、聚合
点波关注不迷路,一键三连好运连连! 先贴上几个案例,水平高超的同学可以挑战一下: 从员工集合中筛选出salary大于8000的员工,并放置到新的集合里. 统计员工的最高薪资.平均薪资.薪资之和. 将员 ...
- flink:StreamGraph转换为JobGraph
1 转换基本流程 2 简单来看可以分为两部分: 第一部分是通过一些util.translator.generator等类将职责进行解耦.托管和分离,期间涉及FlinkPipelineTranslati ...
- leetcode 练习--反转链表
最近开始学习数据结构和算法的学习,也自然开始在 leetcode 上练习,所以每周大概会分享做过的leetcode 练习,尽量做到每天更新一道题目. 作为 leetcode 练习笔记的第一道题目,选择 ...
- 冲刺随笔——Day_Ten
这个作业属于哪个课程 软件工程 (福州大学至诚学院 - 计算机工程系) 这个作业要求在哪里 团队作业第五次--Alpha冲刺 这个作业的目标 团队进行Alpha冲刺 作业正文 正文 其他参考文献 无 ...
- 腾讯牛逼!终于开源了自家的 Tencent JDK——Kona!!.md
是的,继阿里 2019/03 开源基于 OpenJDK 的长期支持版本 Alibaba Dragonwell 之后,腾讯也发布了自家的开源免费的 JDK 版本--Tencent Kona,必须替小马哥 ...
- 写了一个类似与豆瓣的电影的flask小demo
先展示页面 基本的功能是都已经实现了,更多那个地方是可以点的.只不过视频上面还用的宏,哎呀,感觉麻烦.有多麻烦呢,需要先定义一个宏,然后进行引用.我们才能是用,以我的观点,还不如直接是一个循环完事.. ...
- jq中$(function(){})和js中window.onload区别
先看下执行代码: $(function(){ console.log("jq");}) $(function(){ console.log("jq1") ...