大家好,我是路人,这是 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<MethodParameter>?parameterList;
//2.参数解析器列表
List<HandlerMethodArgumentResolver>?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、ZoneId | Servlet 容器中的 request |
ServletResponseMethodArgumentResolver | 参数类型是 ServletResponse、OutputStream、Writer | Servlet 容器中的 response |
ModelMethodProcessor | 参数为 org.springframework.ui.Model 类型 | 来源于 SpringMVC 容器 |
RequestAttributeMethodArgumentResolver | 参数需使用@RequestAttribute | request.getAttribute |
SessionAttributeMethodArgumentResolver | 参数需使用@SessionAttribute | session.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<MultipartFile>、List<MultipartFile>、MultipartFile[]
?????????*?Part、Collection<Part>、List<Part>、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<MultipartFile>?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 个解析器:RequestParamMethodArgumentResolver
和RequestParamMapMethodArgumentResolver
,大家可以设置断点 debug 一下。
注意最后一个参数的类型是 MultiValueMap,这种类型相当于 Map<String,List<String>>
@RequestMapping("/test1")
@ResponseBody
public?Map<String,?Object>?test1(@RequestParam("name")?String?name,
?????????????????????????????????@RequestParam("age")?int?age,
?????????????????????????????????@RequestParam("p1")?String[]?p1Map,
?????????????????????????????????@RequestParam?Map<String,?String>?requestParams1,
?????????????????????????????????@RequestParam?MultiValueMap?requestParams2)?{?//MultiValueMap相当于Map<String,List<String>>
????Map<String,?Object>?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 系列目录
SpringMVC 系列第 1 篇:helloword
SpringMVC 系列第 2 篇:@Controller、@RequestMapping
SpringMVC 系列第 3 篇:异常高效的一款接口测试利器
SpringMVC 系列第 4 篇:controller 常见的接收参数的方式
SpringMVC 系列第 5 篇:@RequestBody 大解密,说点你不知道的
SpringMVC 系列第 6 篇:上传文件的 4 种方式,你都会么?
SpringMVC 系列第 7 篇:SpringMVC 返回视图常见的 5 种方式,你会几种?
SpringMVC 系列第 8 篇:返回 json & 通用返回值设计
SpringMVC 系列第 9 篇:SpringMVC 返回 null 是什么意思?
SpringMVC 系列第 10 篇:异步处理
SpringMVC 系列第 11 篇:集成静态资源
SpringMVC 系列第 12 篇:拦截器
SpringMVC 系列第 13 篇:统一异常处理
SpringMVC 系列第 14 篇:实战篇:通用返回值 & 异常处理设计
SpringMVC 系列第 15 篇:全注解的方式 ?&? 原理解析
SpringMVC 系列第 16 篇:通过源码解析 SpringMVC 处理请求的流程
SpringMVC 系列第 17 篇:源码解析 SpringMVC 容器的启动过程
SpringMVC 系列第 18 篇:强大的 RequestBodyAdvice 解密
SpringMVC 系列第 19 篇:强大的 ResponseBodyAdvice 解密
SpringMVC 系列第 20 篇:RestFull 详解
SpringMVC 系列第 21 篇:接口调用利器 RestTemplate
10、更多系列文章
Spring 高手系列(共 56 篇)
Java 高并发系列(共 34 篇)
MySql 高手系列(共 27 篇)
Maven 高手系列(共 10 篇)
Mybatis 系列(共 12 篇)
聊聊 db 和缓存一致性常见的实现方式
接口幂等性这么重要,它是什么?怎么实现?
泛型,有点难度,会让很多人懵逼,那是因为你没有看这篇文章!
11、最新资料
尚硅谷 Java 学科全套教程(总 207.77GB)
2021 最新版 Java 微服务学习线路图 + 视频
阿里技术大佬整理的《Spring 学习笔记.pdf》
阿里大佬的《MySQL 学习笔记高清.pdf》
2021 版 java 高并发常见面试题汇总.pdf
Idea 快捷键大全.pdf