博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从一个请求入口来带你探究DispatcherServlet的奥秘——SpringMVC的核心组件——万字长文
阅读量:3960 次
发布时间:2019-05-24

本文共 26649 字,大约阅读时间需要 88 分钟。

文章目录

1:十万个为什么?

这一周画了大部分的精力来探究SpringMVC源码,为什么忽然会有这想法呢?因为有一种感觉就是,当你项目上手多了以后,你就会发现一个问题:会非常好奇,为什么你前端发送一个请求(这个请求还可以携带上表单提交数据或者是你的url地址后面加上?的参数,比如http://localhost:8080/some.do?username=zlj)然后这个请求就可以根据路径到达你后端里面指定的Controller类(必须要标记@Controller注解等,或者实现Controller接口等)下的指定方法(这个方法上要有 @RequestMapping注解,比如 @RequestMapping(value = “/some.do”)

),然后这个方法执行完以后,就可以跳转到你的指定界面,这个过程就可以进行把Model填充到View中,进行渲染视图。如下面代码:

@RequestMapping(value = "/some.do")    public ModelAndView dosome(){
//doGet //渲染视图 ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("msg","欢迎使用springmvc做web开发"); return modelAndView; }

上面步骤的顺利执行,一切的一切都要归功于SpringMVC的核心组件——DispatcherServlet。在讲该核心组件之前,我们就要先讲一下Debugger,

俗话说:工欲善其事,必先利其器。要想对源码进行详细的研究,离不开对源码调试,而要想对源码进行调试,一个好的IDE工具是非常必要的。以下演示,我们都使用IntelliJ IDEA为例。
在这里插入图片描述

学完下面的几个章节,我相信大家会对上面代码的执行流程,有一个深刻的了解

2:Debugger的使用

如果我们在启动入口设置断点,并结合单步调试,即可以完整地观察到SpringBoot应用的详细启动过程。通过在处理器方法中设置断点,则可以通过调用栈看到从Web容器的请求处理方法开始,到Spring MVC框架层的处理过程,最终到达处理器方法的执行的整个过程。

因为启动入口是整个应用的根,从根可以得到的调试信息是优先的。而在处理器方法中,因为是请求处理入口执行的最后一环, 通过处理器方法的调试信息,可以得到完整的请求处理的调用链。故在此以处理器方法为例,来演示整个调试过程,以及调试过程中可以得到的信息,从而达到通过调试来探索研究的目的。如果对上面这一段话不理解,我们马上看一张图,来解释这一段话的意思,如下是我已经在调试SpringMVC源码时候的一张图片。

在这里插入图片描述
从上面的栈帧窗口中,我们得到一个暴论如下四点:

1.栈顶是Java线程的run方法,这表示在Web容器接收到请求后,会通过一个独立的线构对该请求进行处理。不同的请求使用不同的线程处理。

2.再向上则是org apache tomcat相关的类,因为Spring Boot默认使用的Web容器是tomcat,所以这里的调用栈就是Web容器内部对请求的处理调用栈。

3.随后进入org. springframework.web. servlet相关的类。这一层 就是Spring MVC框架有Web容器整合的部分,该部分的整合依赖于名为FrameworkServlet这个Servlet组件(其实是其子类DispatcherServlet)。其后续执行都与org.springframework.web.servlet相关包有关。

4.Spring MVC框架层执行的终点也就是处理器方法,整个调用栈就是这样产生的

我们在后面的章节中主要是对DispatcherServlet这一个部分进行源码研究,1,2两点(SpringBoot下SpringMVC框架启动原理)和有关SpringBoot在启动时怎么把DispatcherServlet注册到基于Servlet标准的Web容器中(其实也可以放在SpringBoot下SpringMVC框架启动原理中)。在这里不进行源码研究。

一不小心,扯远了,马上回到正题,开始讲解Debugger的使用。

在这里插入图片描述

我们分两小部分,调试图解和调试的使用来讲解这一部分,这一部分单独写一篇文章,如下

唠嗑:在针对特定组件进行探索时,也需要依赖于IDE提供的强大功能,包括特定类与接口的查找、方法引用的查找、实现类的查找等查找操作。这里所讲到的操作,会在下面章节,用到的时候,来进行讲述。
在这里插入图片描述

讲了这么多,怎么还没进入今天的正题啊,DispatcherServlet快出来,别急,别急(我自己都急了)在讲DispatcherServlet之前,我们有必要讲一下Spring MVC的请求处理路口,为什么还要讲它啊,因为饭要一口口吃才不会噎死,如果我直接上DispatcherServlet类中的doDispatch方法,是不是会一脸懵逼,它从哪里来都不知道啊!!!!

在这里插入图片描述

3:Spring MVC的请求处理路口——processRequest

在这里,我先下一个暴论:

Spring MVC与Web容器之间的整合是通过DispatcherServlet组件实现的。Spring Boot在启动时把DispatcherServlet注册到基于Servlet 标准的Web 容器中,Web容器在接收到HTTP请求时,经过预处理后把该请求交给DispatcherServlet处理,同时DispatcherServlet还负责处理把需要返回的内容写入HTTP响应。这就意味着Spring MVC请求处理的核心就是DispatcherServlet

这一段话的前面几句可能不好理解,我们必须得自己调试一遍SpringBoot下SpringMVC框架的启动原理,才可以很好的理解这段话(比如Spring Boot在启动时是怎么把DispatcherServlet注册到基于Servlet 标准的Web 容器中的?Web容器在接收到HTTP请求时,怎么经过预处理把该请求交给DispatcherServlet处理?)

但是我们一定记住这句话:Web容器在接收到HTTP请求时,经过预处理后把该请求交给DispatcherServlet处理。它对我们后面,怎么找到MVC的请求处理路口有很大的帮助!!!!(自认为)
在这里插入图片描述

在前面提到研究请求处理的步骤可以通过在处理器方法上设置一个断点, 再访问此请求进入断点中断,此时查看该线程的调用栈即可找到相关的处理逻辑。那么这里继续以此思路来查看Web容器中对于请求的处理步骤。

1.在处理器方法的调用栈中,从最顶层开始调用栈的上层查看,排除掉JDK自身包java.*下的方法及tomcat服务器包org apache.*下的方法,最先出现的Spring包org. springframework下相关的方法是Filter组件。

2.在Filter 组件执行完成后,则进入org.springframework 中的Servlet组件执行请求与响应的处理。这个Servlet组件即是Spring MVC与Web容器整合的核心。在Servlet组件中,依次调用org. springframework包中的所有MVC组件,最终执行到开发者定义的处理器方法。这也就是断点所在处,之后即层层向上返回,同时把返回内容写入HTTP响应中。

上面的两断话放在栈帧窗口中,一切就变得好理解起来,下图是按照在处理器方法上设置一个断点,然后浏览器执行请求,就可以进入断点中断中查看。栈帧窗口如下

在这里插入图片描述
在上面的处理步骤中,我们提到了Filter,那我们在简单讲一下Filter到Servlet的过渡
唠嗑:在Servlet标准中,请求的处理过程是先通过由所有的Fiter组件构成Fiter链对请求进行过滤与预处理,如果在Filter 链中没有对请求提前结束处理,则最终会进入Servlet组件中对请求进行处理。

对于Spring MVC来说,最终的Servlet组件就是DispatcherServlet,而其中调用链中出现的Spring MVC提供的一些Filter则各有其功能,在这不在概述

在过滤器执行完成后,将会进入Servlet组件的执行中。通过栈帧窗口可知,在栈帧窗口中最先出现的Servlet组件相关类为javax.servlet.http.HttpServlet,该类为抽象类,执行时肯定是一个具体的类,为该类的子类(由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用)。栈帧窗口如下图

在这里插入图片描述
看了怎么久,怎么还没出现,MVC的请求处理路口啊啊啊,更别说DispatcherServlet的奥秘了,啥都没懂啊!!!!或许这就是源码吧!!!我都快急死了,好,接下来,我就在下一个暴论,让大家看看DispatcherServlet到底在哪。
在这里插入图片描述

在调用栈中,定位到开始调用Servlet 相关方法处,此处为:internalDoFilter:231,ApplicationFilterChain。在该处执行的调用为servlet.service(request, response);, 在此处查看servlet的值,可以看到其具体类型为DispatcherServlet。直接上图,解释这段话的意思

在这里插入图片描述
来了,他来了,我们终于看见DispatcherServlet了!!!!感觉要胜利了!!!接下来我们要讲一下其中的调用过程,为什么忽然在ApplicationFilterChain类中会出现DispatcherServlet呢?我们在这里画一张DispatcherServlet类结构图图片,帮助大家理解。
在这里插入图片描述
对上面的五点做如下解释
第一点和第二点:进入父类的service执行逻辑,如果使用debug下的get请求进行测试,所以执行else分支。在父类的sevice执行逻辑中,对请求方法进行判断,以执行不同的处理。部分HttpServlet类下的service方法代码如下

protected void service(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException{
String method = req.getMethod();//对请求方法进行判断,支持GET,HEAD,POST,PUT,DELETE,OPTIONS,TRACE。7种请求判断//对每种请求方法分别执行对应的do方法 if (method.equals(METHOD_GET)) {
//... }else if (method.equals(METHOD_HEAD)) {
//... doHead(req, resp); } else if (method.equals(METHOD_POST)) {
doPost(req, resp); } else if (method.equals(METHOD_PUT)) {
doPut(req, resp); } else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp); } else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp); } else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp); } else {
//进入此逻辑表示请求方法不被支持,会直接返回错误响应 //..

第三点:HttpServlet的do系列方法中,均返回固定的错误请求,错误状态码为405,请求方法不被支持。所以若要对对应请求方法提供支持,子类必须重写父类对应请求方法的do系列方法。这个时候FrameworkServlet类就出现了,它重写了HttpServlet中所有的do*系列方法。

唠嗑:为什么HttpServlet的do系列方法中,均返回固定的错误请求?

我们在HttpServlet类下的service方法中随便找一个do方法,比如doPost方法,按住ctrl,点击方法,进入doPost方法源码,如下看到if和else分支里面都是sendError方法,就明白啦

protected void doPost(HttpServletRequest req, HttpServletResponse resp)        throws ServletException, IOException    {
String protocol = req.getProtocol(); String msg = lStrings.getString("http.method_post_not_supported"); if (protocol.endsWith("1.1")) {
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg); } else {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg); } }

