@RequestBody 与 @RequestParam的区别分析

概念定位

关于HandlerAdaptor(引用)

在SpringMVC框架中,DispatcherServlet是处理用户Web请求的中枢所在,而HandlerAdapter的作用则是帮助DispatchServlet与handlers对接。

Interface HandlerAdapter: Interface that must be implemented for each handler type to handle a request. This interface is used to allow the DispatcherServlet to be indefinitely extensible. The DispatcherServlet accesses all installed handlers through this interface, meaning that it does not contain code specific to any handler type. 

关于HandlerMethod(引用)

Class HandlerMethod: Encapsulates information about a handler method consisting of a method and a bean. Provides convenient access to method parameters, the method return value, method annotations, etc.

暂且可先将HandlerMethod视作为包装了控制器方法的类,其子类InvocableHandlerMethod能够从当前的HTTP请求中解析用于调用方法所需参数。(invokes the underlying method with argument values resolved from the current HTTP request)

同时,有了HandlerMethod的定义,就有对应的HandlerAdaptor类AbstractHandlerMethodAdapter

Class AbstractHandlerMethodAdapter: Abstract base class for HandlerAdapter implementations that support handlers of type HandlerMethod.

引用自Spring文档  

@RequestBody 与 @RequestParam的区别分析_第1张图片

 图片链接

上图将前文提及的数个概念的调用关系整合到了一起,我们可以通过分析上图所列举的各个方法,找出与“从请求报文中提取方法参数”相关的逻辑。

提取参数的时点

审计invokhandlerMethod()、invokeAndHandle(),invokeForRequest()方法的代码,结合Debug,发现控制器方法所需的参数在invokeForRequest方法中被提取:

@Nullable
    public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
        // 参数被提取
        Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
        // 参数被提取
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Invoking '" + ClassUtils.getQualifiedMethodName(this.getMethod(), this.getBeanType()) + "' with arguments " + Arrays.toString(args));
        }

        Object returnValue = this.doInvoke(args);
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Method [" + ClassUtils.getQualifiedMethodName(this.getMethod(), this.getBeanType()) + "] returned [" + returnValue + "]");
        }

        return returnValue;
    }

方法org.springframework.web.method.support.InvocableHandlerMethod#getMethodArgumentValues之中的逻辑尝试从请求报文对象中提取参数,并将其转化为Handler方法所需的参数类型:

        //获取Handler方法所需参数
        MethodParameter[] parameters = this.getMethodParameters();
        Object[] args = new Object[parameters.length];

        for(int i = 0; i < parameters.length; ++i) {
            MethodParameter parameter = parameters[i];
            parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
            //Debug过程中下面这条语句返回null,暂不清楚该方法的使用场景
            args[i] = this.resolveProvidedArgument(parameter, providedArgs);
            if (args[i] == null) {
                if (this.argumentResolvers.supportsParameter(parameter)) {
                    try {
                       //单独对于每个Handler方法所需参数parameter,结合request报文,尝试生成参数并存放至args数组
                        args[i] = this.argumentResolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
                    } catch (Exception var9) {
                        if (this.logger.isDebugEnabled()) {
                            this.logger.debug(this.getArgumentResolutionErrorMessage("Failed to resolve", i), var9);
                        }

                        throw var9;
                    }
                } else if (args[i] == null) {
                    throw new IllegalStateException("Could not resolve method parameter at index " + parameter.getParameterIndex() + " in " + parameter.getExecutable().toGenericString() + ": " + this.getArgumentResolutionErrorMessage("No suitable resolver for", i));
                }
            }
        }

分歧点

继续分析resolveArgument方法的逻辑

@Nullable
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);
        if (resolver == null) {
            throw new IllegalArgumentException("Unknown parameter type [" + parameter.getParameterType().getName() + "]");
        } else {
            return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
        }
    }

getArgumentResolver返回的resolver对应着控制器方法参数的不同提取方式。

对应标题,这里主要对类org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor(@RequestBody)以及类org.springframework.web.method.annotation.RequestParamMapMethodArgumentResolver(@RequestParam注解的对应类)展开分析。

ArgumentResolver的匹配规则

getArgumentResolver(parameter)方法帮助每个parameter匹配对应的ArgumentResolver,具体来讲是通过调用各argumentResolver的supportsParameter方法:

对于 RequestResponseBodyMethodProcessor有:

