当前位置:   article > 正文

HandlerMethodArgumentResolver参数解析

handlermethodargumentresolver

前言

@RequestMapping注解的方法就是一个请求处理器 handler,Spring MVC 会把该方法封装成 HandlerMethod 对象。HTTP 请求经过 RequestMappingInfo 条件匹配后最终路由到目标 HandlerMethod,接下来就是对目标方法的调用了。
调用方法你得有参数吧,所以 Spring MVC 会先依赖 HandlerMethodArgumentResolverComposite 组件解析参数列表,再反射调用目标方法,本文重点参数的解析。

参数类型

DispatcherServlet 作为一个 Servlet,它能拿到的只有 HttpServletRequest 和 HttpServletResponse。如何把 HttpServletRequest 里的参数或属性转换成 Java 方法所需的参数列表,这本身是一件非常复杂的事情。
Spring MVC 专门抽象了一个策略接口 HandlerMethodArgumentResolver 来解析目标方法调用所需的参数,内置了很多实现类,每个实现类对应一种 Controller 方法的参数类型。

image.png
你不需要去看所有的实现类,等实际用到了再去研究也不迟,这里列举几个常用的实现类:

  • RequestParamMethodArgumentResolver:解析@RequestParam注解的参数,从请求的 Parameters 里面获取参数
  • RequestHeaderMethodArgumentResolver:解析@RequestHeader注解的参数,从请求头里获取参数
  • RequestAttributeMethodArgumentResolver:解析@RequestAttribute注解的参数,从请求属性里获取参数
  • SessionAttributeMethodArgumentResolver:解析@SessionAttribute注解的参数,从 Session 里获取参数
  • ServletCookieValueMethodArgumentResolver:解析@CookieValue注解的参数,从 Cookie 里获取参数
  • RequestResponseBodyMethodProcessor:解析@RequestBody注解的参数,从请求体里面获取参数
  • HandlerMethodArgumentResolverComposite:聚合了一组 Spring MVC 内置的解析器,本身不参与参数解析

HandlerMethodArgumentResolverComposite

Spring MVC 调用目标方法时,使用 HandlerMethodArgumentResolverComposite 实现类来解析参数。
它内部维护了一组解析器,在RequestMappingHandlerAdapter#afterPropertiesSet处被初始化。

private final List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();
  • 1

解析参数时,首先找到能支持给定参数类型的解析器,找不到就只能抛异常。

private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
    HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
    if (result == null) {
        // 遍历解析器,找到能支持参数的解析器并缓存
        for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
            if (resolver.supportsParameter(parameter)) {
                result = resolver;
                this.argumentResolverCache.put(parameter, result);
                break;
            }
        }
    }
    return result;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

再把 MethodParameter 交给找到的解析器解析:

public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    // 查找支持参数类型的解析器 遍历列表,找到就返回
    HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
    if (resolver == null) {
        throw new IllegalArgumentException("Unsupported parameter type [" +
                parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
    }
    return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

我们重点要看的是其它实现类!

常用解析器

篇幅原因,没必要把所有的解析器都分析一遍,这里挑选三个常用的解析器。

RequestParamMethodArgumentResolver

解析@RequestParam注解的参数,从请求的 Parameters 里面获取参数。
image.png
它继承自 AbstractNamedValueMethodArgumentResolver,顾名思义,这一类解析器首先得确定参数的名称 name,才能从 Request 里去拿参数。例如:从 Cookie、Request#Parameters、Request#Attributes、Request#Headers 等等,都得先确定名称才好拿到参数。

解析参数时,父类实现了一些通用的逻辑:

  • 确定参数名称,得到 NamedValueInfo
  • 解析 ${} 占位符、#{} 表达式
  • 解析原始参数值,通常是字符串
  • 原始参数再经过 WebDataBinder 转换成我们想要的 Java 对象
  • 最终预留一个钩子函数 handleResolvedValue 给子类扩展
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    /**
     * 获得参数的名称、是否必须、以及默认值
     * 对于参数的名称,如果参数加了指定注解,则取注解指定的名称
     * 否则,使用ParameterNameDiscoverer组件解析参数名称
     * 方法参数的真实名称解析是个麻烦的过程,Java反射获取有一定的限制,需要用到ASM技术
     */
    NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
    MethodParameter nestedParameter = parameter.nestedIfOptional();
    // 解析占位符/表达式
    Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
    if (resolvedName == null) {
        throw new IllegalArgumentException(
                "Specified name must not resolve to null: [" + namedValueInfo.name + "]");
    }
    // 解析参数值(原始未经过转换的)
    Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
    if (arg == null) {
        // 处理默认值
        if (namedValueInfo.defaultValue != null) {
            arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
        }
        else if (namedValueInfo.required && !nestedParameter.isOptional()) {
            // 抛出参数丢失异常
            handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
        }
        // 处理空值 对于基本类型会抛出异常
        arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
    }
    else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
        arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
    }
    /**
     * 解析出来的原始参数 还要经过WebDataBinder转换成我们想要的Java对象
     * 比如参数date=2023-01-01 我们想转换成LocalDate
     */
    if (binderFactory != null) {
        // 创建数据绑定器
        WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
        try {
            // 数据转换
            arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
        }
        catch (ConversionNotSupportedException ex) {
            throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
                    namedValueInfo.name, parameter, ex.getCause());
        }
        catch (TypeMismatchException ex) {
            throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
                    namedValueInfo.name, parameter, ex.getCause());
        }
        // Check for null value after conversion of incoming argument value
        if (arg == null && namedValueInfo.defaultValue == null &&
                namedValueInfo.required && !nestedParameter.isOptional()) {
            handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest);
        }
    }
    // 留给子类扩展
    handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
    return arg;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

