Spring 是如何将前端请求中的参数解析到指定对象的

Posted HelloWorld_EE

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring 是如何将前端请求中的参数解析到指定对象的相关的知识,希望对你有一定的参考价值。

Spring 是如何将前端请求中的参数解析到指定对象的

注:文章稍微偏源码,对照着源码debug看效果更好一点。

先说下背景,最近看一个项目,有一个接口的定义如下

@PostMapping(value = "/conditionConfig")
public BaseResponse<PageInfo<ConfigDTO>> conditionConfig(@Valid @RequestBody ConfigQuery configQuery) 
    return configConsumer.conditionConfig(configQuery);

其中ConfigQuery模型的定义如下:

@Data
public class ConfigQuery implements Serializable 

	//其他参数省略

    @ApiModelProperty("起始时间")
    private Date startTime;

    @ApiModelProperty("截止时间")
    private Date endTime;

而关于startTime和endTime的请求参数,我以为和正常情况一样,会传类似于2020-12-05T11:29:21.854Z 的时间格式过来。具体如下(注:该格式的请求数据也是可以正常被解析而正常工作的)

curl 'localhost:8080/conditionConfig' \\
  -H 'Connection: keep-alive' \\
  -H 'Pragma: no-cache' \\
  -H 'Cache-Control: no-cache' \\
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (Khtml, like Gecko) Chrome/86.0.4240.183 Safari/537.36' \\
  -H 'Content-Type: application/json' \\
  -H 'Accept: */*' \\
  -H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8' \\
  --data-binary '"endTime": "2020-12-05T11:29:21.854Z","startTime": "2020-12-05T11:29:21.854Z"' \\
  --compressed \\
  --insecure

而看了下前端的请求,发现前端传递的是Long类型的时间戳,具体如下

curl 'localhost:8080/conditionConfig' \\
  -H 'Connection: keep-alive' \\
  -H 'Pragma: no-cache' \\
  -H 'Cache-Control: no-cache' \\
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36' \\
  -H 'Content-Type: application/json' \\
  -H 'Accept: */*' \\
  -H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8' \\
  --data-binary '"startTime":1606579200000,"endTime":1607270400000' \\
  --compressed \\
  --insecure

通过Debug观察,发现后端接受到该请求,是可以自动将Long类型的时间戳转换为Date类型。这个时候我的好奇心就产生了,哎哟,这个自动转换是如何来实现的?

于是就带着这个疑问开始了在Spring的源码中寻找答案之旅,最终也就有了此篇文章的产生。

在这篇文章中分析了SpringMVC中doDispatch方法中的大致实现思路
https://blog.csdn.net/u010412719/article/details/79068994

但分析的比较粗糙,并没有对参数解析这部分进行分析。本篇文章将带着如上的困惑来分析Spring是如何来解析前端传过来的参数的。

如下是doDispatch方法中开始处理reuqest的一行代码:

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

这行代码的ha是RequestMappingHandlerAdapter对象实例,其handle方法直接调用了如下的handleInternal方法,我们关注handleInternal方法如下的那行代码即可。

protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception 
    this.checkRequest(request);
    ModelAndView mav;
    if(this.synchronizeOnSession) 
        //省略
     else 
        //重点
        mav = this.invokeHandlerMethod(request, response, handlerMethod);
    

    //...
    return mav;



protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception 
    ServletWebRequest webRequest = new ServletWebRequest(request, response);

    Object result;
    try 
        WebDataBinderFactory binderFactory = this.getDataBinderFactory(handlerMethod);
        ModelFactory modelFactory = this.getModelFactory(handlerMethod, binderFactory);
        ServletInvocableHandlerMethod invocableMethod = this.createInvocableHandlerMethod(handlerMethod);
        //注意这里:就是将在RequestMappingHandlerAdapter初始化的一系列参数解析期给到ServletInvocableHandlerMethod,在该类的内部会使用到。
        invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
        invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
        invocableMethod.setDataBinderFactory(binderFactory);
        invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
        ModelAndViewContainer mavContainer = new ModelAndViewContainer();
        mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
        modelFactory.initModel(webRequest, mavContainer, invocableMethod);
        mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
        AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
        asyncWebRequest.setTimeout(this.asyncRequestTimeout);
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        asyncManager.setTaskExecutor(this.taskExecutor);
        asyncManager.setAsyncWebRequest(asyncWebRequest);
        asyncManager.registerCallableInterceptors(this.callableInterceptors);
        asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
        if(asyncManager.hasConcurrentResult()) 
            result = asyncManager.getConcurrentResult();
            mavContainer = (ModelAndViewContainer)asyncManager.getConcurrentResultContext()[0];
            asyncManager.clearConcurrentResult();
            if(this.logger.isDebugEnabled()) 
                this.logger.debug("Found concurrent result value [" + result + "]");
            

            invocableMethod = invocableMethod.wrapConcurrentResult(result);
        

        //真正的执行逻辑
        invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);
        if(!asyncManager.isConcurrentHandlingStarted()) 
            ModelAndView var15 = this.getModelAndView(mavContainer, modelFactory, webRequest);
            return var15;
        

        result = null;
     finally 
        webRequest.requestCompleted();
    

    return (ModelAndView)result;