第四点:因为在Spring MVC 的设计中,可以根据@ RequestMapping注解中标记的方法条件,把请求根据请关方法分发到不同的处理器方法上,故最终所有的方法需要调用同一个分发逻辑,所以在FrameworkServlet的do*方法中,可以看到都调用了同一个方法: processRequest。下面是FrameworkServlet类中doGet方法的内容示例。

@Override	protected final void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
processRequest(request, response); }

在上述过程执行完成后,Spring MVC提供的Servlet组件与Web容器整合,在后续的过程中,将调用与原始Servlet无关的processRequest 方法,以此为入口,进入到Spring MVC框架对请求的处理中。上述过程是从原始Web容器调用到MVC框架组件的过程(是不是跟这句话很像啊:Web容器在接收到HTTP请求时,经过预处理后把该请求交给DispatcherServlet处理)。

第五点:在请求处理方法中,所有处理的调用最终会委派给doService方法。在FrameworkServlet类中该方法(doService)是抽象方法,运行期真实调用的是DispatcherServlet重写的doService 方法。

唠嗑:在这里我们讲一下FrameworkServlet类中processRequest 方法的作用:该方法内先进行上下文的初始化操作,最后会进行上下文请求重置的操作,真实的请求与响应操作则是通过doService方法完成的。在构造本地化上下文时,调用方法buildLocaleContext。在这个方法逻辑中,会使用到本地化解析器解析请求中的地区信息,关于本地化解析以后会单独讲

