本文来自作者
列苗
在
GitChat
上分享 「Spring MVC 源码解析: HTTP 请求与响应过程」,
「
阅读原文
」
查看交流实录。
「
文末高能
」
编辑 | 哈比
本文主要根据两种常见的错误场景展开,深入解析 SpringMVC HTTP 请求与响应流程。而整个 SpringMVC HTTP 请求与响应过程涉及的内容远不止于此。主要内容如下:
-
常见的两种错误场景介绍;
-
HTTP 请求与处理源码解析;
-
两种错误场景解决方案;
-
涉及的设计模式介绍。
相关版本:
-
Maven : apache-maven-3.3.9;
-
SpringMVC :4.1.1.RELEASE;
-
Tomcat-Maven-Plugin: 2.2。
介绍方式:代码 + 文字说明 + 源码截图(为减小篇幅,因此源码部分采用截图的方式)。读者阅读时,结合前面列出的流程图/主要操作步骤,再浏览。
一、常见的两种错误场景
本文中涉及的代码下载:https://github.com/wlmshuaia/JsonDemo。
1.场景1
jQuery 以 ajax 方式访问 SpringMVC 接口时,如未显示指定 Content-Type,则会显示 ‘415 (Unsupported Media Type)’ 错误。如:
前端代码片段:
function fSave(url) { var obj = {};
obj['cateId'] = $("input[name=cateId]").val();
obj['cateName'] = $("input[name=cateName]").val();
$.ajax({
url: url,
method: 'post',
data: JSON.stringify(obj),
success: function(data) { console.log(data);
},
error: function(data) { console.log("error..."); console.log(data);
}
});
}
后端接口代码片段为:
@RequestMapping(value = "/save-by-model-2", method = RequestMethod.POST)@ResponseBodypublic Category saveByModel2(@RequestBody Category category){
categoryService.save(category); return category;
}
2. 场景2
SpringMVC 接口定义返回 Json 格式数据时,一般有
字符串和对象
两种方式。而相同条件下,返回 Json 格式字符串时,中文会出现乱码。如:
前端代码片段:
function fSave(url) { var obj = {};
obj['cateId'] = $("input[name=cateId]").val();
obj['cateName'] = $("input[name=cateName]").val();
$.ajax({
url: url,
method: 'post',
contentType: 'application/json',
data: JSON.stringify(obj),
success: function(data) { console.log(data);
},
error: function(data) { console.log("error..."); console.log(data);
}
});
}
后端接口片段:
@RequestMapping(value = "/save-by-map", method = RequestMethod.POST)@ResponseBodypublic String saveByMap(@RequestBody Map valMap) {
categoryService.save(valMap); return valMap.toString();
}@RequestMapping(value = "/save-by-map-2", method = RequestMethod.POST)@ResponseBodypublic Map saveByMap2(@RequestBody Map valMap) {
categoryService.save(valMap); return valMap;
}
二、HTTP 请求与响应处理源码解析
SpringMVC 中 HTTP 请求处理流程时序图如下:
从上图可以看出,所有的 HTTP 请求都会进入到
DispatcherServlet
类的
doDispatch()
方法。该方法中的主要工作为:
-
获取处理执行链对象:
HandlerExecutionChain mappedHandler = getHandler(processedRequest);
-
获取处理适配器:
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
-
调用拦截器的
preHandle
方法;
-
调用具体的接口方法,并返回模型视图对象:
mv = ha.handle(processedRequest, response,mappedHandler.getHandler());
-
调用拦截器的
postHandle
方法;
-
处理结果:
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
。
注:本次源码解析为通过
@EnableWebMvc
方式启动,与配置文件方式启动时的源码略有不同(该方式在 Spring 3.2 以后已被弃用),如
HandlerMapping, HandlerAdapter
的实现 。
接下来深入 SpringMVC 处理 HTTP 请求的过程。
1.获取处理执行链对象
处理执行链类
HandlerExecutionChain
: 由处理对象
handler
和 拦截器列表
interceptorList
组成,通过
HandlerMapping.getHandler()
方法返回。
DispatcherServlet
类中获取处理链对象的
getHandler(HttpServletRequest request)
方法实现为:
从上图可以看到,框架遍历所有的
HandlerMapping
对象,调用对应的
hm.getHandler(request)
方法,如果获取的处理执行链对象不为
null
则返回该处理执行链对象。
HandlerMapping
: 定义请求和处理对象之间的映射关系接口。
HandlerMapping
列表由 SpringMVC 初始化时寻找所有的
HandlerMapping
类组成。
SpringMVC 中,默认的
HandlerMapping
实现有:
上图
HandlerMapping
列表中,
RequestMappingHandlerMapping
类为
@Controller
注解的类中的
@RequestMapping
注解方法
创建
RequestMappingInfo
对象。而前面示例中定义的接口方法为
@RequestMapping
类型,因此此处取得的
HandlerMapping
类为该类。简化的类图如下:
RequestMappingInfo
封装了
@RequestMapping
注解方法相关的状态如下:
而在
RequestMappingHandlerMapping
类中获取处理链对象则由父类
AbstractHandlerMapping
实现:
在
AbstractHandlerMapping
中,主要操作为2步:
-
获取处理器对象;
-
获取处理执行链。
1.1 获取处理器对象
由子类
AbstractHandlerMethodMapping
实现:
上图实现中返回的处理器对象类型为处理方法 (
HandlerMethod
)。主要操作为:
-
调用
UrlPathHelper
类从
request
中获取请求路径
lookupPath
;
-
根据请求路径获取处理方法。
在第2步中,
SpringMVC 会根据
lookupPath
作为 key 值从缓存的
urlMap
对象中获取
HandlerMethod
,
如果根据 key 值未获取到对应的数据,则会遍历所有的
urlMap
数据列表
。
注:
urlMap
中的数据在 SpringMVC 初始化时填入。
addMatchingMappings()
方法中的实现为:
由于该机制的存在,如果定义的
@RequestMapping
与访问的路径不相等,则框架会遍历所有的
@RequestMapping
方法。如果数量很多时,则该部分会消耗较多时间。因此如该部分有性能问题,应尽量使
@RequestMapping
路径与访问路径匹配以减少遍历开销。
优化建议
-
SpringMVC-4.0.3 以后的版本可通过
WebMvcConfigurerAdapter.configurePathMatch(PathMatchConfigurer configurer)
方法自定义实现
UrlPathHelper, AntPathMatcher
优化该问题;
-
修改
RequestMappingHandlerMapping
类中
UrlPathHelper, AntPathMatcher
的实现 (
代码参考
);
-
定义
@RequestMapping
路径与访问路径一致,如访问路径为 ‘index.do’,则相应的
@RequestMapping(value = "/index.do")
;
-
减少单个项目中的
@RequestMapping
的数量。
1.2 获取处理执行链
该方法根据传入的处理器对象建立处理执行链,将传入的处理对象和匹配的拦截器添加到处理执行链对象中,并返回:
2.获取处理适配器
HandlerAdapter
:
MVC 框架的 SPI 接口,允许核心MVC工作流的参数化(这句话不是很懂…有会的读者欢迎指教)。
每一种处理请求的处理器类型必须实现该接口,
DispatcherServlet
类可根据该接口无限扩展,且
DispatcherServlet
根据该接口访问所有已安装的处理器对象
。
而处理器类型设置为
Object
,其他的框架不用修改源码就能和 SpringMVC 整合。完整的接口介绍如下:
传入处理器对象,SpringMVC 遍历所有的
HandlerAdapter
类,如果某个处理适配器支持该处理器类型,则返回该处理器:
supports(handler)
接口判断某个具体的处理适配器是否支持传入的处理器对象,定义如下:
在
RequestMappingHandlerAdapter
类中,支持的处理器类型为
HandlerMethod
:
因此此处返回的
HandlerAdapter
实现类为:
RequestMappingHandlerAdapter
。
3.调用拦截器的
preHandle
方法
调用处理执行链对象的
applyPreHandle()
方法,代码如下:
可以看出框架会遍历在
第1步:获取处理链对象
中获取的拦截器对象,依次调用
preHandle()
方法,如果某次调用返回
false
,则会调用
triggerAfterCompletion()
方法,并返回
false
。
triggerAfterCompletion()
实现如下:
即依次调用拦截器对象的
afterCompletion()
方法。
4.调用具体的接口方法
调用
第2步:获取的处理适配器
对象的
handle()
方法处理请求。而
RequestMappingHandlerAdapter
类中处理请求的是
handleInternal()
方法。方法执行时序图如下:
从上图中可以看出,框架先创建
ServletInvocableHandlerMethod
对象,然后调用该对象的
invokeAndHandle(webRequest, mavContainer)
方法,最后获取
ModelAndView
对象。具体实现如下:
而
invokeAndHandle()
方法中,主要操作为:
-
根据
request
解析参数,并调用具体的方法,获取返回值(即调用
invokeForRequest()
方法);
-
调用返回值处理器处理返回值。
4.1 调用具体的方法
该部分操作步骤为:
-
解析参数;
-
根据参数调用具体的方法,并获取返回值;
-
返回返回值。
解析参数时,会先获取方法定义的所有参数列表,然后根据每个具体的
MethodParameter
类型,调用具体的参数解析器类
HandlerMethodArgumentResolver
的解析参数方法
resolveArgument()
。
源码如下:
此处的
argumentResolvers
对象为
HandlerMethodArgumentResolverComposite
类,该类封装了一系列的参数解析器,属性如下:
解析时,根据
HandlerMethodArgumentResolver.supportsParameter(MethodParameter methodParameter)
判断该参数解析器是否支持该参数。
前面参数由于使用的是
@RequestBody
注解,因此调用
RequestResponseBodyMethodProcessor
类,典型的还有
@RequestParam, @PathVariable, @RequestHeader
等:
而
RequestResponseBodyMethodProcessor
类处理参数时,使用了
HttpMessageConverter
消息转换机制。
4.1.1
HttpMessageConverter
消息转换机制
该机制的流程为:
由于请求的
content-type
为
application/json
类型,所以此处调用的消息转换类为
MappingJackson2HttpMessageConverter
,默认的字符集为
UTF-8
:
而
read()
的实现为调用
Jackson
的类
ObjectMapper.readValue()
方法:
注:
在调用
read()
方法之前,如果无匹配的
HttpMessageConverter
类,则会抛出
HttpMediaTypeNotSupportedException
异常;
在调用
write()
方法之前,如果无匹配的
HttpMessageConverter
类,则会抛出
HttpMediaTypeNotAcceptableException
异常。
解析完参数后
,则通过反射机制调用具体的方法并获取返回值:
4.2 处理返回值
该部分的逻辑和前面解析参数逻辑类似。主要为调用具体的
HandlerMethodReturnValueHandler
(返回值处理器) 处理返回值。
框架中默认的返回值处理器有:
此处由于使用
@ResponseBody
注解,因此调用
RequestResponseBodyMethodProcessor
处理器。该处理器中,使用
HttpMessageConverter
消息转换机制(见 4.1.1),调用
write()
方法处理返回值:
而在调用
write()
方法之前,会调用
getProducibleMediaTypes()
方法获取可生产的媒体类型,如果用户自定义
RequestMapping 的 produces
属性,则此处会返回该值;
如果用户未定义,则根据返回值
Class
类型,遍历系统中的消息转换器列表,获取支持的媒体类型列表:
5.调用拦截器的
postHandle
方法
调用拦截器对象的
applyPostHandle()
方法,代码如下:
从上图可以看出框架会遍历拦截器对象列表,以处理链对象中拦截器对象列表
相反的顺序
调用拦截器对象的
postHandle()
方法。
6.处理结果
调用
processDispatchResult()
方法,实现代码如下:
这部分处理主要分为如下几部分:
-
处理异常;
-
渲染
ModelAndView
对象;
-
调用处理执行链的
triggerAfterCompletion()
方法。
6.1 处理异常
如果在前面的处理中抛出了异常,则会获取相应的模型视图对象。
有两种处理方式:如果异常对象为
ModelAndViewDefiningException
类型,则直接获取模型视图对象;否则的话调用当前系统内的处理异常解析器 (
HandlerExceptionResolver
) 处理:
如果某个异常解析器返回了有效的模型视图对象,则跳出循环。
此处的
ExceptionHandlerExceptionResolver
类通过用户自定义的
@ExceptionHandler
方法解析异常,如果用户未定义,则跳出该解析器。此处示例代码为:
@ExceptionHandler(Exception.class)public String handlerException(HttpServletRequest request, HttpServletResponse response, Exception ex) {
System.out.println("handlerException..."); return "redirect:/error.do";
}
在该类的
doResolveHandlerMethodException()
方法中,会创建一个
ServletInvocableHandlerMethod
对象,然后调用该对象的
invokeAndHandle()
方法:
与前面处理正常
HandlerMethod
流程类似,就不深入探讨了。
6.2 渲染
ModelAndView
对象
在渲染方法
render()
中,如果传入的 mv 对象是
View
引用类型,即为
String
字符串时,则调用当前的视图解析器
ViewResolver
解析该字符串,如当前配置了视图解析器为:
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/"/>
<property name="suffix" value=".html"/>bean>
则该实现会在视图解析器列表
viewResolvers
中:
在解析时,将会添加上对应的
prefix, suffix
, 处理后的
View
对象为:
接下来则调用
View
对象的
render()
方法,根据提供的
Model
对象渲染该视图对象。
6.3 调用处理执行链的
triggerAfterCompletion()
方法
该方法只调用在
preHandle()
方法中成功调用且返回为
true
的拦截器,且从列表后往前调用:
三、解决方案
根据前面的原理介绍可知,文章开头的两种场景错误都是由于在第4步:调用具体的接口方法出错:场景1为解析参数时出错,场景2为返回值处理时出错。
1. 场景1出错原因
见 4.1.1 消息转换机制。场景1中,ajax 请求如果未设置
Content-Type
则会使用默认的类型:
application/x-www-form-urlencoded; charset=UTF-8
。
而由于接收参数定义为
@RequestBody
,对应的参数解析器为
RequestResponseBodyMethodProcessor
类,调用
HttpMessagConverter
消息转换机制。
而 SpringMVC 中并无支持该类型的
HttpMessageConverter
类,因此抛出异常。
2. 场景1解决方案
指定具体的 Content-Type, 如:
$.ajax({
url: url,
method: 'post',
contentType: 'application/json',
data: JSON.stringify(obj),
success: function(data) { console.log(data);
},
error: function(data) { console.log("error..."); console.log(data);
}
});
3. 场景2出错原因
见 4.2 处理返回值。场景2中,定义了
@ResponseBody
注解,调用的返回值处理器为
RequestResponseBodyMethodProcessor
,调用
HttpMessagConverter
消息转换机制。
返回的
Class
类型是
String
,而在 SpringMVC 的消息转换器列表中支持该返回值类型的消息转换器有
StringHttpMessageConverter, MappingJackson2HttpMessageConverter
支持的媒体列表有:
取得媒体列表后,会选取其中的一个: