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

✍前言

你好,我是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类型、数组类型,是不是瞬间逼格高了起来。

兜底转换器

按照添加转换器的顺序,Spring在最后添加了4个通用的转换器用于兜底,你可能平时并不关注它,但它实时就在发挥着它的作用。

ObjectToObjectConverter

将源对象转换为目标类型,非常的通用:Object -> Object:

@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Object.class, Object.class));
}

虽然它支持的是Object -> Object,看似没有限制但其实是有约定条件的:

@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return (sourceType.getType() != targetType.getType() &&
hasConversionMethodOrConstructor(targetType.getType(), sourceType.getType()));
}

是否能够处理的判断逻辑在于hasConversionMethodOrConstructor方法,直译为:是否有转换方法或者构造器。代码详细处理逻辑如下截图:

此部分逻辑可分为两个part来看:

  • part1:从缓存中拿到Member,直接判断Member的可用性,可用的话迅速返回
  • part2:若part1没有返回,就执行三部曲,尝试找到一个合适的Member,然后放进缓存内(若没有就返回null)

part1:快速返回流程

当不是首次进入处理时,会走快速返回流程。也就是第0步isApplicable判断逻辑,有这几个关注点:

  1. Member包括Method或者Constructor
  2. Method:若是static静态方法,要求方法的第1个入参类型必须是源类型sourceType;若不是static方法,则要求源类型sourceType必须是method.getDeclaringClass()的子类型/相同类型
  3. Constructor:要求构造器的第1个入参类型必须是源类型sourceType

创建目标对象的实例,此转换器支持两种方式:

  1. 通过工厂方法/实例方法创建实例(method.invoke(source)
  2. 通过构造器创建实例(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

代码示例

  • 实例方法
// sourceClass
@Data
public class Customer {
private Long id;
private String address; public Person toPerson() {
Person person = new Person();
person.setId(getId());
person.setName("YourBatman-".concat(getAddress()));
return person;
} } // tartgetClass
@Data
public class Person {
private Long id;
private String name;
}

书写测试用例:

@Test
public void test4() {
System.out.println("----------------ObjectToObjectConverter---------------");
ConditionalGenericConverter converter = new ObjectToObjectConverter(); Customer customer = new Customer();
customer.setId(1L);
customer.setAddress("Peking"); Object convert = converter.convert(customer, TypeDescriptor.forObject(customer), TypeDescriptor.valueOf(Person.class));
System.out.println(convert); // ConversionService方式(实际使用方式)
ConversionService conversionService = new DefaultConversionService();
Person person = conversionService.convert(customer, Person.class);
System.out.println(person);
}

运行程序,输出:

----------------ObjectToObjectConverter---------------
Person(id=1, name=YourBatman-Peking)
Person(id=1, name=YourBatman-Peking)
  • 静态工厂方法
// sourceClass
@Data
public class Customer {
private Long id;
private String address;
} // targetClass
@Data
public class Person { private Long id;
private String name; /**
* 方法名称可以是:valueOf、of、from
*/
public static Person valueOf(Customer customer) {
Person person = new Person();
person.setId(customer.getId());
person.setName("YourBatman-".concat(customer.getAddress()));
return person;
}
}

测试用例完全同上,再次运行输出:

----------------ObjectToObjectConverter---------------
Person(id=1, name=YourBatman-Peking)
Person(id=1, name=YourBatman-Peking)

方法名可以为valueOf、of、from任意一种,这种命名方式几乎是业界不成文的规矩,所以遵守起来也会比较容易。但是:建议还是注释写好,防止别人重命名而导致转换生效。

  • 构造器

基本同静态工厂方法示例,略

使用场景

基于本转换器可以完成任意对象 -> 任意对象的转换,只需要遵循方法名/构造器默认的一切约定即可,在我们平时开发书写转换层时是非常有帮助的,借助ConversionService可以解决这一类问题。

对于Object -> Object的转换,另外一种方式是自定义Converter<S,T>,然后注册到注册中心。至于到底选哪种合适,这就看具体应用场景喽,本文只是多给你一种选择

IdToEntityConverter

Id(S) --> Entity(T)。通过调用静态查找方法将实体ID兑换为实体对象。Entity里的该查找方法需要满足如下条件find[EntityName]([IdType])

  1. 必须是static静态方法
  2. 方法名必须为find + entityName。如Person类的话,那么方法名叫findPerson
  3. 方法参数列表必须为1个
  4. 返回值类型必须是Entity类型

说明:此方法可以不必是public,但建议用public。这样即使JVM的Security安全级别开启也能够正常访问

支持的转换Pair如下:ID和Entity都可以是任意类型,能转换就成

@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Object.class, Object.class));
}