讲了这么多,通过查看栈帧窗口,我们终于分析出来MVC的请求入口是FrameworkServlet类中processRequest 方法,在下面一章,我们主要就是讲DispatcherServlet 类doService方法中的核心方法doDispatch方法,对doService方法我们只做一个简单概括

在这里插入图片描述

4:DispatcherServlet类中的doDispatch方法探究——请求分发处理

对DispatcherServlet 类中的doServie方法简单解读,我们查看它的源码,主要就是三个作用:1.保存当前请求属性快照目的 2.把组件放入请求属性中的 3.执行核心doDispatch分发请求方法,如果大家对1,2感兴趣,可以自己探究,这里不在进行分析

接下来,我们直接进入正题,来对doDispatch方法一探究竟
在这里插入图片描述

对请求的所有处理都封装在其doService 方法中调用的doDispatch方法中。一旦进入doDispatch方法,就可以视为与原始Servlet的对接已经完成,后续将会进入Spring MVC相关核心功能。在doDispatch方法中,会执行所有的请求处理操作,包括请求分发、响应处理等核心操作。这也是整个Spring MVC中最复杂的部分。

在这个复杂的分发逻辑中,Spring MVC的高度封装与分层设计的代码依然表现得非常优美。整体处理逻辑可以分为多个步骤,每个步骤都有其独立的作用与封装,相关源码看起来使人舒心:对于源到的研究者来说,其中的设计理念值得学习

4.1 doDispatch方法—— 请求分发概述

请求处理方法虽然复杂,但其方法长度并不长,因为每个步骤的处理都单独封装,下面来详细地看一下 该方法的源码,下面来详细看一下DispatcherServlet类中的doDispatch方法源码并且附加上了重要代码地方的注释:

/***执行请求分发到处理器*处理器通过当前应用中初始化的HandlerMapping处理器映射列表按顺序获取处理器*通过当前应用中初始化的HandlerAdapter处理适配器列表,获取支持当前请求处理器的处理适配器该方法处理可处理的所有请求方法。对于一些特殊的请求方法如Options等,响应需要做额外的一些适配操作,该适配操作交给请求处理器与处理适配器的逻辑去处理*/protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
//定义一个已处理请求,指向参数中的request,已处理请求后续可能改变 HttpServletRequest processedRequest = request;//定义处理器执行链,内部封装拦截器列表与处理器 HandlerExecutionChain mappedHandler = null;//是否是多块请求,默认为否 boolean multipartRequestParsed = false;//获取与当前请求关联的异步管理器,用于执行异步操作 WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);//整体放在try中,用于捕获处理过程中的所有异常 try {
//用于保存处理适配器执行处理器后的返回结果 ModelAndView mv = null;//用于保存处理过程中发生的异常 Exception dispatchException = null;//嵌套一个try,内部获取真实的处理异常,在异常处理中还有可能发生异常,上层的try作用为拦截这层try异常处理中发生的异常 try {
//1.检查多块请求,如果是多块请求,则返回一个新的请求,processedRequest 保存这个新的请求引用:否则返回原始请求 processedRequest = checkMultipart(request);//判断两者是否是同一个引用,如果是则说明为多块请求,且已经处理,此变量为true multipartRequestParsed = (processedRequest != request);//2.获取可处理当前请求的请求处理器,通过HandlerMapping查找,请求处理器中封装了拦截器链和对应的处理器,可以是具体的处理器方法 mappedHandler = getHandler(processedRequest);//如果没有,则执行没有处理器逻辑 报404 if (mappedHandler == null) {
noHandlerFound(processedRequest, response); return; }//3.根据当前请求的处理器获取支持该处理器的处理适配器 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());// 4. 单独处理last-modified请求头,用于判断请求内容是否修改,如果未修改直接返回,浏览器使用本地缓存 String method = request.getMethod(); boolean isGet = "GET".equals(method); if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return; } }//5.通过mappedHandler这个HandlerExecutionChain执行链的封装,链式执行其中所有拦截器的前置拦截方法preHandle if (!mappedHandler.applyPreHandle(processedRequest, response)) {
//任意一个拦截器的前置拦截方法返回了false, 即提前结束请求的处理 return; }//6.最终执行处理适配器的处理方法,传入请求,响应与其对应的处理器,对请求进行处理。在这个处理中,最终调用到了请求对应的处理器方法//执行的返回值是ModelAndView类型,封装了模型数据与视图,后续对此结果进行处理.并根据其中的视图与模型返回响应内容 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());//如果异步处理开始,则直接返回,后续处理均通过异步执行 if (asyncManager.isConcurrentHandlingStarted()) {
return; }//7.应用默认视图名,如果返回值的ModelAndView中不包含视图名,则根据请求设置默认视图名,具体逻辑后面说明 applyDefaultViewName(processedRequest, mv);//8.请求处理正常完成,链式执行所有拦截器的postHandle方法。链式顺序与preHandle相反 mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) {
dispatchException = ex; } catch (Throwable err) {
//可以通过@ExceptionHandler处理这种类型的异常//封装为嵌套异常以供异常处理逻辑使用 dispatchException = new NestedServletException("Handler dispatch failed", err); }// 9.对上面逻辑的执行结果进行处理,包括处理适配器的执行结果处理以及发生的异常处理等 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) {
//10. 拦截后链式执行拦截器链的afterCompletion方法. 在方法内部判断mappedHandler是否为空,如果不为空,则执行器triggerAfterCompletion方法 triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) {
//11.拦截Error类型异常,拦截后链式执行拦截器链的afterCompletion方法 triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err)); }//12.finally 块,做资源清理 finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else {
// Clean up any resources used by a multipart request. if (multipartRequestParsed) {
cleanupMultipart(processedRequest); } } } }

