首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

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)’ 错误。如:前端代码片段:

functionfSave(url){varobj = {}; 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)@ResponseBodypublicCategorysaveByModel2(@RequestBody Category category){ categoryService.save(category);returncategory;}

2. 场景2

SpringMVC 接口定义返回 Json 格式数据时,一般有字符串和对象两种方式。而相同条件下,返回 Json 格式字符串时,中文会出现乱码。如:

前端代码片段:

functionfSave(url){varobj = {}; 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)@ResponseBodypublicStringsaveByMap(@RequestBody Map valMap){ categoryService.save(valMap);returnvalMap.toString();}@RequestMapping(value ="/save-by-map-2", method = RequestMethod.POST)@ResponseBodypublicMap saveByMap2(@RequestBodyMap valMap) { categoryService.save(valMap);returnvalMap;}

二、HTTP 请求与响应处理源码解析

SpringMVC 中 HTTP 请求处理流程时序图如下:

从上图可以看出,所有的 HTTP 请求都会进入到 类的 方法。该方法中的主要工作为:

获取处理执行链对象:

获取处理适配器:

调用拦截器的方法;

调用具体的接口方法,并返回模型视图对象:

调用拦截器的方法;

处理结果:。

注:本次源码解析为通过 方式启动,与配置文件方式启动时的源码略有不同(该方式在 Spring 3.2 以后已被弃用),如 的实现 。

接下来深入 SpringMVC 处理 HTTP 请求的过程。

1.获取处理执行链对象

处理执行链类 : 由处理对象 和 拦截器列表 组成,通过 方法返回。

类中获取处理链对象的 方法实现为:

从上图可以看到,框架遍历所有的 对象,调用对应的 方法,如果获取的处理执行链对象不为 则返回该处理执行链对象。

: 定义请求和处理对象之间的映射关系接口。 列表由 SpringMVC 初始化时寻找所有的 类组成。

SpringMVC 中,默认的 实现有:

上图 列表中, 类为 注解的类中的注解方法创建 对象。而前面示例中定义的接口方法为 类型,因此此处取得的 类为该类。简化的类图如下:

封装了 注解方法相关的状态如下:

而在 类中获取处理链对象则由父类 实现:

在 中,主要操作为2步:

获取处理器对象;

获取处理执行链。

1.1 获取处理器对象

由子类 实现:

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

调用 类从 中获取请求路径 ;

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

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

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

方法中的实现为:

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

优化建议

SpringMVC-4.0.3 以后的版本可通过方法自定义实现优化该问题;

修改类中的实现 (代码参考);

定义路径与访问路径一致,如访问路径为 ‘index.do’,则相应的;

减少单个项目中的的数量。

1.2 获取处理执行链

该方法根据传入的处理器对象建立处理执行链,将传入的处理对象和匹配的拦截器添加到处理执行链对象中,并返回:

2.获取处理适配器

:MVC 框架的 SPI 接口,允许核心MVC工作流的参数化(这句话不是很懂…有会的读者欢迎指教)。

每一种处理请求的处理器类型必须实现该接口, 类可根据该接口无限扩展,且根据该接口访问所有已安装的处理器对象

而处理器类型设置为 ,其他的框架不用修改源码就能和 SpringMVC 整合。完整的接口介绍如下:

传入处理器对象,SpringMVC 遍历所有的 类,如果某个处理适配器支持该处理器类型,则返回该处理器:

接口判断某个具体的处理适配器是否支持传入的处理器对象,定义如下:

在 类中,支持的处理器类型为 :

因此此处返回的 实现类为: 。

3.调用拦截器的 方法

调用处理执行链对象的 方法,代码如下:

可以看出框架会遍历在第1步:获取处理链对象中获取的拦截器对象,依次调用 方法,如果某次调用返回 ,则会调用 方法,并返回 。 实现如下:

即依次调用拦截器对象的 方法。

4.调用具体的接口方法

