✍前言
你好,我是YourBatman。
上篇文章 大篇幅把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静态工具方法里:
static静态方法具有与实例无关性,我个人觉得把该static方法放在一个xxxUtils里统一管理会更好,放在具体某个组件类里反倒容易产生语义上的误导性
DefaultConversionService: public static void addDefaultConverters(ConverterRegistry converterRegistry) { // 1、添加标量转换器(和数字相关) addScalarConverters(converterRegistry); // 2、添加处理集合的转换器 addCollectionConverters(converterRegistry); // 3、添加对JSR310时间类型支持的转换器 converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry)); converterRegistry.addConverter(new StringToTimeZoneConverter()); converterRegistry.addConverter(new ZoneIdToTimeZoneConverter()); converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter()); // 4、添加兜底转换器(上面处理不了的全交给这几个哥们处理) converterRegistry.addConverter(new ObjectToObjectConverter()); converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry)); converterRegistry.addConverter(new FallbackObjectToStringConverter()); converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry)); } }
该静态方法用于注册全局的、默认的转换器们,从而让Spring有了基础的转换能力,进而完成绝大部分转换工作。为了方便记忆这个注册流程,我把它绘制成图供以你保存:
特别强调:转换器的注册顺序非常重要,这决定了通用转换器的匹配结果(谁在前,优先匹配谁)。
针对这幅图,你可能还会有疑问:
1.JSR310转换器只看到TimeZone、ZoneId等转换,怎么没看见更为常用的LocalDate、LocalDateTime等这些类型转换呢?难道Spring默认是不支持的?
1.答:当然不是。 这么常见的场景Spring怎能会不支持呢?不过与其说这是类型转换,倒不如说是格式化更合适。所以会在后3篇文章格式化章节在作为重中之重讲述
2.一般的Converter都见名之意,但StreamConverter有何作用呢?什么场景下会生效
1.答:本文讲述
3.对于兜底的转换器,有何含义?这种极具通用性的转换器作用为何
1.答:本文讲述
StreamConverter
用于实现集合/数组类型到Stream类型的互转,这从它支持的Set<ConvertiblePair> 集合也能看出来:
@Override public Set<ConvertiblePair> getConvertibleTypes() { Set<ConvertiblePair> convertiblePairs = new HashSet<ConvertiblePair>(); convertiblePairs.add(new ConvertiblePair(Stream.class, Collection.class)); convertiblePairs.add(new ConvertiblePair(Stream.class, Object[].class)); convertiblePairs.add(new ConvertiblePair(Collection.class, Stream.class)); convertiblePairs.add(new ConvertiblePair(Object[].class, Stream.class)); return convertiblePairs; }
它支持的是双向的匹配规则:
代码示例
/** * {@link StreamConverter} */ @Test public void test2() { System.out.println("----------------StreamConverter---------------"); ConditionalGenericConverter converter = new StreamConverter(new DefaultConversionService()); TypeDescriptor sourceTypeDesp = TypeDescriptor.valueOf(Set.class); TypeDescriptor targetTypeDesp = TypeDescriptor.valueOf(Stream.class); boolean matches = converter.matches(sourceTypeDesp, targetTypeDesp); System.out.println("是否能够转换:" + matches); // 执行转换 Object convert = converter.convert(Collections.singleton(1), sourceTypeDesp, targetTypeDesp); System.out.println(convert); System.out.println(Stream.class.isAssignableFrom(convert.getClass())); }
运行程序,输出:
----------------StreamConverter--------------- 是否能够转换:true java.util.stream.ReferencePipeline$Head@5a01ccaa true
关注点:底层依旧依赖DefaultConversionService完成元素与元素之间的转换。譬如本例Set -> Stream的实际步骤为:
也就是说任何集合/数组类型是先转换为中间状态的List,最终调用list.stream()转换为Stream流的;若是逆向转换先调用source.collect(Collectors.<Object>toList())把Stream转为List后,再转为具体的集合or数组类型。
说明:若source是数组类型,那底层实际使用的就是ArrayToCollectionConverter,注意举一反三
使用场景
StreamConverter它的访问权限是default,我们并不能直接使用到它。通过上面介绍可知Spring默认把它注册进了注册中心里,因此面向使用者我们直接使用转换服务接口ConversionService便可。
@Test public void test3() { System.out.println("----------------StreamConverter使用场景---------------"); ConversionService conversionService = new DefaultConversionService(); Stream<Integer> result = conversionService.convert(Collections.singleton(1), Stream.class); // 消费 result.forEach(System.out::println); // result.forEach(System.out::println); //stream has already been operated upon or closed }
运行程序,输出:
----------------StreamConverter使用场景--------------- 1
再次特别强调:流只能被读(消费)一次。
因为有了ConversionService提供的强大能力,我们就可以在基于Spring/Spring Boot做二次开发时使用它,提高系统的通用性和容错性。如:当方法入参是Stream类型时,你既可以传入Stream类型,也可以是Collection类型、数组类型,是不是瞬间逼格高了起来。