在整个方法中,每段逻辑都有其自身的目的,且整体从前到后都是按照请求处理的流程在执行。同时,每一段逻辑的处理都会涉及些 Spring MVC的组件(有关组件的使用,以后会单独写一篇文章来讲述),在上述代码中共分为11个逻辑,分别如下。

(1)预处理多块请求。

(2)获取请求处理器。

(3)查找处理适配器。

(4)处理HTTP缓存。

(5)执行前置拦截器链。

(6)处理适配器执行。

(7)返回值视图名处理。

(8)执行后置拦截器链。

(9)处理返回值与响应。

(10)执行完成拦截器链。

(11)清理资源。

在这里,我们只讲述(2),(3),(6),(9)这4点。4点分别对应下面的4个小节

我们先下一个结论图,如下:这一张图片会在DispatcherServlet类中的doDispatch方法中展现的淋漓尽致。等我们讲完了上面4点,回过头来,再总结这张图片。

在这里插入图片描述

客户端发送请求,通过DispatcherServlet类(请求分发中心)下的doDispatch方法接收(对请求的所有处理都封装在其中),然后请求分发中心通过处理器映射器(Mapping)查找处理器执行链

4.2 获取请求处理器

对于请求的处理,最终都是要通过处理器(可以是带@ RequestMapping注解注册的处理器)来执行,SpringMVC把请求处理器的查找与请求处理器的执行分离,执行请求处理器的查找,也是SpringMVC核心操作。该部分代码如下:

/**返回该请求对应的处理器执行链按顺序查找全部处理器映射@param request当前请求@return:未找到可处理该请求的处理器执行链时,返回空,找到时返回该处理器执行链*/	@Nullable	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
//如果处理器映射列表不为空 if (this.handlerMappings != null) {
//遍历全部处理器映射 for (HandlerMapping mapping : this.handlerMappings) {
//尝试执行当前处理器映射的获取处理器方法,HandlerExecutionChain handler = hm HandlerExecutionChain handler = mapping.getHandler(request); //不为空直接返回,即便有多个处理器执行链匹配,也只返回第一个(处理器映射排在前面的优先返回) if (handler != null) {
return handler; } } } return null; }

从以上代码中可以看到虽然表面上逻辑很简单,仅是遍历所有的处理器映射,以此尝试获取与当前请求匹配的处理器执行链,但其实核心逻辑都在处理器映射(Mapping)中。(关于处理器映射的内部细节在以后会讲到)

唠嗑:@RequestMapping注解注册的处理器方法,其相关的处理器映射类为RequestMappingHandlerMapping该映射器内部根据当前请求与所有的注解信息进行匹配,找到最佳匹配并封装为处理器执行链并返回。我们可以对如下的代码打断点,调试得出其相关的处理器映射类为RequestMappingHandlerMapping这个结论。

