前言
上篇文章 大篇幅把Spring全新一代类型转换器介绍完了,已经至少能够考个及格分。在介绍Spring众多内建的转换器里,我故意留下一个尾巴,放在本文专门撰文讲解。
为了让自己能在“拥挤的人潮中”显得不(更)一(突)样(出),A哥特意准备了这几个特殊的转换器助你破局,穿越拥挤的人潮,踏上Spring已为你制作好的高级赛道。
版本约定
- Spring Framework:5.3.1
- Spring Boot:2.4.0
正文
本文的焦点将集中在上文留下的4个类型转换器上。
- StreamConverter:将Stream流与集合/数组之间的转换,必要时转换元素类型
这三个比较特殊,属于“最后的”“兜底类”类型转换器:
- ObjectToObjectConverter:通用的将原对象转换为目标对象(通过工厂方法or构造器)
- IdToEntityConverter:本文重点。给个ID自动帮你兑换成一个Entity对象
- FallbackObjectToStringConverter:将任何对象调用toString()转化为String类型。当匹配不到任何转换器时,它用于兜底
默认转换器注册情况
Spring新一代类型转换内建了非常多的实现,这些在初始化阶段大都被默认注册进去。注册点在DefaultConversionService提供的一个static静态工具方法里:
穿过拥挤的人潮,Spring已为你制作好高级赛道static静态方法具有与实例无关性,我个人觉得把该static方法放在一个xxxUtils里统一管理会更好,放在具体某个组件类里反倒容易产生语义上的误导性
该静态方法用于注册全局的、默认的转换器们,从而让Spring有了基础的转换能力,进而完成绝大部分转换工作。为了方便记忆这个注册流程,我把它绘制成图供以你保存:
穿过拥挤的人潮,Spring已为你制作好高级赛道特别强调:转换器的注册顺序非常重要,这决定了通用转换器的匹配结果(谁在前,优先匹配谁)。
针对这幅图,你可能还会有疑问:
- JSR310转换器只看到TimeZone、ZoneId等转换,怎么没看见更为常用的LocalDate、LocalDateTime等这些类型转换呢?难道Spring默认是不支持的?答:当然不是。 这么常见的场景Spring怎能会不支持呢?不过与其说这是类型转换,倒不如说是格式化更合适。所以会在后3篇文章格式化章节在作为重中之重讲述
- 一般的Converter都见名之意,但StreamConverter有何作用呢?什么场景下会生效答:本文讲述
- 对于兜底的转换器,有何含义?这种极具通用性的转换器作用为何答:本文讲述
StreamConverter
用于实现集合/数组类型到Stream类型的互转,这从它支持的Set<ConvertiblePair> 集合也能看出来:
穿过拥挤的人潮,Spring已为你制作好高级赛道它支持的是双向的匹配规则:
穿过拥挤的人潮,Spring已为你制作好高级赛道代码示例
穿过拥挤的人潮,Spring已为你制作好高级赛道运行程序,输出:
穿过拥挤的人潮,Spring已为你制作好高级赛道关注点:底层依旧依赖DefaultConversionService完成元素与元素之间的转换。譬如本例Set -> Stream的实际步骤为:
穿过拥挤的人潮,Spring已为你制作好高级赛道也就是说任何集合/数组类型是先转换为中间状态的List,最终调用list.stream()转换为Stream流的;若是逆向转换先调用source.collect(Collectors.<Object>toList())把Stream转为List后,再转为具体的集合or数组类型。
说明:若source是数组类型,那底层实际使用的就是ArrayToCollectionConverter,注意举一反三
使用场景
StreamConverter它的访问权限是default,我们并不能直接使用到它。通过上面介绍可知Spring默认把它注册进了注册中心里,因此面向使用者我们直接使用转换服务接口ConversionService便可。
穿过拥挤的人潮,Spring已为你制作好高级赛道再次特别强调:流只能被读(消费)一次。
因为有了ConversionService提供的强大能力,我们就可以在基于Spring/Spring Boot做二次开发时使用它,提高系统的通用性和容错性。如:当方法入参是Stream类型时,你既可以传入Stream类型,也可以是Collection类型、数组类型,是不是瞬间逼格高了起来。
穿过拥挤的人潮,Spring已为你制作好高级赛道兜底转换器
按照添加转换器的顺序,Spring在最后添加了4个通用的转换器用于兜底,你可能平时并不关注它,但它实时就在发挥着它的作用。
穿过拥挤的人潮,Spring已为你制作好高级赛道ObjectToObjectConverter
将源对象转换为目标类型,非常的通用:Object -> Object:
穿过拥挤的人潮,Spring已为你制作好高级赛道是否能够处理的判断逻辑在于hasConversionMethodOrConstructor方法,直译为:是否有转换方法或者构造器。代码详细处理逻辑如下截图:
穿过拥挤的人潮,Spring已为你制作好高级赛道此部分逻辑可分为两个part来看:
- part1:从缓存中拿到Member,直接判断Member的可用性,可用的话迅速返回
- part2:若part1没有返回,就执行三部曲,尝试找到一个合适的Member,然后放进缓存内(若没有就返回null)
part1:快速返回流程
当不是首次进入处理时,会走快速返回流程。也就是第0步isApplicable判断逻辑,有这几个关注点:
- Member包括Method或者Constructor
- Method:若是static静态方法,要求方法的第1个入参类型必须是源类型sourceType;若不是static方法,则要求源类型sourceType必须是method.getDeclaringClass()的子类型/相同类型
- Constructor:要求构造器的第1个入参类型必须是源类型sourceType
创建目标对象的实例,此转换器支持两种方式:
- 通过工厂方法/实例方法创建实例(method.invoke(source))
- 通过构造器创建实例(ctor.newInstance(source))
以上case,在下面均会给出代码示例。
part2:三部曲流程
对于首次处理的转换,就会进入到详细的三部曲逻辑:通过反射尝试找到合适的Member用于创建目标实例,也就是上图的1、2、3步。
step1:determineToMethod,从sourceClass里找实例方法,对方法有如下要求:
- 方法名必须叫"to" + targetClass.getSimpleName(),如toPerson()
- 方法的访问权限必须是public
- 该方法的返回值必须是目标类型或其子类型
step2:determineFactoryMethod,找静态工厂方法,对方法有如下要求:
- 方法名必须为valueOf(sourceClass) 或者 of(sourceClass) 或者from(sourceClass)
- 方法的访问权限必须是public
step3:determineFactoryConstructor,找构造器,对构造器有如下要求:
- 存在一个参数,且参数类型是sourceClass类型的构造器
- 构造器的访问权限必须是public
特别值得注意的是:此转换器不支持Object.toString()方法将sourceType转换为java.lang.String。对于toString()支持,请使用下面介绍的更为兜底的FallbackObjectToStringConverter。
代码示例
- 实例方法
书写测试用例:
穿过拥挤的人潮,Spring已为你制作好高级赛道运行程序,输出:
穿过拥挤的人潮,Spring已为你制作好高级赛道- 静态工厂方法
测试用例完全同上,再次运行输出:
穿过拥挤的人潮,Spring已为你制作好高级赛道方法名可以为valueOf、of、from任意一种,这种命名方式几乎是业界不成文的规矩,所以遵守起来也会比较容易。但是:建议还是注释写好,防止别人重命名而导致转换生效。
- 构造器
基本同静态工厂方法示例,略
使用场景
基于本转换器可以完成任意对象 -> 任意对象的转换,只需要遵循方法名/构造器默认的一切约定即可,在我们平时开发书写转换层时是非常有帮助的,借助ConversionService可以解决这一类问题。
对于Object -> Object的转换,另外一种方式是自定义Converter<S,T>,然后注册到注册中心。至于到底选哪种合适,这就看具体应用场景喽,本文只是多给你一种选择
IdToEntityConverter
Id(S) --> Entity(T)。通过调用静态查找方法将实体ID兑换为实体对象。Entity里的该查找方法需要满足如下条件findEntityName:
- 必须是static静态方法
- 方法名必须为find + entityName。如Person类的话,那么方法名叫findPerson
- 方法参数列表必须为1个
- 返回值类型必须是Entity类型
说明:此方法可以不必是public,但建议用public。这样即使JVM的Security安全级别开启也能够正常访问
支持的转换Pair如下:ID和Entity都可以是任意类型,能转换就成
穿过拥挤的人潮,Spring已为你制作好高级赛道根据ID定位到Entity实体对象简直太太太常用了,运用好此转换器的提供的能力,或许能让你事半功倍,大大减少重复代码,写出更优雅、更简洁、更易于维护的代码。
代码示例
Entity实体:准备好符合条件的findXXX方法
穿过拥挤的人潮,Spring已为你制作好高级赛道应用IdToEntityConverter,书写示例代码:
穿过拥挤的人潮,Spring已为你制作好高级赛道运行程序,正常输出:
穿过拥挤的人潮,Spring已为你制作好高级赛道123----------------IdToEntityConverter---------------是否能够转换:truePerson(id=1, name=YourBatman-byFindPerson)
示例效果为:传入字符串类型的“1”,就能返回得到一个Person实例。可以看到,我们传入的是字符串类型的的1,而方法入参id类型实际为Long类型,但因为它们能完成String -> Long转换,因此最终还是能够得到一个Entity实例的。
使用场景
这个使用场景就比较多了,需要使用到findById()的地方都可以通过它来代替掉。如:
Controller层:
穿过拥挤的人潮,Spring已为你制作好高级赛道Tips:在Controller层这么写我并不建议,因为语义上没有对齐,势必在代码书写过程中带来一定的麻烦。
Service层:
穿过拥挤的人潮,Spring已为你制作好高级赛道Tips:在Service层这么写,我个人觉得还是OK的。用类型转换的领域设计思想代替了自上而下的过程编程思想。
FallbackObjectToStringConverter
通过简单的调用Object#toString()方法将任何支持的类型转换为String类型,它作为底层兜底。
穿过拥挤的人潮,Spring已为你制作好高级赛道1234@Overridepublic Set<ConvertiblePair> getConvertibleTypes() {returnCollections.singleton(new ConvertiblePair(Object.class, String.class));}
该转换器支持CharSequence/StringWriter等类型,以及所有ObjectToObjectConverter.hasConversionMethodOrConstructor(sourceClass, String.class)的类型。
说明:ObjectToObjectConverter不处理任何String类型的转换,原来都是交给它了
代码示例
略。
ObjectToOptionalConverter
将任意类型转换为一个Optional<T>类型,它作为最最最最最底部的兜底,稍微了解下即可。
代码示例
穿过拥挤的人潮,Spring已为你制作好高级赛道使用场景
一个典型的应用场景:在Controller中可传可不传的参数中,我们不仅可以通过@RequestParam(required = false) Long id来做,还是可以这么写:@RequestParam Optional<Long> id。
总结
本文是对上文介绍Spring全新一代类型转换机制的补充,因为关注得人较少,所以才有机会突破。
针对于Spring注册转换器,需要特别注意如下几点:
- 注册顺序很重要。先注册,先服务(若支持的话)
- 默认情况下,Spring会注册大量的内建转换器,从而支持String/数字类型转换、集合类型转换,这能解决协议层面的大部分转换问题。如Controller层,输入的是JSON字符串,可用自动被封装为数字类型、集合类型等等如@Value注入的是String类型,但也可以用数字、集合类型接收
对于复杂的对象 -> 对象类型的转换,一般需要你自定义转换器,或者参照本文的标准写法完成转换。总之:Spring提供的ConversionService专注于类型转换服务,是一个非常非常实用的API,特别是你正在做基于Spring二次开发的情况下。
当然喽,关于ConversionService这套机制还并未详细介绍,如何使用?如何运行?如何扩展?带着这三个问题,咱们下篇见。
网友评论