带你解密:SpringBoot接口到底支持多少种类型的参数?

路人甲Java

共 9083字,需浏览 19分钟

 · 2021-11-12

大家好,我是路人,这是 SpringMVC 系列第 22 篇。

目前所有系列文章已同步至个人博客(itsoku.com),个人博客已改版,更方便翻阅,点击文末左边的阅读原文直达博客。

1、来看 2 个好问题

大家在使用 SpringMVC 或者 SpringBoot 开发接口的时候,有没有思考过下面这 2 个问题

  • 接口的参数到底支持哪些类型?有什么规律可循么?
  • 接口参数的值是从哪里来的呢?

说实话,这 2 个问题非常关键,搞懂原理之后,开发接口将得心应手,今天就带大家从原理上来搞懂这俩问题。

2、SpringMVC 处理请求大概的过程

step1、接受请求

step2、根据请求信息找到能够处理请求的控制器方法

step3、解析请求,组装控制器方法需要的参数的值

step4、通过反射调用送控制器方法

step5、响应结果等

咱们重点来看 step3 参数值组装这个过程。

3、解析处理器方法参数的值

解析参数需要的值,SpringMVC 中专门有个接口来干这个事情,这个接口就是:HandlerMethodArgumentResolver,中文称呼:处理器放放参数解析器,说白了就是解析请求得到 Controller 方法的参数的值。

3.1、处理器方法参数解析器:HandlerMethodArgumentResolver 接口

public interface HandlerMethodArgumentResolver {

 /**
  * 判断当前解析器是否支持解析parameter这种参数
  * parameter:方法参数信息
  */

 boolean supportsParameter(MethodParameter parameter);

 /**
  * 解析参数,得到参数对应的值
  */

 @Nullable
 Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
   NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory)
 throws Exception
;

}

3.1、解析参数值的过程

SpringMVC 中会配置多个 HandlerMethodArgumentResolver,组成一个 HandlerMethodArgumentResolver 列表,用这个列表来解析参数得到参数需要的值,相当于 2 嵌套 for 循环,简化版的过程如下:

//1.得到控制器参数列表
List parameterList;
//2.参数解析器列表
List handlerMethodArgumentResolverList;
//控制器方法参数
Object[] handlerMethodArgs = new Object[parameterList.size()];
int paramIndex = 0;
//遍历参数列表
for (MethodParameter parameter : parameterList) {
    //遍历处理器方法参数解析器列表
    for (HandlerMethodArgumentResolver resolver : handlerMethodArgumentResolverList) {
        if (resolver.supportsParameter(parameter)) {
            handlerMethodArgs[paramIndex++] = resolver.resolveArgument(parameter, webRequest, binderFactory);
            break;
        }
    }
}

解析参数源码的位置:

org.springframework.web.method.support.InvocableHandlerMethod#getMethodArgumentValues

4、常见的 HandlerMethodArgumentResolver

大家可以在InvocableHandlerMethod#getMethodArgumentValues这个位置设置断点,可以详细了解参数解析的过程,debug 中我们可以在这看到 SpringMVC 中默认情况下注册了这么多解析器,如下图:

如下表,列出了一些常见的,以及这些参数解析器能够解析的参数的特点及类型

实现类支持的参数类型参数值
RequestParamMethodArgumentResolver参数需使用@RequestParam 标注,且 name 属性有值,参数通常为普通类型、Map 类型;或 MultipartFile、Part 类型,或 MultipartFile、Part 这两种类型的集合、数组请求参数
RequestParamMapMethodArgumentResolver参数需使用@RequestParam 标注,且 name 属性没有子,参数为 Map 类型;参数的值从 request 的参数中取值,Map 中的 key 对应参数名称,value 对应参数的值请求参数
PathVariableMapMethodArgumentResolver参数需使用@PathVariable 标注,参数通常为普通类型从 url 中取值
RequestHeaderMethodArgumentResolver参数需使用@RequestHeader 标注,参数通常为 Map、MultiValueMap、HttpHeaders 类型请求头
ServletCookieValueMethodArgumentResolver参数需使用@CookieValue 标注,参数为普通类型或者 Cookie 类型cookie
ModelMethodProcessor参数为 Model 类型,控制器中可以调用 model.addAttribute 想模型中放数据,最终这些数据都会通过 request.setAttribute 复制到 request 中来源于 SpringMVC 容器
MapMethodProcessor参数为 Map 类型,值同 ModelMethodProcessor来源于 SpringMVC 容器
ModelAttributeMethodProcessor参数需要使用@ModelAttribute 标注Model.getAttribute
ServletRequestMethodArgumentResolver参数类型为 WebRequest、ServletRequest、MultipartRequest、HttpSession、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneIdServlet 容器中的 request
ServletResponseMethodArgumentResolver参数类型是 ServletResponse、OutputStream、WriterServlet 容器中的 response
ModelMethodProcessor参数为 org.springframework.ui.Model 类型来源于 SpringMVC 容器
RequestAttributeMethodArgumentResolver参数需使用@RequestAttributerequest.getAttribute
SessionAttributeMethodArgumentResolver参数需使用@SessionAttributesession.getAttribute
ExpressionValueMethodArgumentResolver参数需使用@Value 标注从 Spring 配置中取值
ServletModelAttributeMethodProcessor支持为我们自定义的 javabean 赋值-
RequestResponseBodyMethodProcessor参数需使用@RequestBody 标注http 请求中的 body
HttpEntityMethodProcessor参数类型为 HttpEntity 或 RequestEntity 类型,这两种类型的参数基本上包含了请求的所有参数信息http 请求中的完整信息