调用第2步:获取的处理适配器对象的 方法处理请求。而 类中处理请求的是 方法。方法执行时序图如下:

从上图中可以看出,框架先创建 对象,然后调用该对象的 方法,最后获取 对象。具体实现如下:

而 方法中,主要操作为:

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

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

4.1 调用具体的方法

该部分操作步骤为:

解析参数;

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

返回返回值。

解析参数时,会先获取方法定义的所有参数列表,然后根据每个具体的 类型,调用具体的参数解析器类 的解析参数方法 。

源码如下:

此处的 对象为 类,该类封装了一系列的参数解析器,属性如下:

解析时,根据 判断该参数解析器是否支持该参数。

前面参数由于使用的是 注解,因此调用 类,典型的还有 等:

而 类处理参数时,使用了 消息转换机制。

4.1.1 消息转换机制

该机制的流程为:

由于请求的 为 类型,所以此处调用的消息转换类为 ,默认的字符集为 :

而 的实现为调用 的类 方法:

注:在调用 方法之前,如果无匹配的 类,则会抛出 异常;

在调用 方法之前,如果无匹配的 类,则会抛出 异常。

解析完参数后,则通过反射机制调用具体的方法并获取返回值:

4.2 处理返回值

该部分的逻辑和前面解析参数逻辑类似。主要为调用具体的 (返回值处理器) 处理返回值。

框架中默认的返回值处理器有:

此处由于使用 注解,因此调用 处理器。该处理器中,使用 消息转换机制(见 4.1.1),调用 方法处理返回值:

而在调用 方法之前,会调用 方法获取可生产的媒体类型,如果用户自定义 属性,则此处会返回该值;

如果用户未定义,则根据返回值 类型,遍历系统中的消息转换器列表,获取支持的媒体类型列表:

5.调用拦截器的 方法

调用拦截器对象的 方法,代码如下:

从上图可以看出框架会遍历拦截器对象列表,以处理链对象中拦截器对象列表相反的顺序调用拦截器对象的 方法。

6.处理结果

调用 方法,实现代码如下:

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

处理异常;

渲染对象;

调用处理执行链的方法。

6.1 处理异常

如果在前面的处理中抛出了异常,则会获取相应的模型视图对象。

有两种处理方式:如果异常对象为 类型,则直接获取模型视图对象;否则的话调用当前系统内的处理异常解析器 ( ) 处理:

如果某个异常解析器返回了有效的模型视图对象,则跳出循环。

此处的 类通过用户自定义的 方法解析异常,如果用户未定义,则跳出该解析器。此处示例代码为:

在该类的 方法中,会创建一个 对象,然后调用该对象的 方法:

与前面处理正常 流程类似,就不深入探讨了。

6.2 渲染 对象

在渲染方法 中,如果传入的 mv 对象是 引用类型,即为 字符串时,则调用当前的视图解析器 解析该字符串,如当前配置了视图解析器为:

则该实现会在视图解析器列表 中:

在解析时,将会添加上对应的 , 处理后的 对象为:

接下来则调用 对象的 方法,根据提供的 对象渲染该视图对象。

6.3 调用处理执行链的 方法

该方法只调用在 方法中成功调用且返回为 的拦截器,且从列表后往前调用:

三、解决方案

根据前面的原理介绍可知,文章开头的两种场景错误都是由于在第4步:调用具体的接口方法出错:场景1为解析参数时出错,场景2为返回值处理时出错。

1. 场景1出错原因

见 4.1.1 消息转换机制。场景1中,ajax 请求如果未设置 则会使用默认的类型: 。

而由于接收参数定义为 ,对应的参数解析器为 类,调用 消息转换机制。

而 SpringMVC 中并无支持该类型的 类,因此抛出异常。

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中,定义了 注解,调用的返回值处理器为 ,调用 消息转换机制。

返回的 类型是 ,而在 SpringMVC 的消息转换器列表中支持该返回值类型的消息转换器有 支持的媒体列表有:

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

