专栏名称: GitChat技术杂谈
GitChat是新时代的学习工具。
目录
相关文章推荐
码农翻身  ·  “DeepSeek出了一个昏招!” ·  昨天  
程序员的那些事  ·  OpenAI ... ·  2 天前  
程序员小灰  ·  DeepSeek + IDEA!辅助编程太强了! ·  2 天前  
程序猿  ·  本地部署 DeepSeek ... ·  3 天前  
程序员的那些事  ·  突发!o3-mini ... ·  5 天前  
51好读  ›  专栏  ›  GitChat技术杂谈

Spring MVC 源码解析: HTTP 请求与响应过程

GitChat技术杂谈  · 公众号  · 程序员  · 2018-01-23 07:15

正文


本文来自作者 列苗 GitChat 上分享 「Spring MVC 源码解析: HTTP 请求与响应过程」, 阅读原文 查看交流实录。

文末高能

编辑 | 哈比

本文主要根据两种常见的错误场景展开,深入解析 SpringMVC HTTP 请求与响应流程。而整个 SpringMVC HTTP 请求与响应过程涉及的内容远不止于此。主要内容如下:

  1. 常见的两种错误场景介绍;

  2. HTTP 请求与处理源码解析;

  3. 两种错误场景解决方案;

  4. 涉及的设计模式介绍。

相关版本:

  • 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), // 以json字符串方式传递        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), // 以json字符串方式传递        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() 方法。该方法中的主要工作为:

  1. 获取处理执行链对象: HandlerExecutionChain mappedHandler = getHandler(processedRequest);

  2. 获取处理适配器: HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

  3. 调用拦截器的 preHandle 方法;

  4. 调用具体的接口方法,并返回模型视图对象: mv = ha.handle(processedRequest, response,mappedHandler.getHandler());

  5. 调用拦截器的 postHandle 方法;

  6. 处理结果: 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. 获取处理器对象;

  2. 获取处理执行链。

1.1 获取处理器对象

由子类 AbstractHandlerMethodMapping 实现:

上图实现中返回的处理器对象类型为处理方法 ( HandlerMethod )。主要操作为:

  1. 调用 UrlPathHelper 类从 request 中获取请求路径 lookupPath

  2. 根据请求路径获取处理方法。

在第2步中, SpringMVC 会根据 lookupPath 作为 key 值从缓存的 urlMap 对象中获取 HandlerMethod 如果根据 key 值未获取到对应的数据,则会遍历所有的 urlMap 数据列表

注: urlMap 中的数据在 SpringMVC 初始化时填入。

addMatchingMappings() 方法中的实现为:

由于该机制的存在,如果定义的 @RequestMapping 与访问的路径不相等,则框架会遍历所有的 @RequestMapping 方法。如果数量很多时,则该部分会消耗较多时间。因此如该部分有性能问题,应尽量使 @RequestMapping 路径与访问路径匹配以减少遍历开销。


优化建议

  1. SpringMVC-4.0.3 以后的版本可通过 WebMvcConfigurerAdapter.configurePathMatch(PathMatchConfigurer configurer) 方法自定义实现 UrlPathHelper, AntPathMatcher 优化该问题;

  2. 修改 RequestMappingHandlerMapping 类中 UrlPathHelper, AntPathMatcher 的实现 ( 代码参考 );

  3. 定义 @RequestMapping 路径与访问路径一致,如访问路径为 ‘index.do’,则相应的 @RequestMapping(value = "/index.do")

  4. 减少单个项目中的 @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() 方法中,主要操作为:

  1. 根据 request 解析参数,并调用具体的方法,获取返回值(即调用 invokeForRequest() 方法);

  2. 调用返回值处理器处理返回值。

4.1 调用具体的方法

该部分操作步骤为:

  1. 解析参数;

  2. 根据参数调用具体的方法,并获取返回值;

  3. 返回返回值。

解析参数时,会先获取方法定义的所有参数列表,然后根据每个具体的 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() 方法,实现代码如下:

这部分处理主要分为如下几部分:

  1. 处理异常;

  2. 渲染 ModelAndView 对象;

  3. 调用处理执行链的 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', // 解决 415 错误    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 支持的媒体列表有:

取得媒体列表后,会选取其中的一个:







请到「今天看啥」查看全文