判断是否能执行准换的条件是:存在符合条件的find方法,且source可以转换为ID类型(注意source能转换成id类型就成,并非目标类型哦)

@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
Method finder = getFinder(targetType.getType());
return (finder != null
&& this.conversionService.canConvert(sourceType, TypeDescriptor.valueOf(finder.getParameterTypes()[0])));
}

根据ID定位到Entity实体对象简直太太太常用了,运用好此转换器的提供的能力,或许能让你事半功倍,大大减少重复代码,写出更优雅、更简洁、更易于维护的代码。

代码示例

Entity实体:准备好符合条件的findXXX方法

@Data
public class Person { private Long id;
private String name; /**
* 根据ID定位一个Person实例
*/
public static Person findPerson(Long id) {
// 一般根据id从数据库查,本处通过new来模拟
Person person = new Person();
person.setId(id);
person.setName("YourBatman-byFindPerson");
return person;
} }

应用IdToEntityConverter,书写示例代码:

@Test
public void test() {
System.out.println("----------------IdToEntityConverter---------------");
ConditionalGenericConverter converter = new IdToEntityConverter(new DefaultConversionService()); TypeDescriptor sourceTypeDesp = TypeDescriptor.valueOf(String.class);
TypeDescriptor targetTypeDesp = TypeDescriptor.valueOf(Person.class);
boolean matches = converter.matches(sourceTypeDesp, targetTypeDesp);
System.out.println("是否能够转换:" + matches); // 执行转换
Object convert = converter.convert("1", sourceTypeDesp, targetTypeDesp);
System.out.println(convert);
}

运行程序,正常输出:

----------------IdToEntityConverter---------------
是否能够转换:true
Person(id=1, name=YourBatman-byFindPerson)

示例效果为:传入字符串类型的“1”,就能返回得到一个Person实例。可以看到,我们传入的是字符串类型的的1,而方法入参id类型实际为Long类型,但因为它们能完成String -> Long转换,因此最终还是能够得到一个Entity实例的。

使用场景

这个使用场景就比较多了,需要使用到findById()的地方都可以通过它来代替掉。如:

Controller层:

@GetMapping("/ids/{id}")
public Object getById(@PathVariable Person id) {
return id;
} @GetMapping("/ids")
public Object getById(@RequestParam Person id) {
return id;
}

Tips:在Controller层这么写我并不建议,因为语义上没有对齐,势必在代码书写过程中带来一定的麻烦。

Service层:

@Autowired
private ConversionService conversionService; public Object findById(String id){
Person person = conversionService.convert(id, Person.class); return person;
}

Tips:在Service层这么写,我个人觉得还是OK的。用类型转换的领域设计思想代替了自上而下的过程编程思想。

FallbackObjectToStringConverter

通过简单的调用Object#toString()方法将任何支持的类型转换为String类型,它作为底层兜底。

@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Object.class, String.class));
}

该转换器支持CharSequence/StringWriter等类型,以及所有ObjectToObjectConverter.hasConversionMethodOrConstructor(sourceClass, String.class)的类型。

说明:ObjectToObjectConverter不处理任何String类型的转换,原来都是交给它了