实现类比较多,就不一一说了,这里教大家一招,让大家学会如何看每种参数解析器的源码,掌握看源码之后,大家把每个实现类的源码过一下,基本上就知道如何使用了,这里以RequestParamMethodArgumentResolver源码为例来做解读。

5、RequestParamMethodArgumentResolver 源码解读

5.1、supportsParameter 方法:判断支持参数类型

源码如下,挺简单的,大家注意看注释,秒懂

public boolean supportsParameter(MethodParameter parameter) {
    //判断参数上是否有@RequestParam注解
    if (parameter.hasParameterAnnotation(RequestParam.class)) {
        //参数是Map类型
        if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
            //@RequestParam注解name必须有值
            RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
            return (requestParam != null && StringUtils.hasText(requestParam.name()));
        } else {
            return true;
        }
    } else {
        //判断参数上是否有@RequestPart注解,有则返回false
        if (parameter.hasParameterAnnotation(RequestPart.class)) {
            return false;
        }
        parameter = parameter.nestedIfOptional();
        /**
         * 参数微信是否为下面这些类型,通常文件上传的时候用这种类型接受参数
         * MultipartFile、Collection、List、MultipartFile[]
         * Part、Collection、List、Part[]
         */

        if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
            return true;
        } else if (this.useDefaultResolution) {
            // 是否开启了默认解析,useDefaultResolution默认是false
            return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
        } else {
            return false;
        }
    }
}

5.2、resolveArgument 方法

resolveArgument 方法最终会调用RequestParamMethodArgumentResolver#resolveName方法,代码如下,如果是文件上传的,就获取的是 MultipartFile 对象,否则就是调用request.getParameterValues从参数中取值

protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
    HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);

    Object arg = null;
    MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
    if (multipartRequest != null) {
        List files = multipartRequest.getFiles(name);
        if (!files.isEmpty()) {
            arg = (files.size() == 1 ? files.get(0) : files);
        }
    }
    if (arg == null) {
        String[] paramValues = request.getParameterValues(name);
        if (paramValues != null) {
            arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
        }
    }
    return arg;
}

5、@RequestParam:取请求中的参数

5.1、简介

@RequestParam 注解我们用到的比较多,被这个注解标注的参数,会从 request 的请求参数中取值,参数值为 request.getParameter("@RequestParam 注解 name 的值")

重点来看下这个类的源码,如下,大家要学会看源码中的注释,Spring 注释写的特别的好,这里给 spring 点个赞,注释中详细说明了其用法,大家注意下面匡红的部分,稍后用一个案例代码让大家了解其他常见几种用法,这个注解的用法掌握了,其他的注解都是雷同的,大家去看起源码以及对应的参数解析器,就会秒懂了。

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {

 /**
  * 对应request中参数名称
  */

 @AliasFor("name")
 String value() default "";

 /**
  * 同value
  */

 @AliasFor("value")
 String name() default "";

 /**
  * 请求中是否必须有这个参数
  */

 boolean required() default true;

 /**
  * 默认值
  */

 String defaultValue() default ValueConstants.DEFAULT_NONE;

}

5.2、案例

案例代码如下,注意 5 个参数,这 5 个参数反应了@RequestParam所有的的用法,这个接口的参数解析会用到 2 个解析器:RequestParamMethodArgumentResolverRequestParamMapMethodArgumentResolver,大家可以设置断点 debug 一下。

