SpringBoot实战:Spring如何找到对应转换器优雅使用枚举参数

目录
  • 找入口
    • 请求入口是DispatcherServlet
  • 查找转换器
  • Spring 如何查找转换器
  • 类型转换
  • 跟随源码找到自定义转换器工厂类和转换器类的实现逻辑
    • 无论是GET请求,还是传参式的POST请求(即Form模式)

找入口

请求入口是DispatcherServlet

所有的请求最终都会落到doDispatch方法中的

ha.handle(processedRequest, response, mappedHandler.getHandler())逻辑。

我们从这里出发,一层一层向里扒。

跟着代码深入,我们会找到

org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest的逻辑:

public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {

    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    if (logger.isTraceEnabled()) {
        logger.trace("Arguments: " + Arrays.toString(args));
    }
    return doInvoke(args);
}

可以看出,这里面通过getMethodArgumentValues方法处理参数,然后调用doInvoke方法获取返回值。

继续深入,能够找到

org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveArgument方法

这个方法就是解析参数的逻辑。

试想一下,如果是我们自己实现这段逻辑,会怎么做呢?

  1. 输入参数
  2. 找到目标参数
  3. 检查是否需要特殊转换逻辑
  4. 如果需要,进行转换
  5. 如果不需要,直接返回

获取输入参数的逻辑在

org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveName

单参数返回的是 String 类型,多参数返回 String 数组。

核心代码如下:

String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
    arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
}

所以说,无论我们的目标参数是什么,输入参数都是 String 类型或 String 数组

  • 然后 Spring 把它们转换为我们期望的类型。
  • 找到目标参数的逻辑在DispatcherServlet中,根据 uri 找到对应的 Controller 处理方法
  • 找到方法就找到了目标参数类型。
  • 接下来就是检查是否需要转换逻辑,也就是
  • org.springframework.validation.DataBinder#convertIfNecessary
  • 顾名思义,如果需要就转换,将字符串类型转换为目标类型。

在我们的例子中,就是将 String 转换为枚举值。

查找转换器

org.springframework.beans.TypeConverterDelegate#convertIfNecessary方法中

继续深扒找到这么一段逻辑:

if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
    try {
        return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
    }
    catch (ConversionFailedException ex) {
        // fallback to default conversion logic below
        conversionAttemptEx = ex;
    }
}

这段逻辑中,调用了

org.springframework.core.convert.support.GenericConversionService#canConvert方法

检查是否可转换,如果可以转换,将会执行类型转换逻辑。

检查是否可转换的本质就是检查是否能够找到对应的转换器。

  • 如果能找到,就用找到的转换器开始转换逻辑
  • 如果找不到,那就是不能转换,走其他逻辑。

我们可以看看查找转换器的代码

org.springframework.core.convert.support.GenericConversionService#getConverter

可以对我们自己写代码有一些启发:

private final Map<ConverterCacheKey, GenericConverter> converterCache = new ConcurrentReferenceHashMap<>(64);
protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
    ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType);
    GenericConverter converter = this.converterCache.get(key);
    if (converter != null) {
        return (converter != NO_MATCH ? converter : null);
    }
    converter = this.converters.find(sourceType, targetType);
    if (converter == null) {
        converter = getDefaultConverter(sourceType, targetType);
    }
    if (converter != null) {
        this.converterCache.put(key, converter);
        return converter;
    }
    this.converterCache.put(key, NO_MATCH);
    return null;
}

转换为伪代码就是:

  1. 根据参数类型和目标类型,构造缓存 key
  2. 根据缓存 key从缓存中查询转换器
  3. 如果能找到且不是 NO_MATCH,返回转换器;如果是 NO_MATCH,返回 null;如果未找到,继续
  4. 通过org.springframework.core.convert.support.GenericConversionService.Converters#find查询转换器
  5. 如果未找到,检查源类型和目标类型是否可以强转,也就是类型一致。如果是,返回 NoOpConverter,如果否,返回 null。
  6. 检查找到的转换器是否为 null,如果不是,将转换器加入到缓存中,返回该转换器
  7. 如果否,在缓存中添加 NO_MATCH 标识,返回 null

