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

简介: 5. 穿过拥挤的人潮,Spring已为你制作好高级赛道(下)

兜底转换器


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


image.png


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方法,直译为:是否有转换方法或者构造器。代码详细处理逻辑如下截图:


image.png


此部分逻辑可分为两个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


image.png


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


通过工厂方法/实例方法创建实例(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。


代码示例


  • 实例方法


// 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,然后注册到注册中心。至于到底选哪种合适,这就看具体应用场景喽,本文只是多给你一种选择


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类型,它作为最最最最最底部的兜底,稍微了解下即可。


代码示例


@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 id。


✍总结


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


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


1.注册顺序很重要。先注册,先服务(若支持的话)


2.默认情况下,Spring会注册大量的内建转换器,从而支持String/数字类型转换、集合类型转换,这能解决协议层面的大部分转换问题。

  1. 如Controller层,输入的是JSON字符串,可用自动被封装为数字类型、集合类型等等
  2. 如@Value注入的是String类型,但也可以用数字、集合类型接收


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

相关文章
|
6月前
|
Java 开发者 Spring
Spring高级杂记
Spring高级杂记
40 0
|
6月前
|
存储 缓存 Java
【Spring原理高级进阶】有Redis为啥不用?深入剖析 Spring Cache:缓存的工作原理、缓存注解的使用方法与最佳实践
【Spring原理高级进阶】有Redis为啥不用?深入剖析 Spring Cache:缓存的工作原理、缓存注解的使用方法与最佳实践
|
6月前
|
前端开发 搜索推荐 Java
【Spring底层原理高级进阶】基于Spring Boot和Spring WebFlux的实时推荐系统的核心:响应式编程与 WebFlux 的颠覆性变革
【Spring底层原理高级进阶】基于Spring Boot和Spring WebFlux的实时推荐系统的核心:响应式编程与 WebFlux 的颠覆性变革
|
6月前
|
Java 测试技术 数据库连接
【Spring源码解读!底层原理高级进阶】【下】探寻Spring内部:BeanFactory和ApplicationContext实现原理揭秘✨
【Spring源码解读!底层原理高级进阶】【下】探寻Spring内部:BeanFactory和ApplicationContext实现原理揭秘✨
|
5月前
|
前端开发 Java 开发者
深入理解 Spring Boot 注解:核心功能与高级用法详解
深入理解 Spring Boot 注解:核心功能与高级用法详解
310 1
|
6月前
|
存储 安全 Java
第10章 Spring Security 的未来趋势与高级话题(2024 最新版)(下)
第10章 Spring Security 的未来趋势与高级话题(2024 最新版)
90 2
|
6月前
|
安全 Cloud Native Java
第10章 Spring Security 的未来趋势与高级话题(2024 最新版)(上)
第10章 Spring Security 的未来趋势与高级话题(2024 最新版)
77 2
|
6月前
|
数据采集 前端开发 Java
数据塑造:Spring MVC中@ModelAttribute的高级数据预处理技巧
数据塑造:Spring MVC中@ModelAttribute的高级数据预处理技巧
68 3
|
6月前
|
XML Java 数据格式
进阶注解探秘:深入Spring高级注解的精髓与实际运用
进阶注解探秘:深入Spring高级注解的精髓与实际运用
70 2
|
6月前
|
Java 关系型数据库 MySQL
高级对象装配:解析Spring创建复杂对象的秘诀
高级对象装配:解析Spring创建复杂对象的秘诀
58 0
高级对象装配:解析Spring创建复杂对象的秘诀