代码示例

略。

ObjectToOptionalConverter

将任意类型转换为一个Optional<T>类型,它作为最最最最最底部的兜底,稍微了解下即可。

代码示例

@Test
public void test5() {
System.out.println("----------------ObjectToOptionalConverter---------------");
ConversionService conversionService = new DefaultConversionService();
Optional<Integer> result = conversionService.convert(Arrays.asList(2), Optional.class); System.out.println(result);
}

运行程序,输出:

----------------ObjectToOptionalConverter---------------
Optional[[2]]

使用场景

一个典型的应用场景:在Controller中可传可不传的参数中,我们不仅可以通过@RequestParam(required = false) Long id来做,还是可以这么写:@RequestParam Optional<Long> id

✍总结

本文是对上文介绍Spring全新一代类型转换机制的补充,因为关注得人较少,所以才有机会突破。

针对于Spring注册转换器,需要特别注意如下几点:

  1. 注册顺序很重要。先注册,先服务(若支持的话)
  2. 默认情况下,Spring会注册大量的内建转换器,从而支持String/数字类型转换、集合类型转换,这能解决协议层面的大部分转换问题。
    1. 如Controller层,输入的是JSON字符串,可用自动被封装为数字类型、集合类型等等
    2. 如@Value注入的是String类型,但也可以用数字、集合类型接收

对于复杂的对象 -> 对象类型的转换,一般需要你自定义转换器,或者参照本文的标准写法完成转换。总之:Spring提供的ConversionService专注于类型转换服务,是一个非常非常实用的API,特别是你正在做基于Spring二次开发的情况下。

当然喽,关于ConversionService这套机制还并未详细介绍,如何使用?如何运行?如何扩展?带着这三个问题,咱们下篇见。


推荐阅读

【Spring类型转换】系列:

【Jackson】系列:

【数据校验Bean Validation】系列:

【新特性】系列:

【程序人生】系列:

还有诸如【Spring配置类】【Spring-static】【Spring数据绑定】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】...更多原创专栏,关注BAT的乌托邦回复专栏二字即可全部获取,也可加我fsx1056342982,交个朋友。

有些已完结,有些连载中。我是A哥(YourBatman),咱们下期再见

5. 穿过拥挤的人潮,Spring已为你制作好高级赛道的更多相关文章

  1. 最新整理的spring面试题从基础到高级,干货满满

    最新整理的spring面试题从基础到高级,干货满满 前言: 收藏了一些关于Spring的面试题,一方面是为了准备找工作的时候看面试题,另一方面,通过面试题的方式加深一些自己的理论知识. spring ...

  2. Spring 已看 没用

    注解 @Autwired 依赖注入  作用: 自动按照类型注入.当使用注解注入属性时,set方法可以省略.它只能注入其他bean类型.当有多个类型匹配时,使用要注入的对象变量名称作为bean的id,在 ...

  3. 记录一次 @Autowired 无法注入( spring依赖正常 idea显示有spring已注入的图标)导致空指针异常的原因

    首先,参考 https://blog.csdn.net/weixin_40475523/article/details/81085990 然后发现 是因为我把自己的这个类加上了 @Service 注解 ...

  4. Spring Security研究(2)-高级web特性

    1, 添加 HTTP/HTTPS 信道安全 <http> <intercept-url pattern="/secure/**" access="ROL ...

  5. hi3531 SDK已编译文件系统制作jffs2文件系统镜像并解决问题 .

    一, 安装SDK 1.Hi3531 SDK包位置 在"Hi3531_V100R001***/01.software/board"目录下,您可以看到一个 Hi3531_SDK_Vx. ...

  6. Spring系列(三) Bean装配的高级技术

    profile 不同于maven的profile, spring的profile不需要重新打包, 同一个版本的包文件可以部署在不同环境的服务器上, 只需要激活对应的profile就可以切换到对应的环境 ...

  7. Spring使用笔记(三) 高级装配

    高级装配 一.环境与Profile 一)配置profile bean 环境的改变导致配置改变(需求:通过环境决定使用哪个bean),可以通过Spring的Profile解决. Profile可以在程序 ...

  8. 09 Spring框架 AOP (二) 高级用法

    上一篇文章我们主要讲了一点关于AOP编程,它的动态考虑程序的运行过程,和Spring中AOP的应用,前置通知,后置通知,环绕通知和异常通知,这些都是Spring中AOP最简单的用法,也是最常用的东西, ...

  9. hi3531 SDK已编译文件系统制作jffs2文件系统镜像并解决这个问题 .

    一, 安装SDK 1.Hi3531 SDK包位置 在"Hi3531_V100R001***/01.software/board"文件夹下,您能够看到一个 Hi3531_SDK_Vx ...

