2024-01-17  阅读(10)
原文作者:路人 原文地址: http://www.itsoku.com/course/6/233

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

202401172042080461.png

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

实现类 支持的参数类型 参数值
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点个赞,注释中详细说明了其用法,大家注意下面匡红的部分,稍后用一个案例代码让大家了解其他常见几种用法,这个注解的用法掌握了,其他的注解都是雷同的,大家去看起源码以及对应的参数解析器,就会秒懂了。

202401172042084852.png

    @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<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、本文案例代码结构说明

202401172042090473.png


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] ,回复【面试题】 即可免费领取。

阅读全文