@Controllerpublic class MyController {
@RequestMapping(value = "/some.do") public ModelAndView dosome(){
//doGet ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("msg","欢迎使用springmvc做web开发"); modelAndView.setViewName("show"); return modelAndView; }

在这里插入图片描述

如下,是进入到getHandler方法进行调试的过程,因为是for循环,所以我们重复执行了两遍,第一遍尝试执行当前处理器映射并获取处理器方法,该映射为BeanNameUrlHandlerMapping类,根据下图可以看出该映射获取不到handler,然后接着for循环
在这里插入图片描述
第二遍for循环继续尝试执行当前处理器映射并获取处理器方法,该映射为@RequestMappingHandleMapping,根据下图可以看出该映射成功获取到了handler(mappedHandler: “HandlerExecutionChain with [com.zlj.MyController#dosome()and 0 interceptors),跳出for循环。
在这里插入图片描述
返回到doDispatcher方法中
在这里插入图片描述

这也正是Spring MVC高度封装的表现,把每一个复杂的逻辑都封装为 一个接口, 不仅可以通过不同的接口实现来完成不同的映射查找功能,同时无论映射查找方法有多复杂,这里看到的代码结构仍然非常清晰。在这里把抽象与封装的概念体现得淋漓尽致。

同时注意,查找到的是处理器执行链(mappedHandler: “HandlerExecutionChain with [com.zlj.MyController#dosome()and 0 interceptors),其中封装了最终执行的处理器(dosome),以及在执行处理器前后要执行的拦截器链,即把与该请求匹配的所有拦截器链式封装到处理器执行链中,以供后续执行使用。 我们可以再看一下HandlerExecutionChain 执行链类的源码,有助于理解上面的一段话

HandlerExecutionChain类的部分源码

/**Handler执行链,包括处理程序对象和任何处理程序拦截器。 通过HandlerMapping的{@link HandlerMapping#getHandler}方法返回。*/public class HandlerExecutionChain {
private static final Log logger = LogFactory.getLog(HandlerExecutionChain.class); private final Object handler; @Nullable private HandlerInterceptor[] interceptors; @Nullable private List
interceptorList; //...

4.3 查找处理适配器

SpringMVC对于请求处理器的查找与执行是分离的。而根据请求处理器的类型不同,又需要使用不同的适配器去执行该处理器,这正是处理适配器HandlerAdapter的作用。要想使用对应的处理适配器执行处理器,则需要先获取可以执行当前处理器的适配器。换句话说, HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());的作用:在SpringMVC中控制器的实现方式有很多种,如下,有单纯实现某个Controller接口的;也有使用注解的等,也就是想知道你的处理器的实现方式,根据处理器不同的实现方式,返回不同的适配器。

//实现接口Controller方式public class ControllerRealize2 implements org.springframework.web.servlet.mvc.Controller{
@Override public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("msg","欢迎使用springmvc做web开发"); modelAndView.setViewName("show"); return modelAndView; }}//实现注解的方式@Controllerpublic class MyController {
@RequestMapping(value = "/some.do") public ModelAndView dosome(){
//doGet ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("msg","欢迎使用springmvc做web开发"); modelAndView.setViewName("show"); return modelAndView; }}

查找逻辑代码如下:

/**返回支持传入的处理器对象的处理适配器@param handler前面逻辑查找到的处理器*/protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
//遍历处理适配器列表,调用supports方法,找到支持该处理器的适配器 for (HandlerAdapter adapter : this.handlerAdapters) {
//按顺序查找,第一个支持的适配器被返回 if (adapter.supports(handler)) {
return adapter; } } } //未找到的处理适配器直接抛出异常 throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler"); }

这里的查找逻辑与处理器映射的查找逻辑基本相同。

处理适配器的作用不但是执行处理器(adapter.supports(handler)),而且还有更大的作用即适配( mv = ha.handle(processedRequest, response, mappedHandler.getHandler());)。用于把请求参数适配为处理器需要的参数,并执行处理器,之后再把处理器的返回值适配为ModelAndVview统一的模型视图类型, 用于后续操作中对相应的统一处理。

唠嗑:对于@RequestMapping注解注册的处理器,类型为HanderMethod,通过RequestMappingHandlerMapping返回。对于该类型的处理器,其适配器为RequestMappingHandlerAdapter其内部处理逻辑极其复杂,包括参数绑定和返回值处理,在下一小节进行讲述。

通过打断点进行调试,可以证明为什么其适配器为RequestMappingHandlerAdapter
如下是进入getHandlerAdapter方法一步步调试的过程,这里不在详细概述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

4.4 处理适配器执行

在完成上面的所有处理后,会进入处理适配器的执行逻辑,这里只是简单地调用处理适配器的handle方法,该方法返回一个ModelAndView类型的值。

这个方法是整个请求处理过程中最核心的方法,也是在调用了这个方法后,最终才调用到了处理器方法。所有对请求处理逻辑的封装都在这个方法内部。

在上面一小节也讲过对于@RequestMapping对应的处理器方法,其处理适配器为RequestMappingHandlerAdapter,在该适配器处理过程中,包含了处理器方法参数与请求参数绑定的功能及处理器方法返回值的自动处理功能,最终适配为ModelAndView,类型的返回值到调用处。关于其内部执行逻辑,总体也是比较复杂的,在以后会单独写一篇文章介绍

4.5 处理返回值与响应

在这里插入图片描述

在拿到处理适配器的处理结果ModelAndView后,如上图,接下来就可以根据这个返回值向响应中添加返回数据了。

通过方法processDispatchResult处理前面过程中产生的分发结果。在无异常时,主要处理对象是处理适配器返回的ModelAndView。发生异常时,处理对象为产生的异常,异常处理后结果为ModelAndView,使用此ModelAndView继续执行后续处理。DispatcherServlet类下的processDispatchResult方法源码如下:

/***用于处理适配器调用处理器后适配过的Mode1AndView结果, 或者发生异常时把异常处理为Mode lAndView结果,继续执行后续处理*/private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,			@Nullable Exception exception) throws Exception {
boolean errorView = false;// 如果出现了异常 if (exception != null) {
//如果异常类型为ModelAndViewDefiningException if (exception instanceof ModelAndViewDefiningException) {
//该异常内部包含一个ModelAndView类型的属性,用于提供包含ModelAndView结果的异常封装 logger.debug("ModelAndViewDefiningException encountered", exception);//直接使用异常中封装的ModelAndView作为最终的ModelAndView结果 mv = ((ModelAndViewDefiningException) exception).getModelAndView(); } else {
//其他异常类型,先获取处理器 Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);//执行process处理器异常方法,获取处理了异常结果后得到的ModelAndView结果 mv = processHandlerException(request, response, handler, exception);//如果mv不为空,则说明返回了包含异常的视图,即返回的视图为异常视图 errorView = (mv != null); } }///如果视图与模型不为空,且视图与模型没有标记为被清理 if (mv != null && !mv.wasCleared()) {
//视图与模型不为空时,执行渲染视图的操作 render(mv, request, response);//如果是异常视图,渲染后需要清空请求属性中的异常信 if (errorView) {
WebUtils.clearErrorRequestAttributes(request); } } else {
//如果视图为null,则打印一个日志 if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned."); } }//如果异步处理已经开始,则直接返回结束执行 if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
return; }//处理器执行链不为空时,触发拦截器链的完成后方法,这里的完成后方法执行是在请求处理正常完成时执行的。还有异常时执行的完成后方法 if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, null); } }