invokeHandlerMethod 方法中关注两点:

1)new了一个ServletInvocableHandlerMethod对象invocableMethod。
2)调用invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);

因此,继续看invocableMethod.invokeAndHandle的内部实现。

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception 
//1.执行请求,得到返回值
    Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
    this.setResponseStatus(webRequest);
    if(returnValue == null) 
        if(this.isRequestNotModified(webRequest) || this.getResponseStatus() != null || mavContainer.isRequestHandled()) 
            mavContainer.setRequestHandled(true);
            return;
        
     else if(StringUtils.hasText(this.getResponseStatusReason())) 
        mavContainer.setRequestHandled(true);
        return;
    

    mavContainer.setRequestHandled(false);

    try 
//2.对返回值进行处理        this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);
     catch (Exception var6) 
        if(this.logger.isTraceEnabled()) 
            this.logger.trace(this.getReturnValueHandlingErrorMessage("Error handling return value", returnValue), var6);
        

        throw var6;
    

invokeAndHandle主要干了两件事情:

1)执行请求,得到返回值
2)对返回值进行处理

由于我们重点关注的是第一部分,因此我们继续看invokeForRequest方法。

InvocableHandlerMethod

public Object invokeForRequest(NativeWebRequest request, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception 
    //1.解析参数,例如:@RequestBody、@RequestParam注解的参数,
    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));
    
    //2.真正开始执行
    Object returnValue = this.doInvoke(args);
    if(this.logger.isTraceEnabled()) 
        this.logger.trace("Method [" + ClassUtils.getQualifiedMethodName(this.getMethod(), this.getBeanType()) + "] returned [" + returnValue + "]");
    

    return returnValue;

上面这个方法主要也干了两件事情
1)解析参数,例如:如何将请求中的参数映射到使用@RequestBody、@RequestParam注解的参数上去

2)真正开始执行业务逻辑

在文章开始的地方,我们就说明了我们的目的:Spring是如何来解析请求的参数的,到这里,我们就离目标越来越近了,因此我们主要看第一部分,即getMethodArgumentValues方法的内部实现。

getMethodArgumentValues方法的源码如下:

private Object[] getMethodArgumentValues(NativeWebRequest request, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception 
    //1.得到目标参数信息,即我们接口中定义的参数信息。
    MethodParameter[] parameters = this.getMethodParameters();
    Object[] args = new Object[parameters.length];
    //2.开始对目标参数一个一个解析
    for(int i = 0; i < parameters.length; ++i) 
        MethodParameter parameter = parameters[i];
        parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
        args[i] = this.resolveProvidedArgument(parameter, providedArgs);
        if(args[i] == null) 
            //2.1判断此参数是否能被解析
            if(this.argumentResolvers.supportsParameter(parameter)) 
                try 
                    //2.2.使用HandlerMethodArgumentResolverComposite 进行解析
                    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.getMethod().toGenericString() + ": " + this.getArgumentResolutionErrorMessage("No suitable resolver for", i));
            
        
    

    return args;

上面对主要的代码添加了响应的注释。

下面来看HandlerMethodArgumentResolverComposite中的resolveArgument方法是如何来解析参数的。

public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, 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);
    

HandlerMethodArgumentResolverComposite 该类中定义了一系列的解析器,来解析参数,这就是比较明显的策略模式,方便扩展。

例如:
1)@RequestBody 注解的参数使用RequestResponseBodyMethodProcessor

2)@RequestParam 注解的参数使用 RequestParamMethodArgumentResolver

而定义的这一系列的参数解析器,是在那里被初始化的呢?

答案:是在RequestMappingHandlerAdapter的afterPropertiesSet方法中,具体如下