Spring 内部使用Map作为缓存,用来存储通用转换器接口GenericConverter,这个接口会是我们自定义转换器的包装类。

  • 我们还可以看到,转换器缓存用的是ConcurrentReferenceHashMap,这个类是线程安全的
  • 可以保证并发情况下,不会出现异常存储。但是getConverter方法没有使用同步逻辑。
  • 换句话说,并发请求时,可能存在性能损耗。

不过,对于 web 请求场景,并发损耗好过阻塞等待。

Spring 如何查找转换器

org.springframework.core.convert.support.GenericConversionService.Converters#find

就是找到对应转换器的核心逻辑:

private final Map<ConvertiblePair, ConvertersForPair> converters = new ConcurrentHashMap<>(256);
@Nullable
public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) {
    // Search the full type hierarchy
    List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType());
    List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType());
    for (Class<?> sourceCandidate : sourceCandidates) {
        for (Class<?> targetCandidate : targetCandidates) {
            ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate);
            GenericConverter converter = getRegisteredConverter(sourceType, targetType, convertiblePair);
            if (converter != null) {
                return converter;
            }
        }
    }
    return null;
}

@Nullable
private GenericConverter getRegisteredConverter(TypeDescriptor sourceType,
        TypeDescriptor targetType, ConvertiblePair convertiblePair) {
    // Check specifically registered converters
    ConvertersForPair convertersForPair = this.converters.get(convertiblePair);
    if (convertersForPair != null) {
        GenericConverter converter = convertersForPair.getConverter(sourceType, targetType);
        if (converter != null) {
            return converter;
        }
    }
    // Check ConditionalConverters for a dynamic match
    for (GenericConverter globalConverter : this.globalConverters) {
        if (((ConditionalConverter) globalConverter).matches(sourceType, targetType)) {
            return globalConverter;
        }
    }
    return null;
}

我们可以看到,Spring 是通过源类型和目标类型组合起来,查找对应的转换器。

而且,Spring 还通过getClassHierarchy方法,将源类型和目标类型的家族族谱全部列出来,用双层 for 循环遍历查找。

上面的代码中,还有一个matches方法,在这个方法里面,调用了ConverterFactory#getConverter方法

也就是用这个工厂方法,创建了指定类型的转换器。

private final ConverterFactory<Object, Object> converterFactory;
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
    boolean matches = true;
    if (this.converterFactory instanceof ConditionalConverter) {
        matches = ((ConditionalConverter) this.converterFactory).matches(sourceType, targetType);
    }
    if (matches) {
        Converter<?, ?> converter = this.converterFactory.getConverter(targetType.getType());
        if (converter instanceof ConditionalConverter) {
            matches = ((ConditionalConverter) converter).matches(sourceType, targetType);
        }
    }
    return matches;
}

类型转换

经过上面的逻辑,已经找到判断可以进行转换。

org.springframework.core.convert.support.GenericConversionService#convert

其核心逻辑就是已经找到对应的转换器了,下面就是转换逻辑

public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
    Assert.notNull(targetType, "Target type to convert to cannot be null");
    if (sourceType == null) {
        Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
        return handleResult(null, targetType, convertNullSource(null, targetType));
    }
    if (source != null && !sourceType.getObjectType().isInstance(source)) {
        throw new IllegalArgumentException("Source to convert from must be an instance of [" +
                sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
    }
    GenericConverter converter = getConverter(sourceType, targetType);
    if (converter != null) {
        Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
        return handleResult(sourceType, targetType, result);
    }
    return handleConverterNotFound(source, sourceType, targetType);
}

其中的GenericConverter converter = getConverter(sourceType, targetType)就是前文中getConverter方法。

  • 此处还是可以给我们编码上的一些借鉴的:getConverter方法在canConvert中调用了一次
    然后在后续真正转换的时候又调用一次这是参数转换逻辑
  • 我们该怎么优化这种同一请求内多次调用相同逻辑或者请求相同参数呢?
    那就是使用缓存。为了保持一次请求中前后两次数据的一致性和请求的高效,推荐使用内存缓存。

执行到这里,直接调用

ConversionUtils.invokeConverter(converter, source, sourceType, targetType)转换

其内部是使用

org.springframework.core.convert.support.GenericConversionService.ConverterFactoryAdapter#convert

方法,代码如下:

public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
    if (source == null) {
        return convertNullSource(sourceType, targetType);
    }
    return this.converterFactory.getConverter(targetType.getObjectType()).convert(source);
}

