SpringMVC的请求方法参数解析原理
环境: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());
这一行
可以看到,之前的mappedHandler = getHandler(processedRequest);
这一行代码是确定了请求的handler方法,也就是确定Controller的处理方法。那么,HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
这一行是为找到的handler再确定一个HandlerAdapter适配器,来解决参数处理问题
我们可以查看寻找HandlerAdapter的过程
这个过程和寻找Handler方法类似,也就是在定义好的4种HanderAdapter中遍历,这四种适配器分别为
RequestMappingHandlerAdapter
:支持方法上标注@RequestMapping注解的HandlerFunctionAdapter
:支持函数式编程的HttpRequestHandlerAdapter
SimpleControllerHandlerAdapter
1.2 执行目标方法
那么对于此次请求,找到了RequestMappingHandlerAdapter
这个适配器,接着进行了一系列验证之后,真正执行目标方法的代码为doDispatch方法中的这一行
我们查看这一方法的执行,进入到RequestMappingHandlerAdapter
类中,发现真正执行的是这个类的handleInternal()
方法,而在这个方法中,关键是这一行代码
进入invokeHandlerMethod
这一方法,在这一方法的前几行,可以看到这样的代码
这是为invocableMethod
也就是调用方法设置参数解析器和返回值处理器
1.3 参数解析器HandlerMethodArgumentResolver
参数解析器的作用是确定将要执行的目标方法的每一个参数的值是什么,而在SpringMVC中,目标方法能写多少种类型的参数,就取决于参数解析器的个数
我们查看argumentResolvers
参数的值
总共有27个参数解析器,可以看到,RequestParamMethodArgumentResolver
是支持@RequestParam
注解的参数的解析器,ServletRequestMethodArgumentResolver
是支持ServletRequest
或者HttpSession
参数的解析器
参数解析器是一个接口类,有两个方法,分别是supportsParameter
和resolveArgument
,它的作用流程是首先调用supportsParameter
判断解析器是否支持当前参数,如果支持,就再调用resolveArgument
解析此参数
1.4 返回值处理器HandlerMethodReturnValueHandlers
同样,返回值处理器也确定了Controller方法能返回的值的种类
我们查看returnValueHandlers
参数的值
总共有15个返回值处理器
1.5 反射调用方法
再对invocableMethod
进行一系列的封装之后,最后执行invocableMethod.invokeAndHandle(webRequest, mavContainer);
这一行代码,我们进入invokeAndHandle
方法,这是在ServletInvocableHandlerMethod
类中
在这个方法内,我们放行第一行代码Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
,发现会跳转到相应的Controller方法中,说明这就是真正执行的方法
我们继续进入invokeForRequest
方法中
可以看到第一行代码是确定方法参数值,而最后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. 目标方法执行完成
我们可以在目标方法参数中使用Map,Model或者HttpServletRequest等复杂参数
,这三者都可以给request域中放入数据。对于Map参数和Model参数,都有相应的参数解析器HandlerMethodArgumentResolver来对其进行处理。在目标方法执行完成后,将所有的数据都放在ModelAndViewContainer
中,包含了要去的页面地址view和存放的数据model
4. 处理派发结果
在目标方法执行完成之后,最终代码会来到这一行processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
,也就是对目标方法执行完成后得到的ModelAndView
进行处理,渲染模型
5. 自定义参数绑定原理
对于请求方法中注解参数、原生Servlet API参数和复杂参数,都有相应的参数解析器进行解析。对于自定义的参数,SpringMVC会对页面提交的请求数据和对象属性进行绑定
比如有这样的对象类
@Data
public class Person {
private String username;
private Integer age;
private Date birth;
private Pet pet;
}
@Data
public class Pet {
private String name;
private Integer age;
}
前端的提交为
<form action="/saveuser" method="post">
name: <input name="username" value="Tom"/><br/>
age: <input name="age" value="18"/><br/>
birthday: <input name="birth" value="2000/11/11"/><br/>
pet's name: <input name="pet.name" value="tomcat"/><br/>
pet's age: <input name="pet.age" value="3"/><br/>
<input type="submit" value="submit">
</form>
后端的处理方法为
@PostMapping("/saveuser")
public Person test5(Person person) {
return person ;
}
发起请求后,页面会显示Person类的json数据。那么,SpringMVC是如何对请求参数和对象进行绑定的呢
5.1 ServletModelAttributeMethodProcessor
我们开启debug,进入2.1小节的代码处,对for循环进行遍历,查看到底是哪一个参数解析器会支持此参数
最后,我们发现是ServletModelAttributeMethodProcessor
这个参数解析器,所以自定义类型参数是由ServletModelAttributeMethodProcessor
这个参数解析器进行处理
5.2 判断参数不是简单实体类型
我们可以查看这个类是如何判断能够处理自定义类型参数的
可以看到,如果参数上有标注@ModelAttribute
注解或者参数不是简单实体(Simple Property)的参数,就代表这个参数解析器是支持的。那么,什么是简单实体的参数呢?我们step into这个方法
如果这个类是简单类型(Simple Value Type)或者这是一个数组但数据元素是简单类型,那就代表这个类是简单类型的。至于什么是简单类型,
满足以上条件就代表这个类是简单类型
5.3 解析参数,进行参数绑定
在判断是否支持方法参数之后,就进行解析参数的过程
在这个过程中,会首先创建出对应的空对象
比如在这里,创建出了空的Person对象实例attribute。接下来就会对这个实例进行数据绑定
if (bindingResult == null) {
// Bean property binding and validation;
// skipped in case of binding failure on construction.
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
,这里的WebDataBinder
是web数据绑定器,作用是将请求参数的值绑定到指定的JavaBean里面
我们查看这个binder
,其中之前创建的attribute
还有许多的converter
类型转换器
可以看到总共有124个类型转换器。因为我们要将请求中的数据绑定到JavaBean中,而传过来的请求中的数据根据HTTP协议是文本类型的,所以需要多种类型转换器将各文本数据转换成相应的JavaBean中各类型的属性
最后,进行数据绑定过程
再进行完这一步之后,我们可以看到attribute已经不是一个空对象
至于这一步bindRequestParameters(binder, webRequest);
具体干了些啥,可以查看底层源码,大概就是先进行数据转换,将文本类型转换为目标类型,再对JavaBean进行属性赋值,这样就完成了JavaBean的封装
6. 总结
对于用注解标注的参数,原生Servlet API的参数,复杂参数还是自定义类型的参数,SpringMVC定义好了27个参数解析器HandlerMethodArgumentResolver来对这些参数进行处理。在解析方法参数的过程中,先调用resolvers.supportsParameter
判断参数解析器是否支持该方法参数,支持的话就调用resolvers.resolveArgument
解析出参数,所以参数解析器的个数就决定了可以使用的方法参数的类型个数
每个参数解析器解析参数的过程是不同的,尤其是对于自定义类型的参数,首先要对请求中的数据进行类型转换,再封装为相应的JavaBean目标对象
在进行方法参数解析之后,会执行相应的Controller方法,如果方法中有Map或者Model或者request参数,会将这些数据放在ModelAndViewContainer
中,这个类包含要去的页面地址view和存放的数据model
最后进行派发结果的处理,渲染出模型