public void afterPropertiesSet() 
    this.initControllerAdviceCache();
    List handlers;
    if(this.argumentResolvers == null) 
        //这里,将一系列的解析器加入到了HandlerMethodArgumentResolverComposite类型的对象argumentResolvers中
        
        handlers = this.getDefaultArgumentResolvers();
        this.argumentResolvers = (new HandlerMethodArgumentResolverComposite()).addResolvers(handlers);
    

    if(this.initBinderArgumentResolvers == null) 
        handlers = this.getDefaultInitBinderArgumentResolvers();
        this.initBinderArgumentResolvers = (new HandlerMethodArgumentResolverComposite()).addResolvers(handlers);
    

    if(this.returnValueHandlers == null) 
        handlers = this.getDefaultReturnValueHandlers();
        this.returnValueHandlers = (new HandlerMethodReturnValueHandlerComposite()).addHandlers(handlers);
    




private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() 
    List<HandlerMethodArgumentResolver> resolvers = new ArrayList();
    resolvers.add(new RequestParamMethodArgumentResolver(this.getBeanFactory(), false));
    resolvers.add(new RequestParamMapMethodArgumentResolver());
    resolvers.add(new PathVariableMethodArgumentResolver());
    resolvers.add(new PathVariableMapMethodArgumentResolver());
    resolvers.add(new MatrixVariableMethodArgumentResolver());
    resolvers.add(new MatrixVariableMapMethodArgumentResolver());
    resolvers.add(new ServletModelAttributeMethodProcessor(false));
    resolvers.add(new RequestResponseBodyMethodProcessor(this.getMessageConverters(), this.requestResponseBodyAdvice));
    resolvers.add(new RequestPartMethodArgumentResolver(this.getMessageConverters(), this.requestResponseBodyAdvice));
    resolvers.add(new RequestHeaderMethodArgumentResolver(this.getBeanFactory()));
    resolvers.add(new RequestHeaderMapMethodArgumentResolver());
    resolvers.add(new ServletCookieValueMethodArgumentResolver(this.getBeanFactory()));
    resolvers.add(new ExpressionValueMethodArgumentResolver(this.getBeanFactory()));
    resolvers.add(new SessionAttributeMethodArgumentResolver());
    resolvers.add(new RequestAttributeMethodArgumentResolver());
    resolvers.add(new ServletRequestMethodArgumentResolver());
    resolvers.add(new ServletResponseMethodArgumentResolver());
    resolvers.add(new HttpEntityMethodProcessor(this.getMessageConverters(), this.requestResponseBodyAdvice));
    resolvers.add(new RedirectAttributesMethodArgumentResolver());
    resolvers.add(new ModelMethodProcessor());
    resolvers.add(new MapMethodProcessor());
    resolvers.add(new ErrorsMethodArgumentResolver());
    resolvers.add(new SessionStatusMethodArgumentResolver());
    resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
    if(this.getCustomArgumentResolvers() != null) 
        resolvers.addAll(this.getCustomArgumentResolvers());
    

    resolvers.add(new RequestParamMethodArgumentResolver(this.getBeanFactory(), true));
    resolvers.add(new ServletModelAttributeMethodProcessor(true));
    return resolvers;

继续看,回到文章的最开头,我们的接口参数是使用@RequestBody 注解

@PostMapping(value = "/conditionConfig")
public BaseResponse<PageInfo<ConfigDTO>> conditionConfig(@Valid @RequestBody ConfigQuery configQuery) 
    return configConsumer.conditionConfig(configQuery);

因此,参数的解析工作就是由RequestResponseBodyMethodProcessor来完成,来看下该类的resolveArgument方法

public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception 
    parameter = parameter.nestedIfOptional();
    //重点:解析参数
    Object arg = this.readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
    String name = Conventions.getVariableNameForParameter(parameter);
    WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
    if(arg != null) 
        this.validateIfApplicable(binder, parameter);
        if(binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) 
            throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
        
    

    mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
    return this.adaptArgumentIfNecessary(arg, parameter);

继续看readWithMessageConverters 方法

protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter, Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException 
    HttpServletRequest servletRequest = (HttpServletRequest)webRequest.getNativeRequest(HttpServletRequest.class);
    ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
    //重点,解析参数
    Object arg = this.readWithMessageConverters(inputMessage, parameter, paramType);
    if(arg == null && this.checkRequired(parameter)) 
        throw new HttpMessageNotReadableException("Required request body is missing: " + parameter.getMethod().toGenericString());
     else 
        return arg;
    

AbstractMessageConverterMethodArgumentResolver

protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException 
    boolean noContentType = false;

    MediaType contentType;
    try 
        contentType = inputMessage.getHeaders().getContentType();
     catch (InvalidMediaTypeException var14) 
        throw new HttpMediaTypeNotSupportedException(var14.getMessage());
    

    if(contentType == null) 
        noContentType = true;
        contentType = MediaType.APPLICATION_OCTET_STREAM;
    

    Class<?> contextClass = parameter != null?parameter.getContainingClass():null;
    Class<T> targetClass = targetType instanceof Class?(Class)targetType:null;
    if(targetClass == null) 
        ResolvableType resolvableType = parameter != null?ResolvableType.forMethodParameter(parameter):ResolvableType.forType(targetType);
        targetClass = resolvableType.resolve();
    

    HttpMethod httpMethod = ((HttpRequest)inputMessage).getMethod();
    Object body = NO_VALUE;

    Object inputMessage;
    try 
        inputMessage = new AbstractMessageConverterMethodArgumentResolver.EmptyBodyCheckingHttpInputMessage(inputMessage);
        /*
        重点:有一系列的message converter
        */
        Iterator var10 = this.messageConverters.iterator();

        while(var10.hasNext()) 
            HttpMessageConverter<?> converter = (HttpMessageConverter)var10.next();
            Class<HttpMessageConverter<?>> converterType = converter.getClass();
            if(converter instanceof GenericHttpMessageConverter) 
                GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter)converter;
                //看此converter是否能解析该类型的参数数据,如果能,则进行解析
                //例如:MappingJackson2HttpMessageConvertor 就能解析二进制流到我们定义的对象。
                if(genericConverter.canRead(targetType, contextClass, contentType)) 
                    if(this.logger.isDebugEnabled()) 
                        this.logger.debug("Read [" + targetType + "] as \\"" + contentType + "\\" with [" + converter + "]");
                    

                    if(((HttpInputMessage)inputMessage).getBody() != null) 
                        inputMessage = this.getAdvice().beforeBodyRead((HttpInputMessage)inputMessage, parameter, targetType, converterType);
                        //真正利用MappingJackson2HttpMessageConvertor此converter来进行解析。
                        body = genericConverter.read(targetType, contextClass, (HttpInputMessage)inputMessage);
                        body = this.getAdvice().afterBodyRead(body, (HttpInputMessage)inputMessage, parameter, targetType, converterType);
                     else 
                        body = this.getAdvice().handleEmptyBody((Object)null, (HttpInputMessage)inputMessage, parameter, targetType, converterType);
                    
                    break;
                
             else if(targetClass != null && converter.canRead(targetClass, contentType)) 
                if(this.logger.isDebugEnabled()) 
                    this.logger.debug("Read [" + targetType + "] as \\"" + contentType + "\\" with [" + converter + "]");
                

                if(((HttpInputMessage)inputMessage).getBody() != null) 
                    inputMessage = this.getAdvice().beforeBodyRead((HttpInputMessage)inputMessage, parameter, targetType, converterType);
                    body = converter.read(targetClass, (HttpInputMessage)inputMessage);
                    body = this.getAdvice().afterBodyRead(body, (HttpInputMessage)inputMessage, parameter, targetType, converterType);
                 else 
                    body = this.getAdvice().handleEmptyBody((Object)null, (HttpInputMessage)inputMessage, parameter, targetType, converterType);
                
                break;
            
        
     catch (IOException var15) 
        throw new HttpMessageNotReadableException("Could not read document: " + var15.getMessage(), var15);
    

    if(body != NO_VALUE) 
        return body;
     else if(httpMethod != null && SUPPORTED_METHODS.contains(httpMethod) && (!noContentType || ((HttpInputMessage)inputMessage).getBody() != null)) 
        throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
     else 
        return null;
    

readWithMessageConverters方法中逻辑如下:

1)遍历每个MessageConverter,看是否能对此类型的参数进行解析

2)如果能,则就交由此MessageConverter来解析。

AbstractMessageConverterMethodArgumentResolver解析器中包括一系列的MessageConverter,其中就包括:MappingJackson2HttpMessageConvertor,这个convertor就是用来将前端传过来的Json字符串转换为对象的。而MappingJackson2HttpMessageConvertor 内部是借助于Jackson来完成json到对象之间的转换。

也就是说:Jackson发现json中的startTime是Long类型,而对象是中Date类型,因此就将Long类型的时间戳转换为了Date对象。

到这里,我们就将其实现思路给了解清楚了,希望对你有收获。

最后,真的好就好久没有写文章了。

以上是关于Spring 是如何将前端请求中的参数解析到指定对象的的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot 前端请求参数自动映射到枚举,后端响应JSON数据自动解析枚举

如何指定一个请求参数绑定到某个控制器方法参数而不是Spring MVC中的模型属性?

自定义spring参数注解 - 打破@RequestBody单体限制

如何解析json字符串及返回json数据到前端

Spring MVC - 重定向后保留请求参数

源码剖析Spring MVC如何将请求映射到Controller?