这里就是调用ConverterFactory工厂类构建转换器(即IdCodeToEnumConverterFactory类的getConverter方法)

然后调用转换器的conver方法(即IdCodeToEnumConverter类的convert方法),将输入参数转换为目标类型。

具体实现可以看一下实战篇中的代码,这里不做赘述。

至此,我们把整个路程通了下来。

跟随源码找到自定义转换器工厂类和转换器类的实现逻辑

无论是GET请求,还是传参式的POST请求(即Form模式)

这里需要强调一下的是,由于实战篇中我们用到的例子是简单参数的方式,也就是Controller的方法参数都是直接参数

  • 没有包装成对象。这样的话,Spring 是通过RequestParamMethodArgumentResolver处理参数。
  • 如果是包装成对象,会使用ModelAttributeMethodProcessor处理参数。这两个处理类中查找类型转换器逻辑都是相同的。
  • 都可以使用上面这种方式,实现枚举参数的类型转换。

但是 HTTP Body 方式却不行,为什么呢?

  • Spring 对于 body 参数是通过RequestResponseBodyMethodProcessor处理的
  • 其内部使用了MappingJackson2HttpMessageConverter转换器,逻辑完全不同。

所以,想要实现 body 的类型转换,还需要走另外一种方式,更多关于Spring对应转换器的枚举参数的资料请关注我们其它相关文章!

(0)