随机推荐

  1. guitar pro系列教程(十九):Guitar Pro添加音符之前我们要做什么?

    前面的章节我们已经讲了不少关于{cms_selflink page='index' text='Guitar Pro'}的功能之类的讲解,那一般我们在打谱之前要做的是什么呢,很多新手玩家,对这方面也是 ...

  2. day98:MoFang:服务端项目搭建

    目录 1.准备工作 2.创建项目启动文件manage.py 3.构建全局初始化函数并在函数内创建app应用对象 4.通过终端脚本启动项目 5.项目加载配置 6.数据库初始化 1.SQLAlchemy初 ...

  3. 一口气带你读懂80年IT发展史

    计算机的发展历史有多长?真正意义上的计算机诞生,距今也只有80多年的时间.80年,对于每一个人来说,是很长的时间,但对于整个历史来说,只是短短的一瞬间.这八十多年只是整段历史中的一粒尘埃罢了,但却对这 ...

  4. 集群--lvs

    快一个月没有更新博客了,最近一段时间在忙世界技能大赛网络系统系管理这个项目,没有太多的时间,我百忙之中更新一下.最近赛题中有说到集群这个,lvs这个东西(我也该学学这个了,一直停留在基础部分,是时候学 ...

  5. Mybatis log plugin 破解!!!

    前言 今天重新装了IDEA2020,顺带重装了一些插件,毕竟这些插件都是习惯一直在用,其中一款就是Mybatis Log plugin,按照往常的思路,在IDEA插件市场搜索安装,艹,眼睛一瞟,竟然收 ...

  6. OpenCV阈值处理函数threshold处理32位彩色图像的案例

    ☞ ░ 前往老猿Python博文目录 ░ 一.概述 openCV图像的阈值处理又称为二值化,之所以称为二值化,是它可以将一幅图转换为感兴趣的部分(前景)和不感兴趣的部分(背景).转换时,通常将某个值( ...

  7. Python的富比较方法__lt__、__gt__之间的关联关系分析

    Python的富比较方法包括__lt__.__gt__分别表示:小于.大于,对应的操作运算符为:"<".">".那么是否象普通数字运算一样,这两个方 ...

  8. PyQt(Python+Qt)学习随笔:clicked和clicked(bool)信号连接同名函数出现的问题

    在Qt中,控件中的clicked()信号和clicked(bool)信号是两个不同的信号,映射槽函数时,clicked()信号映射到的槽函数是不带参的,clicked(bool)信号映射到的槽函数是带 ...

  9. Node.js 应用---定时给自己发送邮件

    参照传智播客的视频所写代码. js代码: //引用superagent包,用于服务器发送http请求 const request = require('superagent'); //导入cheeri ...

  10. 《Eroico》关卡与操作设计

    操作设计: 没有给明操作教程,操作全靠蒙,只有改建的位置可以看到. 但游戏的难度并没有给玩家适应操作感,随着难度提升怪物血量增厚,但怪物并没有僵直英雄却有僵直.第一个小猫妖便给了玩家一个痛击. 方向键 ...