写点什么

5. 穿过拥挤的人潮,Spring 已为你制作好高级赛道

用户头像
YourBatman
关注
发布于: 2020 年 12 月 22 日
5. 穿过拥挤的人潮,Spring已为你制作好高级赛道

分享、成长,拒绝浅藏辄止。关注公众号【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 篇文章格式化章节在作为重中之重讲述

  1. 一般的 Converter 都见名之意,但 StreamConverter 有何作用呢?什么场景下会生效

1. 答:本文讲述

  1. 对于兜底的转换器,有何含义?这种极具通用性的转换器作用为何

1. 答:本文讲述


StreamConverter

用于实现集合/数组类型到 Stream 类型的互转,这从它支持的Set<ConvertiblePair> 集合也能看出来:

@Overridepublic 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} */@Testpublic 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---------------是否能够转换:truejava.util.stream.ReferencePipeline$Head@5a01ccaatrue
复制代码

关注点:底层依旧依赖DefaultConversionService完成元素与元素之间的转换。譬如本例 Set -> Stream 的实际步骤为:



也就是说任何集合/数组类型是先转换为中间状态的 List,最终调用list.stream()转换为 Stream 流的;若是逆向转换先调用source.collect(Collectors.<Object>toList())把 Stream 转为 List 后,再转为具体的集合 or 数组类型。

说明:若 source 是数组类型,那底层实际使用的就是 ArrayToCollectionConverter,注意举一反三


使用场景

StreamConverter 它的访问权限是 default,我们并不能直接使用到它。通过上面介绍可知 Spring 默认把它注册进了注册中心里,因此面向使用者我们直接使用转换服务接口 ConversionService 便可。

@Testpublic 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:

@Overridepublic Set<ConvertiblePair> getConvertibleTypes() {	return Collections.singleton(new ConvertiblePair(Object.class, Object.class));}
复制代码

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

@Overridepublic 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@Datapublic 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@Datapublic class Person { private Long id; private String name;}
复制代码

书写测试用例:

@Testpublic 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@Datapublic class Customer {    private Long id;    private String address;}
// targetClass@Datapublic 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 里的该查找方法需要满足如下条件`findEntityName`:

  1. 必须是 static 静态方法

  2. 方法名必须为find + entityName。如 Person 类的话,那么方法名叫findPerson

  3. 方法参数列表必须为 1 个

  4. 返回值类型必须是 Entity 类型


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


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

@Overridepublic Set<ConvertiblePair> getConvertibleTypes() {	return Collections.singleton(new ConvertiblePair(Object.class, Object.class));}
复制代码

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

@Overridepublic 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 方法

@Datapublic 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,书写示例代码:

@Testpublic 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---------------是否能够转换:truePerson(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 层:

@Autowiredprivate ConversionService conversionService;
public Object findById(String id){ Person person = conversionService.convert(id, Person.class);
return person;}
复制代码

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


FallbackObjectToStringConverter

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

@Overridepublic Set<ConvertiblePair> getConvertibleTypes() {	return Collections.singleton(new ConvertiblePair(Object.class, String.class));}
复制代码

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

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


代码示例

略。


ObjectToOptionalConverter

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


代码示例

@Testpublic 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),咱们下期再见


发布于: 2020 年 12 月 22 日阅读数: 58
用户头像

YourBatman

关注

分享、成长,拒绝浅尝辄止。 2018.01.21 加入

分享、成长,拒绝浅尝辄止。公众号:BAT的乌托邦

评论

发布
暂无评论
5. 穿过拥挤的人潮,Spring已为你制作好高级赛道