public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(RequestBody.class);
    }

参数如果加上了RequestBody注解,则使用该ArgumentResolver。 

对于RequestParamMapMethodArgumentResolver,有:

public boolean supportsParameter(MethodParameter parameter) {
        RequestParam requestParam = (RequestParam)parameter.getParameterAnnotation(RequestParam.class);
        return requestParam != null && Map.class.isAssignableFrom(parameter.getParameterType()) && !StringUtils.hasText(requestParam.name());
    }

参数加上RequestParam注解,且控制器方法规定的参数类型能被转化为Map类,且RequestParam注解的name属性未被定义,则使用该ArgumentResolver。

 对于RequestParamMethodArgumentResolver,有:

    public boolean supportsParameter(MethodParameter parameter) {
        if (parameter.hasParameterAnnotation(RequestParam.class)) {
            if (!Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
                return true;
            } else {
                RequestParam requestParam = (RequestParam)parameter.getParameterAnnotation(RequestParam.class);
                return requestParam != null && StringUtils.hasText(requestParam.name());
            }
        }
        //暂时不做分析 
        else if (parameter.hasParameterAnnotation(RequestPart.class)) {
            return false;
        } else {
            parameter = parameter.nestedIfOptional();
            if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
                return true;
            } else {
                // 对应下文的分支3
                return this.useDefaultResolution ? BeanUtils.isSimpleProperty(parameter.getNestedParameterType()) : false;
            }
        }
    }

在加上RequestParam注解的前提下,控制器方法规定的参数类型不能被转化为Map类型或者RequestParam的name属性被定义时,使用该ArgumentResolver。

下面的分支没有用过,暂时不做分析。


提取待转化对象

对于 RequestResponseBodyMethodProcessor,resolveArgument方法通过逐层调用,最终会使用方法org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.EmptyBodyCheckingHttpInputMessage#EmptyBodyCheckingHttpInputMessage,以如下语句获取Request中的Body体,以便进一步转换:

InputStream inputStream = inputMessage.getBody();

因此,对于@RequestBody注解的控制器方法参数,http头信息或是url中的变量信息不会影响待转化对象的内容。

调试过程中发现inputStream内包含Http头信息、Url信息,目前暂未发现将url信息摒除的相关逻辑,只能认为是在后续的转化过程中url信息可能会被“无视”。

对于 RequestParamMapMethodArgumentResolver,会通过如下方式提取待转化对象:

Map parameterMap = webRequest.getParameterMap();

getParameterMap方法的具体实现方式直接取决于Web容器。例如Tomcat的Coyote。在本文里想吃透Coyote框架的逻辑是不可能的,但通过静态源码分析相关方法org.apache.catalina.connector.Request#parseParameters,发现其包含有form-data,form-urlencoded的处理分支,因此可以认为当content-type为application/json时,该ArgumentResolver不会抛出异常,而会直接无视post的报文内容(url中参数的提取似乎与该段逻辑无关)。

if ("multipart/form-data".equals(contentType)) {
    this.parseParts(false);
    success = true;
} else if (!"application/x-www-form-urlencoded".equals(contentType)) {
    success = true;
    // 对于application/json,该分支会被自动满足,并转入结束流程。
} else {
    // 这里是对form-urlencoded类型请求的处理逻辑
    .....

对于RequestParamMethodArgumentResolver,个人认为主要逻辑集中在org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver#resolveArgument和方法org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver#resolveName方法:

Object arg = this.resolveName(resolvedName.toString(), nestedParameter, webRequest);
if (arg == null) {
    if (namedValueInfo.defaultValue != null) {
        arg = this.resolveStringValue(namedValueInfo.defaultValue);
    } else if (namedValueInfo.required && !nestedParameter.isOptional()) {
        this.handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
    }

    arg = this.handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
} else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
    arg = this.resolveStringValue(namedValueInfo.defaultValue);
}
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
    //Case 1: arguments of type MultipartFile
    //in conjunction with Spring's MultipartResolver abstraction
    HttpServletRequest servletRequest = (HttpServletRequest)request.getNativeRequest(HttpServletRequest.class);
    Object arg;
    if (servletRequest != null) {
        arg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
        if (arg != MultipartResolutionDelegate.UNRESOLVABLE) {
            return arg;
        }
    }

    //Case 2: arguments of type javax.servlet.http.Part
    // in conjunction with Servlet 3.0 multipart requests
    arg = null;
    MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest)request.getNativeRequest(MultipartHttpServletRequest.class);
    if (multipartRequest != null) {
        List files = multipartRequest.getFiles(name);
        if (!files.isEmpty()) {
            arg = files.size() == 1 ? files.get(0) : files;
        }
    }
    //Case 3:  simple types (int, long, etc.) not annotated with @RequestParam
    if (arg == null) {
        String[] paramValues = request.getParameterValues(name);
        if (paramValues != null) {
            arg = paramValues.length == 1 ? paramValues[0] : paramValues;
        }
    }

    return arg;
}