注意最后一个参数的类型是 MultiValueMap,这种类型相当于 Map>

@RequestMapping("/test1")
@ResponseBody
public Map test1(@RequestParam("name") String name,
                                 @RequestParam("age") int age,
                                 @RequestParam("p1") String[] p1Map,
                                 @RequestParam Map requestParams1,
                                 @RequestParam MultiValueMap requestParams2) 
//MultiValueMap相当于Map>
    Map result = new LinkedHashMap<>();
    result.put("name", name);
    result.put("age", age);
    result.put("p1Map", p1Map);
    result.put("requestParams1", requestParams1);
    result.put("requestParams2", requestParams2);
    return result;
}

发送请求

http://localhost:8080/chat17/test1?name=ready&age=35&p1=1&p1=2&p1=3

接口输出

{
 "name""ready",
 "age"35,
 "p1Map": [
  "1",
  "2",
  "3"
 ],
 "requestParams1": {
  "name""ready",
  "age""35",
  "p1""1"
 },
 "requestParams2": {
  "name": [
   "ready"
  ],
  "age": [
   "35"
  ],
  "p1": [
   "1",
   "2",
   "3"
  ]
 }
}

7、总结

本文带大家了解了参数解析器HandlerMethodArgumentResolver的作用,掌握这个之后,大家就知道控制器的方法中参数的写法,建议大家下去之后,多翻翻这个接口的实现类,掌握常见的参数的各种用法,这样出问题了,才能够快速定位问题,提升快速解决问题的能力。

8、代码位置及说明

8.1、git 地址

https://gitee.com/javacode2018/springmvc-series

8.2、本文案例代码结构说明

9、SpringMVC 系列目录

  1. SpringMVC 系列第 1 篇:helloword
  2. SpringMVC 系列第 2 篇:@Controller、@RequestMapping
  3. SpringMVC 系列第 3 篇:异常高效的一款接口测试利器
  4. SpringMVC 系列第 4 篇:controller 常见的接收参数的方式
  5. SpringMVC 系列第 5 篇:@RequestBody 大解密,说点你不知道的
  6. SpringMVC 系列第 6 篇:上传文件的 4 种方式,你都会么?
  7. SpringMVC 系列第 7 篇:SpringMVC 返回视图常见的 5 种方式,你会几种?
  8. SpringMVC 系列第 8 篇:返回 json & 通用返回值设计
  9. SpringMVC 系列第 9 篇:SpringMVC 返回 null 是什么意思?
  10. SpringMVC 系列第 10 篇:异步处理
  11. SpringMVC 系列第 11 篇:集成静态资源
  12. SpringMVC 系列第 12 篇:拦截器
  13. SpringMVC 系列第 13 篇:统一异常处理
  14. SpringMVC 系列第 14 篇:实战篇:通用返回值 & 异常处理设计
  15. SpringMVC 系列第 15 篇:全注解的方式  &  原理解析
  16. SpringMVC 系列第 16 篇:通过源码解析 SpringMVC 处理请求的流程
  17. SpringMVC 系列第 17 篇:源码解析 SpringMVC 容器的启动过程
  18. SpringMVC 系列第 18 篇:强大的 RequestBodyAdvice 解密
  19. SpringMVC 系列第 19 篇:强大的 ResponseBodyAdvice 解密
  20. SpringMVC 系列第 20 篇:RestFull 详解
  21. SpringMVC 系列第 21 篇:接口调用利器 RestTemplate

10、更多系列文章

  1. Spring 高手系列(共 56 篇)
  2. Java 高并发系列(共 34 篇)
  3. MySql 高手系列(共 27 篇)
  4. Maven 高手系列(共 10 篇)
  5. Mybatis 系列(共 12 篇)
  6. 聊聊 db 和缓存一致性常见的实现方式
  7. 接口幂等性这么重要,它是什么?怎么实现?
  8. 泛型,有点难度,会让很多人懵逼,那是因为你没有看这篇文章!

11、最新资料

  1. 尚硅谷 Java 学科全套教程(总 207.77GB)
  2. 2021 最新版 Java 微服务学习线路图 + 视频
  3. 阿里技术大佬整理的《Spring 学习笔记.pdf》
  4. 阿里大佬的《MySQL 学习笔记高清.pdf》
  5. 2021 版 java 高并发常见面试题汇总.pdf
  6. Idea 快捷键大全.pdf
浏览 21
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报