SpringBoot2各类型参数解析原理(源码分析)
Posted AC_Jobim
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot2各类型参数解析原理(源码分析)相关的知识,希望对你有一定的参考价值。
SpringBoot2各类型参数解析原理(源码分析)
环境:SpringBoot 2.5.2
一、使用注解来获取请求参数
测试链接:http://localhost:8080/car/2/owner/zhangsan?age=15&inters=football
测试方法:
@GetMapping("/car/{id}/owner/{username}")
public Map<String,Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String name,
@PathVariable Map<String,String> pv,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String,String> header,
@RequestParam("age") Integer age,
@RequestParam("inters") List<String> inters,
@RequestParam Map<String,String> params){
Map<String,Object> map = new HashMap<>();
map.put("id",id);
map.put("name",name);
map.put("pv",pv);
map.put("userAgent",userAgent);
map.put("headers",header);
map.put("age",age);
map.put("inters",inters);
map.put("params",params);
return map;
}
1.1 获取HandlerAdapter
对doDispatch()方法
进行debug,mappedHandler = getHandler(processedRequest);
这一行代码是确定了请求的handler方法,也就是确定Controller的处理方法。那么,HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
这一行是为找到的handler再确定一个HandlerAdapter适配器,来解决参数处理问题。
getHandlerAdapter()方法,为当前Handler找一个适配器HandlerAdapter
寻找Handler方法类似,也就是在定义好的4种HanderAdapter中遍历,这四种适配器分别为:
RequestMappingHandlerAdapter
:支持方法上标注@RequestMapping注解的HandlerFunctionAdapter
:支持函数式编程的HttpRequestHandlerAdapter
:业务自行处理请求,不需要通过modelAndView转到试图SimpleControllerHandlerAdapter
:标准控制器,返回ModelAndView
getHandlerAdapter里调用了adapter.supports(handler)
方法来确定adapter。
public final boolean supports(Object handler) {
return handler instanceof HandlerMethod && this.supportsInternal((HandlerMethod)handler);
}
最终getHandlerAdapter()
方法方法返回了RequestMappingHandlerAdapter
1.2 执行目标方法
找到了RequestMappingHandlerAdapter
这个适配器,接着进行了一系列验证之后,真正执行目标方法的代码为doDispatch方法中的这一行
我们查看这一方法的执行,进入到RequestMappingHandlerAdapter
类中,发现真正执行的是这个类的handleInternal()
方法,而在这个方法中,关键是这一行代码
进入invokeHandlerMethod()
方法,有两行关键代码
这是为invocableMethod
也就是调用方法设置参数解析器和返回值处理器
1.2.1 参数解析器(HandlerMethodArgumentResolver)
HandlerMethodArgumentResolver是一个接口
public interface HandlerMethodArgumentResolver {
//解析器是否支持当前参数
boolean supportsParameter(MethodParameter var1);
将request中的请求参数解析到当前Controller参数上,在这里进行类型转换
@Nullable
Object resolveArgument(MethodParameter var1, @Nullable ModelAndViewContainer var2, NativeWebRequest var3, @Nullable WebDataBinderFactory var4) throws Exception;
}
参数解析器的作用判断是否支持解析这种参数,支持就调用 resolveArgument解析最终确定将要执行的目标方法的每一个参数的值是什么,而在SpringMVC中,目标方法能写多少种类型的参数,就取决于参数解析器的个数。
HandlerMethodArgymentResolver接口
1.2.2 返回值处理器(HandlerMethodReturnValueHandler)
返回值处理器也确定了Controller方法能返回的值的种类
1.2.3 反射调用方法
再对invocableMethod
进行一系列的封装之后,最后执行invocableMethod.invokeAndHandle(webRequest, mavContainer);
这一行代码,我们进入invokeAndHandle
方法,这是在ServletInvocableHandlerMethod
类中
进入invokeForRequest()
方法中
1.2.4 确定目标方法的参数值详细
进入getMethodArgumentValues方法
,这个方法便是确定目标方法参数值的详细过程
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
MethodParameter[] parameters = this.getMethodParameters(); // 获取目标方法的参数列表
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
} else {
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) {
if (!this.resolvers.supportsParameter(parameter)) { // 判断当前的参数解析器中是否有支持此方法参数的参数解析器,详见1.2.4.1
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); // 判断当前的参数解析器中是否有支持此方法参数的参数解析器,详见1.2.4.2
} catch (Exception var10) {
if (logger.isDebugEnabled()) {
String exMsg = var10.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw var10;
}
}
}
return args;
}
}
1.2.4.1 resolvers.supportsParameter(parameter)
判断在27个参数解析器中是否存在支持当前方法参数的参数解析器,我们可以看到方法内部挨个遍历确认支持的参数解析器。另外,其中还用到了argumentResolverCache
缓存,如果以后有同一类型的参数进来,就可以直接从缓存中获取
public boolean supportsParameter(MethodParameter parameter) {
return this.getArgumentResolver(parameter) != null; //检查支持该参数的参数解析器是否为null
}
//挨个判断所有参数解析器哪个支持解析这个参数,并返回该参数解析器
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter); //先从缓存中找
if (result == null) {
Iterator var3 = this.argumentResolvers.iterator();
while(var3.hasNext()) {
HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, resolver); //如果找到则放入缓存中
break;
}
}
}
}
1.2.4.2 resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory)
@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("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
} else {
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); // 解析参数
}
}
resolveArgument内部会再配合UrlPathHelper(会将url中的变量解析出来,放在request的请求域中),最终得到变量值。
二、对于传入的是Servlet API的参数的处理
测试代码:
@GetMapping("/goto")
public String goToPage(HttpServletRequest request) {
request.setAttribute("msg","成功了...");
request.setAttribute("code",200);
return "forward:/success"; //转发到 /success请求
}
当传入参数类型为HttpServletRequest时,请求最终还是会进入到resolvers.supportsParameter(parameter)
方法
public boolean supportsParameter(MethodParameter parameter) {
return this.getArgumentResolver(parameter) != null; //检查支持该参数的参数解析器是否为null
}
//挨个判断所有参数解析器哪个支持解析这个参数,并返回该参数解析器
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter); //先从缓存中找
if (result == null) {
Iterator var3 = this.argumentResolvers.iterator();
while(var3.hasNext()) {
HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, resolver); //如果找到则放入缓存中
break;
}
}
}
}
遍历那27个参数解析器,当遍历到ServletRequestMethodArgumentResolver
时,进入supportsParameter
方法
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
Principal.class.isAssignableFrom(paramType) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}
可以看出WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
这些都能通过ServletRequestMethodArgumentResolver
进行解析。
三、请求方法参数为复杂参数
Map、Model
(map、model里面的数据会被放在request的请求域 request.setAttribute)、Errors/BindingResult、RedirectAttributes( 重定向携带数据)、ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilde也可以作为请求参数
我们在编写接口时会传入复杂参数,如Map、Model等,这种类似的参数会有相应的参数解析器进行解析,并且最后会将解析出的值放到request域中,下面我们一起来探析一下其中的原理。
测试链接:http://localhost:8080/params
测试代码:
@GetMapping("params")
public String testParam(Map<String,String> map,
Model model,
HttpServletRequest request,
HttpServletResponse response) {
map.put("hello","hello666");
model.addAttribute("world","world666");
request.setAttribute("message","HelloWorld");
Cookie cookie = new Cookie("c1","v1");
response.addCookie(cookie);
return "forward:/success";
}
3.1 获得目标方法的参数对象
获得目标方法的参数对象,对于Map、Model
类型的参数,其值就是mavContainer的model属性
方法调用栈:
- 首先,解析参数都是跟上面一样的步骤,就是进行到resolvers.supportsParameter(parameter)这个方法后,遍历那26个参数解析器,拿到对应的解析器去解析,而对于
map
这类型的数据来说,就是对应的MapMerhodProcessor
解析器
- 之后进行试图解析:
- 拿到解析器后,就得找对应的参数进行影射了,对于上面的使用@requestmapping注解的方法,它会去url缓存中获取参数值。而对于map类型的参数,它来到了
mavContainer.getModel()
这个方法,准备获取模型数据。
而getModel()
这个方法他会返回一个ModeMap类型的数据
最终,他是返回一个ModelMap的子类BindingAwareModelMap
BindingAwareModelMap的继承树
- 然后开始解析下一个
Model
类型的参数
视图解析的时候也调用getModel()
方法返回一个ModeMap类型的数据
debug后发现,居然跟解析Map类型调用的是一样的方法,而且两者返回的是同一个BindingAwareModelMap
3.2 执行目标请求方法,设置mavContainer的model属性
执行目标请求方法,并将目标方法的返回值进行返回
进入到请求方法内部
执行完目标方法后mavContainer
中已经放入了数据
3.3 设置mavContainer的view属性
之后调用HandlerMethodReturnValueHandlerComposite的handleReturnValue()
方法处理返回值
其内部又调用handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest)
方法
此时的mavContainer(view已经为“”forward:/success)
3.4 获取ModelAndView对象(将mavContainer对象封装)
之后执行RequestMappingHandlerAdapter的getModelAndView()
方法,返回ModelAndView
将所有的数据都放在 ModelAndView中;包含要去的页面地址View。还包含Model数据。
3.5 将数据放到request域中
此时doDispatch()
方法中已经获得了带有数据的ModelAndView对象
方法的调用栈:
开始执行DispatchServlet的另一个方法processDispatchResult()
方法,用来处理分发结果
然后又调用了render()
方法,最终调用了InternalResourceView类中的renderMergedOutputModel()
方法
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
this.exposeModelAsRequestAttributes(model, request); //暴露模型作为请求域属性
this.exposeHelpers(request);
String dispatcherPath = this.prepareForRendering(request, response);
RequestDispatcher rd = this.getRequestDispatcher(request, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + this.getUrl() + "]: Check that the corresponding file exists within your web application archive!");
} else {
if (this.useInclude(request, response)) {
response.setContentType(this.getContentType());
if (this.logger.isDebugEnabled()) {
this.logger.debug("Including [" + this.getUrl() + "]");
}
rd.include(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Forwarding to [" + this.getUrl() + "]");
}
rd.forward(request, response);
}
}
}
exposeModelAsRequestAttributes()
方法就是将model数据重新放入请求域中
protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception {
//model中的所有数据遍历挨个放在请求域中
model.forEach((name,以上是关于SpringBoot2各类型参数解析原理(源码分析)的主要内容,如果未能解决你的问题,请参考以下文章
Spring Boot 2从入门到入坟 | 请求参数处理篇:源码分析之各种类型参数解析原理
SpringBoot2之web开发(上)——之静态资源和请求参数处理