在上面的处理过程中,有两个比较重要的方法。第一个是processHandlerException方法;第二个是对返回的ModelAndView结进行渲染的render方法。重点介绍第二个方法render

4.5.1 render核心方法——对结果统一处理的渲染方法

对该结果进行统一处理的逻辑核心为render渲染方法,DispatcherServlet类下的render方法的详细逻辑代码如下:

/**对指定ModelAndView进行渲染这一步是一个请求的处理过程中的最后一步,其中包含了通过视图名获取视图的逻辑 @param mv 需要渲染的Mode lAndView@param request 请求@param response响应@throws ServletException 如果视图不存在或不能被解析抛出@throws Exception 渲染时发生任何异常抛出*/protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
//先通过Locale解析器获取请求对应的LocaleException Locale locale = (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());//设置获取的Locale为相应的Locale response.setLocale(locale);//最终获取的视图 View view;//如果ModelAndView中的视图为视图名,则获取这个视图名 String viewName = mv.getViewName(); if (viewName != null) {
//把视图名解析为视图 14 view = resolveViewName(viewName, mv.getModelInternal(), locale, request); if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + getServletName() + "'"); } } else {
// 如果不是视图名,而直接是一个视图类型,则获取视图 view = mv.getView(); if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " + "View object in servlet with name '" + getServletName() + "'"); } }//代理调用视图类的渲染方法 if (logger.isTraceEnabled()) {
logger.trace("Rendering view [" + view + "] "); } try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value()); }//执行视图的渲染方法,每种模板引擎都有其对应的视图实现,视图渲染对应于模板引擎的渲染模板 32 view.render(mv.getModelInternal(), request, response); } catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "]", ex); } throw ex; }

中第一个关键点是通过视图名解析视图的方法resolveViewName(代码第14行),该方法内容如下