相关推荐

  • Spring MVC处理参数中的枚举类型通用实现方法

    前言 在开发的过程中,会涉及到大量的枚举类型数据,我们都知道,Springmvc本身能自动转换很多的数据类型,也支持你自定义转换类型,非常灵活. 本文主要介绍的是关于Spring MVC处理参数的枚举类型通用实现的相关内容,下面话不多说了,来一起看看详细的介绍吧 业务场景: 前端提交了枚举的一个属性value,想由spring来完成参数类型自动转换成对应的枚举. 比方有一个枚举 @AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @

  • MyBatis中如何优雅的使用枚举详解

    问题 本文主要给大家介绍的是关于MyBatis使用枚举的相关内容,我们在编码过程中,经常会遇到用某个数值来表示某种状态.类型或者阶段的情况,比如有这样一个枚举: public enum ComputerState { OPEN(10), //开启 CLOSE(11), //关闭 OFF_LINE(12), //离线 FAULT(200), //故障 UNKNOWN(255); //未知 private int code; ComputerState(int code) { this.code =

  • Spring boot JPA实现分页和枚举转换代码示例

    1.实现分页 方法一:使用Pageable 使用Pageable作为入参传入Repository,返回值用Page<T>接收 UserRepository package com.kinglead.demo.dao; ​ import com.kinglead.demo.entity.User; import org.springframework.data.jpa.repository.JpaRepository; ​ public interface UserRepository exte

  • SpringBoot使用validation-api实现对枚举类参数校验的方法

    前言 之前写了一个博客是关于使用SpringBoot使用validation-api实现参数校验,当时使用的注解都是validation-api自带的注解只能完成对空值.长度等简单的校验,在我们日常的使用当中会遇到对参数是否在枚举值类的校验,针对这种情况我们怎么来实现呢? SpringBoot使用validation-api实现参数校验可参考我的博客:SpringBoot使用validation-api实现参数校验 正文 SpringBoot使用validation-api实现对枚举类参数校验

  • springboot枚举类型传递的步骤

    目录 测试 Converter 灵活化 在本周写项目时,需要将枚举类型作为参数进行传递. 测试 首先先建立一个枚举类: public enum ScoreType { TOTAL_SCORE("总评成绩"), MIDDLE_SCORE("期中成绩"), FINAL_SCORE("期末成绩"); String des; // 描述 ScoreType(String des) { this.des = des; } public String get

  • SpringBoot实战:Spring如何找到对应转换器优雅使用枚举参数

    目录 找入口 请求入口是DispatcherServlet 查找转换器 Spring 如何查找转换器 类型转换 跟随源码找到自定义转换器工厂类和转换器类的实现逻辑 无论是GET请求,还是传参式的POST请求(即Form模式) 找入口 请求入口是DispatcherServlet 所有的请求最终都会落到doDispatch方法中的 ha.handle(processedRequest, response, mappedHandler.getHandler())逻辑. 我们从这里出发,一层一层向里扒

  • SpringBoot实战之实现结果的优雅响应案例详解

    今天说一下 Spring Boot 如何实现优雅的数据响应:统一的结果响应格式.简单的数据封装. 前提 无论系统规模大小,大部分 Spring Boot 项目是提供 Restful + json 接口,供前端或其他服务调用,格式统一规范,是程序猿彼此善待彼此的象征,也是减少联调挨骂的基本保障. 通常响应结果中需要包含业务状态码.响应描述.响应时间戳.响应内容,比如: { "code": 200, "desc": "查询成功", "tim

  • SpringBoot实战之高效使用枚举参数(原理篇)案例详解

    找入口 对 Spring 有一定基础的同学一定知道,请求入口是DispatcherServlet,所有的请求最终都会落到doDispatch方法中的ha.handle(processedRequest, response, mappedHandler.getHandler())逻辑.我们从这里出发,一层一层向里扒. 跟着代码深入,我们会找到org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest的

  • SpringBoot实战之处理异常案例详解

    前段时间写了一篇关于实现统一响应信息的博文,根据文中实战操作,能够解决正常响应的一致性,但想要实现优雅响应,还需要优雅的处理异常响应,所以有了这篇内容. 作为后台服务,能够正确的处理程序抛出的异常,并返回友好的异常信息是非常重要的,毕竟我们大部分代码都是为了 处理异常情况.而且,统一的异常响应,有助于客户端理解服务端响应,并作出正确处理,而且能够提升接口的服务质量. SpringBoot提供了异常的响应,可以通过/error请求查看效果: 这是从浏览器打开的场景,也就是请求头不包括content

  • SpringBoot集成Spring Security的方法

    至今Java能够如此的火爆Spring做出了很大的贡献,它的出现让Java程序的编写更为简单灵活,而Spring如今也形成了自己的生态圈,今天咱们探讨的是Spring旗下的一个款认证工具:SpringSecurity,如今认证框架主流"shiro"和"SpringSecurity",由于和Spring的无缝衔接,使用SpringSecurity的企业也越来越多. 1.Spring Security介绍 Spring security,是一个强大的和高度可定制的身份验

  • Springboot整合Spring Cloud Kubernetes读取ConfigMap支持自动刷新配置的教程

    1 前言 欢迎访问南瓜慢说 www.pkslow.com获取更多精彩文章! Docker & Kubernetes相关文章:容器技术 之前介绍了Spring Cloud Config的用法,但对于Kubernetes应用,可能会需要读取ConfigMap的配置,我们看看Springboot是如何方便地读取ConfigMap和Secret. 2 整合Spring Cloud Kubenetes Spring Cloud Kubernetes提供了Spring Cloud应用与Kubernetes服

  • SpringBoot使用Spring Security实现登录注销功能

    1.首先看下我的项目结构 我们逐个讲解 /** * 用户登录配置类 * @author Administrator * */ public class AdminUserDateils implements UserDetails { private static final long serialVersionUID = -1546619839676530441L; private transient YCAdmin yCAdmin; public AdminUserDateils() { }

  • JAVA入门教学之快速搭建基本的springboot(从spring boot到spring cloud)

    安装JDK https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html 使用的是jdk8,这里使用的是windows10 64位的操作系统,所以下载对应的jdk版本 点击链接会要你登录,登录以后才可以下载. 下载安装以后找到jdk的安装目录,我这里是C:\Program Files\Java\jdk1.8.0_211 配置JAVA_HOME,值就是你安装jdk的地址C:\Program Files\Java

  • Spring/Spring Boot 中优雅地做参数校验拒绝 if/else 参数校验

    数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据. 最普通的做法就像下面这样.我们通过 if/else 语句对请求的每一个参数一一校验. @RestController @RequestMapping("/api/person") public class PersonController { @PostMapping public ResponseEnti

  • SpringBoot实战教程之新手入门篇

    目录 1.SpringBoot简介 2.SpringBoot安装 2.1.基于Maven的安装 2.2.基于Gradle的安装 3.创建HelloWorld应用 4.代码结构 5.Configuration类 5.1.导入其它配置 6.自动化配置 6.1.逐渐替换自动化配置 6.2.禁止特定的自动化配置 7.使用@SpringBootApplication注解 总结 1. SpringBoot 简介 Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化 Spri

随机推荐