对于resolveName方法, 由于我没有使用过MultipartRequest所以不做分析(注释文本是我从文档里抄的= =)。但是Case3则比较容易理解:即使没有加上RequestParam注解,参数类型为简单类型的参数也会被该方法处理。对应上文提到的分支三。分支三中使用的方法getParameterValues的具体实现方式同样直接依赖于底层容器。


转换为方法参数

以 RequestResponseBodyMethodProcessor为例,其会调用MessageConverters来尝试将待转化对象转换为控制器方法所需的参数,例如json串形式的参数就能够依靠对应的messageConverter类来进行转换。(这也说明了为什么以x-www-form-urlencoded形式提交的post报文无法被读取的原因——没有相应的MessageConverter,自然也就无法进行转化!

相关逻辑出现在org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters方法:

Iterator var11 = this.messageConverters.iterator();

HttpMessageConverter converter;
Class converterType;
GenericHttpMessageConverter genericConverter;
while(true) {
    if (!var11.hasNext()) {
        break label98;
    }

    converter = (HttpMessageConverter)var11.next();
    converterType = converter.getClass();
    genericConverter = converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter)converter : null;
    if (genericConverter != null) {
        //判断当前MessageConverter能否实施参数转化。
        if (genericConverter.canRead(targetType, contextClass, contentType)) {
            break;
        }
    } else if (targetClass != null && converter.canRead(targetClass, contentType)) {
        break;
    }
    if (this.logger.isDebugEnabled()) {
    this.logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
    }

    if (message.hasBody()) {
        HttpInputMessage msgToUse = this.getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
        //进行转换。
        body = genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) : converter.read(targetClass, msgToUse);
        body = this.getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
    } else {
        body = this.getAdvice().handleEmptyBody((Object)null, message, parameter, targetType, converterType);
    }    
}

此外,相关逻辑会在以下两种场合抛出 media type not supported异常,分别对应了Http头所设contentType无效,及请求报文体Body无法找到匹配MessageConverter进行转换的场合。

try {
         contentType = inputMessage.getHeaders().getContentType();
       } catch (InvalidMediaTypeException var16) {
           throw new HttpMediaTypeNotSupportedException(var16.getMessage());
       }
// .... 当所有的messageConverters都不能处理待转化对象时,才会进入该分支
else if (httpMethod != null && SUPPORTED_METHODS.contains(httpMethod) && (!noContentType || message.hasBody())) {
            throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
        }

出于篇幅限制,这里不对MessageConverters的运作原理进行进一步分析。 

 对于 RequestParamMapMethodArgumentResolver,逻辑比较简明,其实就是将paramterMap里的键值放入参数对应的map中。需要注意的是,可以通过将控制器方法参数类型设置为MultiValueMap来存储数组型参数。

if (MultiValueMap.class.isAssignableFrom(paramType)) {
            MultiValueMap result = new LinkedMultiValueMap(parameterMap.size());
            parameterMap.forEach((key, values) -> {
                String[] var3 = values;
                int var4 = values.length;

                for(int var5 = 0; var5 < var4; ++var5) {
                    String value = var3[var5];
                    result.add(key, value);
                }

            });
            return result;
        } else {
            Map result = new LinkedHashMap(parameterMap.size());
            parameterMap.forEach((key, values) -> {
                if (values.length > 0) {
                    result.put(key, values[0]);
                }

            });
            return result;
        }

 对于RequestParamMethodArgumentResolver,其需要依赖于下面这段逻辑,来完成对仍处于中间类型(例如String、String[])的参数的类型转换:

if (binderFactory != null) {
    WebDataBinder binder = binderFactory.createBinder(webRequest, (Object)null, namedValueInfo.name);

    try {
        arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
    } catch (ConversionNotSupportedException var11) {
        throw new MethodArgumentConversionNotSupportedException(arg, var11.getRequiredType(), namedValueInfo.name, parameter, var11.getCause());
    } catch (TypeMismatchException var12) {
        throw new MethodArgumentTypeMismatchException(arg, var12.getRequiredType(), namedValueInfo.name, parameter, var12.getCause());
    }
}

经过步步调试,跟踪至方法org.springframework.beans.TypeConverterDelegate#convertIfNecessary(java.lang.String, java.lang.Object, java.lang.Object, java.lang.Class, org.springframework.core.convert.TypeDescriptor)。它的流程较为冗长,文档里也没有对其进行过多介绍。但该段逻辑值得注意:

//没有使用过,看文档里介绍是老版本Spring API中用到的功能?
PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);

ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
        if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {
            TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
            if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
                try {
                    return conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
                } catch (ConversionFailedException var14) {
                    conversionAttemptEx = var14;
                }
            }
        }



//...后面的分支暂时不做深入

想要搞清楚conversionService的特性,必须要看调试,举个例子吧,首先我注册了一个能转换String类型实例至BookEntityt实例的自定义Formatter(配置方法网上能查到)

@RequestBody 与 @RequestParam的区别分析_第2张图片

 并且配置控制器方法参数的注解为@RequestParam、类型为BookEntity的参数book,但是,提交请求时故意为book参数构造了一个数组:

@RequestBody 与 @RequestParam的区别分析_第3张图片

 可见我们注册的Converter支持String类型至BookEntity类的转化,那么当提交参数类型为String[]时转换会失败吗?实际上并没有。

结合已注册的各类Converter,conversionService可以智能地通过多次中转来完成转化:

@RequestBody 与 @RequestParam的区别分析_第4张图片 ConversionService方法首先调用ArrayToObjectConverter

org.springframework.core.convert.support.ArrayToObjectConverter#convert方法首先直接提取了数组中的第一个元素并忽略其他元素,造成了参数内容的损失

该方法会接着调用conversionService.convert方法进行第二次转换。这时才会真正用到了我们新增的Converter。

@Nullable
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
    if (source == null) {
        return null;
    } else if (sourceType.isAssignableTo(targetType)) {
        return source;
    } else if (Array.getLength(source) == 0) {
        return null;
    } else {
        Object firstElement = Array.get(source, 0);
        // 再度调用conversionService函数尝试进行第二次转换
        return this.conversionService.convert(firstElement, sourceType.elementTypeDescriptor(firstElement), targetType);
    }
}

我们是在Formatter实例中定义了从String到BookEntity的转换方法,convert方法也会根据具体的实现方式,最终调用Formatter里定义的相关方法完成参数转换,此处不做进一步介绍。

通过分析源码,发现实现中转转换的原理就是提取源类型、目标类型所继承自的所有父类与接口,两两组合并检查是否有满足要求的Converter:

public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) {
            List> sourceCandidates = this.getClassHierarchy(sourceType.getType());
            List> targetCandidates = this.getClassHierarchy(targetType.getType());
            Iterator var5 = sourceCandidates.iterator();

            while(var5.hasNext()) {
                Class sourceCandidate = (Class)var5.next();
                Iterator var7 = targetCandidates.iterator();

                while(var7.hasNext()) {
                    Class targetCandidate = (Class)var7.next();
                    ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate);
                    GenericConverter converter = this.getRegisteredConverter(sourceType, targetType, convertiblePair);
                    if (converter != null) {
                        return converter;
                    }
                }
            }

            return null;
        }

总结

@RequestParam与@RequestBody注解的参数在MVC框架逻辑中的分歧时机:发生在调用对应ArgumetentResolver时

从Request报文中提取待转换对象的方法:@RequestParam会使用getParametersMap、getParameterValue(name)方法来获取待转换对象,这两个方法在底层实现上支持解析form、urlencoded-form类别的报文体;@RequestBody则直接使用getBody()方法获取报文字节流,自行进行参数解析。

转化为控制器方法所需参数的方法:@RequestParam会考虑使用Converter(org.springframework.core.convert.converter)来完成参数的转换。@RequestBody则使用MessageConverter(org.springframework.http.converter)进行转换。

你可能感兴趣的:(java,开发语言,后端)