protected View resolveViewName(String viewName, @Nullable Map
model, Locale locale, HttpServletRequest request) throws Exception {
//如果视图解析器列表不为空 if (this.viewResolvers != null) {
//遍历视图解析器列表 for (ViewResolver viewResolver : this.viewResolvers) {
//调用视图解析器的resolveViewName 方法,把视图名解析为视图 View view = viewResolver.resolveViewName(viewName, locale); if (view != null) {
//第一个不为空的视图被返回 return view; } } } return null; }

这里通过新的组件——视图解析器来对视图名进行解析,得到最终要使用的视图。不同模板引擎有不同的视图解析器,

例如我们使用的Thymeleaf模板引擎,对应的视图解析器为ThymeleafViewResolver,解析逻辑则是通过配置中的spring thymeleafprefix+视图名+spring.thymeleaf.suffix的形式,从classpath下查找视图名资源,并解析为具体的ThymeleafView。

在这里我们对我们的代码进行调试如下(给出一个完整步骤):

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
调试完源码,得出我们的视图解析器为InternalResourceViewResolver,解析逻辑这是通过springmvc.xml文件配置的如下

然后解析为具体的View

在这里插入图片描述

获取到具体的视图后,后续的处理核心是视图渲染方法的执行(第32行代码:view.render(mv.getModelInternal(), request, response);)。在视图渲染方法的执行过程中,通过Model对模板进行渲染,并把渲染后的结果写入相应的输出流,最终返回给请求方。不同模板引擎对应着不同的视图,不同的视图又有其自身的渲染方法。

5:DispatcherServlet请求分发过程总结

在第四章,我们通过一张图片,并结合DispatcherServlet源码,来讲述了DispatcherServlet请求分发的大致过程。接下来,我们对这张图做一个总结,然后再走一遍完整的调试过程。

DispatcherServlet的请求分发的大致过程如下,该过程是在DispatcherServlet类中的doDispatch方法中实现的,所以后面进行调试的时候,主要就是针对此方法。

在这里插入图片描述

1、前端控制器DispatcherServlet由框架提供

作用:接收请求,进行请求分发,处理响应结果

2、处理器映射器HandlerMapping由框架提供

作用:根据请求URL,找到对应的Handler

3、处理器适配器HandlerAdapter由框架提供

作用:调用处理器(Handler|Controller)的方法

4、处理器Handler又名Controller,后端处理器

作用:接收用户请求数据,调用业务方法处理请求

5、视图解析器ViewResolver由框架提供

作用:视图解析,把逻辑视图名称解析成真正的物理视图
支持多种视图技术:JSTLView,FreeMarker…

6、页面资源,程序员开发

作用:将数据展现给用户

6:doDispatch方法的完整源码调试

在前面我们已经说到 对请求的所有处理都封装在其doService 方法中调用的doDispatch方法中。一旦进入doDispatch方法,就可以视为与原始Servlet的对接已经完成,后续将会进入Spring MVC相关核心功能。在doDispatch方法中,会执行所有的请求处理操作,包括请求分发、响应处理等核心操作。这也是整个Spring MVC中最复杂的部分。 接下来我们就对doDispatch方法进行调试

代码示例如下:

@Controllerpublic class MyController {
@RequestMapping(value = "/some.do") public ModelAndView dosome(){
//doGet ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("msg","欢迎使用springmvc做web开发"); modelAndView.setViewName("show"); return modelAndView; }}

springmvc.xml部分配置

第一步:在方法处打一个断点,并以debug模式启动

在这里插入图片描述
在这里插入图片描述
第二步:在浏览器访问页面
在这里插入图片描述
第三步:在栈帧窗口中,找到doService:943,DispatcherServlet栈帧,并重新启动debug模式运行。
在这里插入图片描述
第四步:在此访问浏览器,就可以跳转到doDispatch方法入口处

在这里插入图片描述

第五步:对doDispatch方法执行单步调试
在这里插入图片描述
展示重点部分的调试过程
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

第六步:doDispatch方法的大致源码调试过后,接下来,

在这里插入图片描述

在这里插入图片描述

上面就是doDispatch方法的完整源码调试,在此方法中,我们遇到如下的方法,都可以进入方法内部单步执行,找到我们对应的映射器和适配器,还有视图解析器。

mappedHandler = getHandler(processedRequest);		//..HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());//..mv = ha.handle(processedRequest, response, mappedHandler.getHandler());//.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

7:一些想说的话

终于把想写的给写完了,等等这句话啥意思??说明SpringMVC框架并没有这么简单,还有很多东西还要自己去学去慢慢体会。下面把值的探究的问题一一列出来

在这里插入图片描述

7.1 Filter组件——过滤器

我们在找SpringMVC的请求入口之前,会先通过由所有的Filter组件构成的Filter链对请求进行过滤与预处理,在这个Filter链中没有对请求提前结束处理,才会进入Servlet组件对请求进行处理,这里的Filter组件就值得我们去探究。

举一个例子:从页面上只能发起两种请求,GET、POST ,其他的请求方式(PUT,DELETE)没法使用。那我们就可以通过HiddenHttpMethodFilter组件来解决此问题(查看它的源码就可以知道),它可以把普通的请求转化为规定形式的请求,使用方式也很简单,如果是使用Spring 1.x——基于XML来开发项目的,只要在web.xml配置文件加上如下代码即可

HiddenHttpMethodFilter
org.springframework.web.filter.HiddenHttpMethodFilter
HiddenHttpMethodFilter
/*

在这里插入图片描述

7.2 本地化与主题的使用

在第3章我们得出了一个结论:所有的请求入口在Spring的DispatcherServlet组件中,都是以processRequest方法开始的。该方法是DispatcherServlet的父类FrameworkServlet类下的一个方法。大家看processRequest方法的源码,可以得出如下结论:该方法先进行上下文的初始化操作,最后会进行上下文请求重置的操作,真正的请求与响应操作则是通过doService方法完成的。

在构造本地化上下文时,调用方法buildLocaleContext。 在这个方法逻辑中,会使用到本地化解析器解析请求中的地区信息,这也是一个值得深究的问题

该方法有3个比较关键的组件

本地化上下(LocaleContext): 用于在整个请求期间维护当前请求的本地化信息,请求的本地化信息通过request.getLocale()方法执行,以供后续所有的处理过程使用。在任何一个处理过程中,可能都需要使用到本地化信息,根据不同的判断,输出不同的内容,所以本地化信息使用这种方式存储。同时本地化信息除包括地区信息外,还可以包括时区信息。

请求属性上下文(ServletRequestAttributes): 包括Request 对象的属性和Session 对象的属性两部分,用于提供给后续的处理过程来读写其属性,一 般用于跨方法传递参数。如在A方法中通过上下文设置的属性,在整个请求处理过程中,B方法也可以通过上下文获取该属性。

异步管理器( WebAsyncManager):用于对异步请求响应提供支持,所有的异步操作都通过异步管理器进行管理,其同样是上下文,绑定到请求的生命周期中。后续在对异步的所有处理中,都需要通过请求获取与之绑定的异步管理器进行操作。

上面的三个组件,可能对于我们来说一脸懵逼,但是他们都很有用,在以后的工作中,如果遇到了对应问题,诶,我们就可以在次找到该问题所在的组件,进行深究

在这里插入图片描述

7.3 Interceptor组件——拦截器

第4章讲到,在DispatcherServlet的doDispatch方法中,每段逻辑都有其自身的目的,且整体从前到后都是按照请求处理的流程在执行。在上述doDispatch方法中共分为11个逻辑,分别如下。

(1)预处理多块请求。

(2)获取请求处理器。

(3)查找处理适配器。

(4)处理HTTP缓存。

(5)执行前置拦截器链。

(6)处理适配器执行。

(7)返回值视图名处理。

(8)执行后置拦截器链。

(9)处理返回值与响应。

(10)执行完成拦截器链。

(11)清理资源。

这里面的(5),(8)就是跟拦截器组件有关,在以后的工作中我们用到拦截器是不可避免的。

举一个例子:拦截器最常见的使用场最是用户登录校验。一个网站添加用户登录相关功能后,通过登录请求校验用户提供的用户名与密码,作为用户的认证信息。当这个登录请求对应的处理器校验通过时,会把用户认证信息放入Session中,并把Sessionld通过响应头Set-Cookie传递给请求方,请求方把此Sessionld放入Cookie.

在下次请求时,通过请求头中的Cookie传递此Sessionld,服务端再通过Sessionld 获取当前请求的Session数据。在拦截器中,先尝试通过Session 获取用户的认证信息,如果获取失败,则拦截此请求,并响应重定向信息,把页面重定向到登录页面:如果成功获取用户的认证信息,则放行此请求,执行后续处理。

你看一个小小的登录问题就牵扯到了两个,我们要学习的知识,Cookie和拦截器。

在这里插入图片描述

7.4 handle方法执行过程

在第4章,我们只是一笔带过处理适配器执行(handle)的问题,可是这个问题中的实现方法是整个请求处理过程中最核心的方法,也是在调用了这个方法后,最终才调用到了处理器方法。

所有对请求处理的封装都在这个方法(handle)内部

在这里插入图片描述

7.5 @RequestMapping查找原理

在第4章讲述了,如何获取一个请求处理器链( mappedHandler = getHandler(processedRequest);),对getHandler代码逻辑也进行了探究。该代码逻辑虽然表面上看起来很简单,就是遍历所有的处理器映射,来尝试获取与当前请求匹配的处理器执行链,但是,其实核心逻辑都在处理器映射组件(RequestMappingHandlerMapping)中

在这里插入图片描述

7.6 SpringMVC相关组件

在第4章介绍的整个请求的处理过程(doDispatch)中出现了很多组件,这些组件都是Spring MVC整个处理过程中不可获缺的部分。正是通过这些组件之间的搭配组合,才令整个Spring MVC框架完整地运行起来,使用框架进行开发时,各种便捷功能都是通过这些组件辅助完成的。这就像盖房子一样,通过一个个小组件,最终搭建起一个完整的艺术品。

如下就是常说的DispatcherServlet的九大组件。其属性定义的源码:

//多块请求解析器:用于判断请求是否为多块请求,对多块请求进行一些预先解析。//简单来说就是文件上传功能实现@Nullableprivate MultiipartResolver multipartResolver;// 本地化解析器:用于解析请求对应的Locale。 可以是其子接口LocaleContextResolver本地化上下文解析器,额外添加获取时区功能。//简单来说就是区域信息解析器和国际化@Nullableprivate LocaleResolver localeResolver//主题解析器:用于解析请求对应的主题名,以支持主题选择与切换。@Nullableprivate ThemeResolver themeResolver;//处理器映射列表:用于根据请求查找对应的处理器执行链。@Nullableprivate List
handlerMappings;//处理适配器列表:用于对处理器执行链进行适配执行。@Nullableprivate List
handlerAdapters;//处理器异常解析器列表:用于解析整个处理过程中发生的异常,把异常解析为ModelAndView的模型视图结果,并执行统一的渲染逻辑@Nullableprivate List
viewResolvers;

在整个请求的处理过程中,使用到了上面的所有组件。除了组件的使用外,还有个很关键的部分与组件有关,那就是这些组件是如何在DispatcherServlet 中初始化的,运行时这些组件的来源是什么,它们是如何初始化的,通过Spring MVC配置属性与配置类进行的配置又是如何影响到各个组件的特性的???

在这里插入图片描述

对上面值得探究的6个问题,会在以后为大家讲述

转载地址:http://prozi.baihongyu.com/

你可能感兴趣的文章
一次很折腾的扩容,记录一下之后再整理
查看>>
VirtualBox虚拟机网络配置
查看>>
oracle vm virtualbox虚拟机下,CentOS7系统网络配置
查看>>
Windows 10下Docker使用经验谈
查看>>
centos下nmap安装和基础命令
查看>>
ubuntu出现有线已连接却无法上网
查看>>
一句话命令
查看>>
解决Linux CentOS中cp -f 复制强制覆盖的命令无效的方法
查看>>
wdcpv3升级到v3.2后,多PHP版本共存的安装方法
查看>>
centos tar压缩与解压缩
查看>>
Centos 7防火墙firewalld/iptables开放80端口
查看>>
centos 7 yum源文件配置详解及163 yum源更换
查看>>
PHP统计当前网站的访问人数,访问信息,被多少次访问。
查看>>
Windows10远程报错CredSSP加密oracle修正
查看>>
Windows server 2016 设置多用户登陆
查看>>
偶然发现的面包屑
查看>>
每天自动升级你的Centos
查看>>
WDCP v3版本的小工具集
查看>>
CentOS 7 下挂载NTFS文件系统磁盘并设置开机自动挂载
查看>>
Mysql修改最大连接数&重启
查看>>