而 返回值类型取得的媒体类型为 , 将该媒体类型设置为 的 , 因此返回中文乱码:

而 类支持的媒体类型只有两种,因此返回值正常:

4. 场景2解决方案

在接口上方定义 ,如:

@RequestMapping(value ="/save-by-map", method = RequestMethod.POST, java produces ="text/plain;charset=utf-8")@ResponseBodypublicStringsaveByMap(@RequestBody Map valMap){ categoryService.save(valMap);returnvalMap.toString();}

或者在 springmvc 的配置文件中配置消息转换器的媒体类型,如:

四、设计模式

本章内容不包括对具体的设计模式原理介绍,只介绍前面框架源码中出现的设计模式的应用场景,如读者对相关的设计模式原理有兴趣,可查看每小节的链接或者自行学习。

前面研究的原理中,涉及的设计模式如下:

1.模板方法模式

该模式在框架中使用的地方很多,主要作用为: 定义算法的骨架。

如获取处理执行链方法中, 方法定义了算法的骨架如下:

而 则由具体的子类来实现:

2.适配器模式

该模式本质为: (接口不兼容时,通过)转换匹配,(从而)复用功能。

如 的实现,可适配所有的处理器类型。是常见的适配器模式实现方式:

可以看出 方法传入 对象,在实现类中操作传入的 对象。

有新的处理器类型时,实现该接口即可。如 方式使用的 ,以及已经弃用的 。

还有种缺省适配方式,由子类选择覆盖需要的方法。如 类:

3.责任链模式

该模式本质为:分离对象的职责,动态的组合在一起。

常见的有两种实现方式:

责任链:请求在链上传递,有一个对象处理过则跳出;

功能链:请求在链上传递,链上的每个责任对象负责处理某一方面的功能,处理过不跳出,向下传递。

如 的实现,为功能链的实现:

定义的拦截器列表 ,循环调用 方法 ,处理用户请求之前,均会经过该拦截器列表中所有的对象处理。

4.策略模式

该模式本质为:分离算法,选择实现。即对应同一操作,提供不同的策略实现。

如参数解析器 :

返回值处理器 :

5.工厂方法模式

该模式形式为:提供一个创建对象实例的接口,让子类决定实例化哪一个类。

如在 方法中,创建 时,会传入 对象。

而该类即为典型的工厂方法模式的实现:

在其默认的实现里,通过 方法创建 对象,该实现可由子类自行扩展:

6.组合模式

该模式定义为:将对象组合成树型结构,以统一叶子对象和组合对象。

一般有以下两种场景:

统一整体和部分的操作;

统一地使用组合结构中的所有对象。

SpringMVC 中参数解析时调用的 类, 组合一系列的参数解析器,即为第 2 种用法:

还有 等。

7.单例模式

模式定义为:保证一个类仅有一个实例,并提供一个全局访问点。该模式在日常的开发中应用也非常广泛。

两种典型的实现方式:

懒汉式:一般为延迟加载方式,在使用的对象实例不存在时,再创建对象实例;

饿汉式:一般在初始化时,创建对象实例。

SpringMVC 中,获取 类时,即通过缓存的方式实现的懒汉式单例模式。将创建的 对象缓存在 中:

注: 类为管理异步请求处理的主要类。

SpringMVC 框架中还有很多使用的设计模式未被列出,以上列举的只是前面深入源码过程中比较典型的几个。

五、写在最后

以上就是我所理解的 SpringMVC 处理 HTTP 请求的过程。而前面所列出的也只是整个 SpringMVC 框架的冰山一角,还有很多等待着我们去探索。

在深入学习的过程中,感触最深的也是自己的无知,此处引用一句名言:越学习,越发现自己的无知。

本文篇幅较长,文中涉及的流程图画法也比较粗糙,希望这种技术交流的方式能给你一些启发。

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180123G0312V00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券