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
>
@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、本文案例代码结构说明
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。