这里要强调一下,解析方法的参数名不是一件容易的事。如果你给参数加上了注解,在注解里指定了 name,这是一个很好的习惯,Spring MVC 会直接拿注解里给定值作为 name。如果你没有加注解或没有在注解里指定 name,你会发现 Spring MVC 也可以正确的绑定参数,但是背后的处理逻辑就会复杂很多。你可能会说,有什么复杂的?直接反射获取方法的参数名不就好了吗?反射确实可以获取到参数名,但是有很大的限制。首先,你的 JDK 版本必须是 8 及以上,且编译时加上-parameters参数才能让 javac 编译类时保留参数名称,否则反射获取到的参数名是 arg0、arg1 这样无意义的名称,因为对于 JVM 运行时来说,方法的参数名的确没有作用。
getNamedValueInfo()会获取参数的名称,你可以看一下它的处理流程:

private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
    NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
    if (namedValueInfo == null) {
        // 如果加了注解且指定了参数名,直接注解参数名
        namedValueInfo = createNamedValueInfo(parameter);
        // 否则通过ParameterNameDiscoverer解析参数名称
        namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
        // 缓存起来,解析代价较高
        this.namedValueInfoCache.put(parameter, namedValueInfo);
    }
    return namedValueInfo;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

updateNamedValueInfo()里面,如果没有通过注解拿到参数名,Spring MVC 就必须尝试自己解析参数名了,这里又会依赖 ParameterNameDiscoverer 组件。

public String getParameterName() {
    if (this.parameterIndex < 0) {
        return null;
    }
    ParameterNameDiscoverer discoverer = this.parameterNameDiscoverer;
    if (discoverer != null) {
        String[] parameterNames = null;
        if (this.executable instanceof Method) {
            parameterNames = discoverer.getParameterNames((Method) this.executable);
        }
        else if (this.executable instanceof Constructor) {
            parameterNames = discoverer.getParameterNames((Constructor<?>) this.executable);
        }
        if (parameterNames != null) {
            this.parameterName = parameterNames[this.parameterIndex];
        }
        this.parameterNameDiscoverer = null;
    }
    return this.parameterName;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

总之,确认参数名称以后,子类要做的,就是实现自己取参数的逻辑了。对于 RequestParamMethodArgumentResolver,自然是从 Request#Parameters 里面取了,不过你要注意的是,除了普通参数,还有文件上传需要处理哦。

protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
    HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);

    if (servletRequest != null) {
        // 解析文件上传参数 MultipartFile
        Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
        if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
            return mpArg;
        }
    }
    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);
        }
    }
    // 普通参数,直接从ServletRequest.getParameterValues取
    if (arg == null) {
        String[] paramValues = request.getParameterValues(name);
        if (paramValues != null) {
            arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
        }
    }
    return arg;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

至此,我们就拿到参数了。但是现在参数还是 String 类型的,Spring MVC 不能直接拿这个参数去调用目标方法,因为参数类型可能并不匹配。例如:请求参数date=2023-01-01,Controller 方法里的参数类型是 LocalDate,这就需要做类型的转换了。Spring MVC 把这个职责交给了 WebDataBinder,它是数据绑定器,它会把 Request 里的参数绑定到我们想要的目标对象上,这里会调用DataBinder#convertIfNecessary做参数类型的转换。
到这里,@RequestParam参数的解析就完成了。为了便于理解,我画了一张流程图:
image.png

