❤️SpringMVC成神之路之二十二、参数解析器HandlerMethodArgumentResolver解密

小明的学习圈子2023-12-24后端SpringMVC成神之路

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.2、解析参数值的过程

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中默认情况下注册了这么多解析器,如下图:

image-20231231202642987

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

实现类支持的参数类型参数值
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<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;
}

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

6.1、简介

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

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

image-20231231202750535

@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;
}

6.2、案例

案例代码如下,注意5个参数,这5个参数反应了@RequestParam所有的的用法,这个接口的参数解析会用到2个解析器:RequestParamMethodArgumentResolverRequestParamMapMethodArgumentResolver,大家可以设置断点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、本文案例代码结构说明

image-20231231202855365

来源:http://www.itsoku.com/course/6/233

Last Updated 2024/4/6 11:55:17