目前所有系列文章已同步至个人博客(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 注释写的特别的好,这里给 sprin***个赞,注释中详细说明了其用法,大家注意下面匡红的部分,稍后用一个案例代码让大家了解其他常见几种用法,这个注解的用法掌握了,其他的注解都是雷同的,大家去看起源码以及对应的参数解析器,就会秒懂了。
@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