SpringMVC的请求方法参数解析原理(一)——注解参数和原生Servlet API

环境:SpringBoot 2.4.2

SpringMVC在处理Web请求时可以接受的传参类型有多种,可以使用注解来获取请求参数,比如@RequestParam,可以使用Servlet API,比如HttpSession,可以使用复杂参数,比如Model和Map,可以使用自定义对象参数,比如自定义的Person类

本文探讨SpringMVC是如何处理这些参数的

1. 参数处理原理

1.1 HandlerAdapter

SpringMVC的关键类是DispatcherServlet,我们打开这个类的源代码,在doDispatch()方法处加上断点。发送一个请求,单步调试到HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());这一行

SpringMVC的请求方法参数解析原理(一)——注解参数和原生Servlet API

可以看到,之前的mappedHandler = getHandler(processedRequest);这一行代码是确定了请求的handler方法,也就是确定Controller的处理方法。那么,HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());这一行是为找到的handler再确定一个HandlerAdapter适配器,来解决参数处理问题

我们可以查看寻找HandlerAdapter的过程

SpringMVC的请求方法参数解析原理(一)——注解参数和原生Servlet API

这个过程和寻找Handler方法类似,也就是在定义好的4种HanderAdapter中遍历,这四种适配器分别为

  • RequestMappingHandlerAdapter:支持方法上标注@RequestMapping注解的
  • HandlerFunctionAdapter:支持函数式编程的
  • HttpRequestHandlerAdapter
  • SimpleControllerHandlerAdapter

1.2 执行目标方法

那么对于此次请求,找到了RequestMappingHandlerAdapter这个适配器,接着进行了一系列验证之后,真正执行目标方法的代码为doDispatch方法中的这一行

SpringMVC的请求方法参数解析原理(一)——注解参数和原生Servlet API

我们查看这一方法的执行,进入到RequestMappingHandlerAdapter类中,发现真正执行的是这个类的handleInternal()方法,而在这个方法中,关键是这一行代码

SpringMVC的请求方法参数解析原理(一)——注解参数和原生Servlet API

进入invokeHandlerMethod这一方法,在这一方法的前几行,可以看到这样的代码

SpringMVC的请求方法参数解析原理(一)——注解参数和原生Servlet API

这是为invocableMethod也就是调用方法设置参数解析器和返回值处理器

1.3 参数解析器HandlerMethodArgumentResolver

参数解析器的作用是确定将要执行的目标方法的每一个参数的值是什么,而在SpringMVC中,目标方法能写多少种类型的参数,就取决于参数解析器的个数

我们查看argumentResolvers参数的值

SpringMVC的请求方法参数解析原理(一)——注解参数和原生Servlet API
SpringMVC的请求方法参数解析原理(一)——注解参数和原生Servlet API

总共有27个参数解析器,可以看到,RequestParamMethodArgumentResolver是支持@RequestParam注解的参数的解析器,ServletRequestMethodArgumentResolver是支持ServletRequest或者HttpSession参数的解析器

参数解析器是一个接口类,有两个方法,分别是supportsParameterresolveArgument,它的作用流程是首先调用supportsParameter判断解析器是否支持当前参数,如果支持,就再调用resolveArgument解析此参数

1.4 返回值处理器HandlerMethodReturnValueHandlers

同样,返回值处理器也确定了Controller方法能返回的值的种类

我们查看returnValueHandlers参数的值

SpringMVC的请求方法参数解析原理(一)——注解参数和原生Servlet API

总共有15个返回值处理器

1.5 反射调用方法

再对invocableMethod进行一系列的封装之后,最后执行invocableMethod.invokeAndHandle(webRequest, mavContainer);这一行代码,我们进入invokeAndHandle方法,这是在ServletInvocableHandlerMethod类中

SpringMVC的请求方法参数解析原理(一)——注解参数和原生Servlet API

在这个方法内,我们放行第一行代码Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);,发现会跳转到相应的Controller方法中,说明这就是真正执行的方法

我们继续进入invokeForRequest方法中

SpringMVC的请求方法参数解析原理(一)——注解参数和原生Servlet API

可以看到第一行代码是确定方法参数值,而最后this.doInvoke(args)便是使用反射方式调用方法

2. 确定目标方法的参数值详细

在上文提到invokeForRequest方法中的第一行便是确定方法参数值,我们进入这一行的getMethodArgumentValues方法,这个方法便是确定目标方法参数值的详细过程

// InvocableHandlerMethod类
protected Object[] getMethodArgumentValues(NativeWebRequest request, 
                                           @Nullable ModelAndViewContainer mavContainer,
			                   Object... providedArgs) throws Exception {

    MethodParameter[] parameters = getMethodParameters();  // 获取目标方法的参数列表
    if (ObjectUtils.isEmpty(parameters)) {
	return EMPTY_ARGS;
    }

    Object[] args = new Object[parameters.length];  // 方法返回值:确定好的方法参数
    for (int i = 0; i < parameters.length; i++) {
        MethodParameter parameter = parameters[i];  // 获得参数列表的参数
        parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
        args[i] = findProvidedArgument(parameter, providedArgs);
        if (args[i] != null) {
	    continue;
        }
        if (!this.resolvers.supportsParameter(parameter)) {  // 判断当前的参数解析器中是否有支持此方法参数的参数解析器,详见2.1
	    throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
        }
        try {
	    args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);  // 使用参数解析器解析方法参数,详见2.2
        }
        catch (Exception ex) {
            // Leave stack trace for later, exception may actually be resolved and handled...
            if (logger.isDebugEnabled()) {
	        String exMsg = ex.getMessage();
	        if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
	            logger.debug(formatArgumentError(parameter, exMsg));
	        }
            }
	    throw ex;
        }
    }
    return args;
}

2.1 this.resolvers.supportsParameter(parameter)

这行代码为了判断在27个参数解析器中是否存在支持当前方法参数的参数解析器,我们可以看到方法内部是一个循环遍历,挨个遍历确认支持的参数解析器。另外,其中还用到了argumentResolverCache缓存,如果以后有同一类型的参数进来,就可以直接从缓存中获取

@Override
public boolean supportsParameter(MethodParameter parameter) {
    return getArgumentResolver(parameter) != null;
}

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

2.2 this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);

@Override
@Nullable
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);  // 解析参数
}

而在resolveArgument方法中分别执行Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);获取解析出参数名和参数值。需要注意的是,resolveName方法真正调用的是对应参数解析器中的这个方法

3. 总结

对于用注解标注的参数和原生Servlet API的参数,SpringMVC是通过参数解析器HandlerMethodArgumentResolver来处理参数的,在解析方法参数的过程中,先调用resolvers.supportsParameter判断参数解析器是否支持该方法参数,支持的话就调用resolvers.resolveArgument解析出参数,所以参数解析器的个数就决定了可以使用的方法参数的类型个数

上一篇:报错Ordinal parameter not bound


下一篇:Mybatis的dao层传递单参出现的问题