RequestHeaderMethodArgumentResolver

看完 RequestParamMethodArgumentResolver,再看@RequestHeader参数解析就很简单了。
RequestHeaderMethodArgumentResolver 也是需要确定参数名的,所以它也继承了 AbstractNamedValueMethodArgumentResolver,整体流程都一样,区别是它是从请求头里拿参数:

protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
    String[] headerValues = request.getHeaderValues(name);
    if (headerValues != null) {
        return (headerValues.length == 1 ? headerValues[0] : headerValues);
    } else {
        return null;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

同样的,拿到的参数值依然是 String,也需要经过 WebDataBinder 的类型转换,这些都是在父类中统一实现的。

RequestResponseBodyMethodProcessor

最后再看一个十分常用的,@RequestBody参数的解析器。它把请求体作为参数,所以不需要知道参数名,也就没继承 AbstractNamedValueMethodArgumentResolver。
image.png
它还实现了 HandlerMethodReturnValueHandler 接口,所以它不止能解析参数,还能处理返回值,因为本文只分析参数的解析,所以先跳过返回值的处理。

它是怎么把请求体转换成 Controller 方法参数的呢?
首先我们得知道,HTTP 请求体的媒体类型 Content-Type 可以有多种类型,它可以是 JSON、Xml、表单、甚至是二进制数据。而被@RequestBody注解的参数,我们也可以用多种不同的类来接收,比如可以是 Object、Map、String、甚至是你自己定义的 POJO。
这就涉及到更加复杂的数据转换过程了,Spring MVC 通过一个叫 HttpMessageConverter 的组件,来完成这个复杂的转换过程。
解析器会把参数交给readWithMessageConverters()方法解析,自己只处理解析后的数据校验工作:

public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

    parameter = parameter.nestedIfOptional();
    /**
     * 通过HttpMessageConverter转换读取请求体
     * 要根据请求的Content-Type和参数类型交给HttpMessageConverter解析
     */
    Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
    String name = Conventions.getVariableNameForParameter(parameter);

    if (binderFactory != null) {
        WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
        if (arg != null) {
            // 数据校验
            validateIfApplicable(binder, parameter);
            if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
            }
        }
        if (mavContainer != null) {
            mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
        }
    }

    return adaptArgumentIfNecessary(arg, parameter);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

先把 HttpServletRequest 封装成 ServletServerHttpRequest,方便读取请求头和请求体,再调用重载方法解析参数:

protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
        Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

    HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
    Assert.state(servletRequest != null, "No HttpServletRequest");
    // Request封装 方便读取请求头和请求体
    ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
    // 解析参数
    Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
    if (arg == null && checkRequired(parameter)) {
        throw new HttpMessageNotReadableException("Required request body is missing: " +
                parameter.getExecutable().toGenericString(), inputMessage);
    }
    return arg;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

数据类型的转换,由请求头 Content-Type 和参数类型共同决定。例如:当 Content-Type 是“application/json”时,参数类型可以是 Map,因为转换器知道如何把 JSON 转换成 Map;反之,如果 Content-Type 是“text/plain”,参数类型就只能是 String 了,如果声明 Map 就会报错,因为转换器不知道怎么把一段文本转换成 Map,即使它可能是个正确的 JSON。
数据的转换依赖 HttpMessageConverter 组件,这里先不细说,总之 Spring MVC 会先调用canRead()判断当前转换器是否支持读取给定的参数,如果支持紧接着就会调用其read()把请求体转换成我们期望的最终对象。
至此,@RequestBody参数也就解析完成了。

尾巴

RequestMappingHandlerAdapter 要想调用目标方法处理请求,首先得先解析出正确的参数。如何从 HttpServletRequest 解析出 Java 方法需要的参数列表,是个非常复杂的过程,方法的调用反而简单,通过反射就可以。
Spring MVC 的 Controller 方法支持很多种参数类型,通过注解就可以很简单的读取 Request 参数值、属性值、请求头的值、Cookie、Session 值等等,功能很丰富,如何解析这些参数就成了麻烦事。Spring MVC 为此专门抽象出了 HandlerMethodArgumentResolver 策略接口,不同类型的参数设计不同的实现类去解析,最后通过一个叫 HandlerMethodArgumentResolverComposite 的类把这些解析器聚集到一起,最终实现了这一套复杂的参数解析逻辑。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/917057
推荐阅读
相关标签
  

闽ICP备14008679号