【Spring MVC】请求处理过程

1  前言

前面分析了 Spring MVC 的创建过程,本章分析Spring MVC 是怎么处理请求的。我们这里分两步:首先分析 HtpServletBean、FrameworkServlet 和 DispatcherServlet 这三个 Servlet 的处理过程,这样大家可以明白从Servlet 容器将请求交给Spring MVC一直到 DispatcherServlet具体处理请求之前都做了些什么,最后再重点分析 Spring MVC 中最核心的处理方法doDispatch 的结构。

2  HttpServletBean

HttpServletBean 主要参与了创建工作,并没有涉及请求的处理。之所以单独将它列出来是为了明确地告诉大家这里没有具体处理请求。

3  FrameworkServlet

前面讲过 Servlet 的处理过程:首先是从 Servlet 接口的 service 方法开始,然后在 HtpServlet 的 service 方法中根据请求的类型不同将请求路由到了 doGet、doHead、doPost、doPutdoDelete doOptions 和doTrace 七个方法,并且做了 doHead,doOptions和 doTrace 的默认实现其中doHead 调用 doGet,然后返回只有 header 没有 body的response。

在FrameworkServlet中重 写了service、doGet、doPost、doPut、doDelete、doOptionsdoTrace 方法(除了doHead 的所有处理请求的方法)。在 service 方法中增加了对 PATCH类型请求的处理,其他类型的请求直接交给了父类进行处理:dOptions 和 doTrace 方法可以通过设置 dispatchOptionsRequest 和 dispatchTraceRequest参数决定是自已处理还是交给父类处理(默认都是交给父类处理,doOptions 会在父类的处理结果中增加 PATCH类型);doGetdoPost、doPut和doDelete 都是自已处理。所有需要自已处理的请求都交给了 processRequest方法进行统一处理。

下面来看一下 service 和doGet 的代码,别的需要自已处理的方法都和 doGet 类似。

我们发现这里所做的事情跟 HttpServlet 里将不同类型的请求路由到不同方法进行处理的思路正好相反,这里又将所有的请求合并到了 processRequest 方法。当然并不是说 SpringMVC 中就不对 request 的类型进行分类,而全部执行相同的操作了,恰恰相反,Spring MVO中对不同类型请求的支持非常好,不过它是通过另外一种方式进行处理的,它将不同类型的请求用不同的 Handler 进行处理,后面再详细分析。

可能有的读者会想,直接覆盖了 service 不是就可以了吗? HttpServlet 是在 service 方法中将请求路由到不同的方法的,如果在 service 中不再调用super.service0,而是直接将请求交给processRequest处理不是更简单吗?从现在的结构来看确实如此,不过那么做其实存在着一些问题。比如,我们为了某种特殊需求需要在 Post 请求处理前对 request 做一些处理,这时可能会新建一个继承自 DispatcherServlet 的类,然后覆盖doPost 方法,在里面先对 request 做处理然后再调用 supper.doPost0,但是父类根本就没调用doPost,所以这时候就会出问题了。虽然这个问题的解决方法也很简单,但是按正常的逻辑,调用 doPost应该可以完成才合理,而目一般情况下开发者并不需要对 Spring MVC 内部的结构非常了解,所以Spring MVC 的这种做法虽然看起来有点笨拙但是是必要的。

 

processRequest方法中的核心语句是 doService(request,response),这是一个模板方法,在 DispatcherServlet 中具体实现。在 doService 前后还做了一些事情(也就是大家熟悉的装饰模式):首先获取了LocaleContextHolder 和 RequestContextHolder 中原来保存的 LocaleContext 和 RequestAttributes并设置到 previousLocaleContext 和 previousAtributes 临时属性,然后调用 buildLocaleContext 和 buildRequestAttributes 方法获取到当前请求的 LocaleContext和RequestAttributes,并通过 initContextHolders方法将它们设置到 LocaleContextHolder 和 RequestContextHolder 中(处理完请求后再恢复到原来的值),接着使用request 拿到异步处理管理器并设置了拦截器,做完这些后执行了doService 方法,执行完后,最后(fnally中)通过resetContextHolders方法将原来的 previousLocaleContext 和 previousAttributes 恢复到 LocaleContextHolder 和RequestContextHolder 中,并调用 publishRequestHandledEvent方法发布了-个 ServletRequestHandledEvent 类型的消息。

这里涉及了异步请求相关的内容,Spring MVC 中异步请求的内容会在后面专门讲解。除了异步请求和调用doService 方法具体处理请求,processRequest自己主要做了两件事情:@对LocaleContext和 RequestAttributes 的设置及恢复;处理完后发布了 Servlet.RequestHandledEvent 消息。

首先来看一下LocaleContext 和 RequestAttributes。LocaleContext 里面存放着 Locale(也就是本地化信息,如zh-cn等),RequestAttributes 是 spring 的一个接口,通过它可以get/setremoveAttribute,根据 scope 参数判断操作 request还是 session。这里具体使用的是 ServletRequestAttributes类,在ServletRequestAttributes 里面还封装了request、response 和 session,而且都提供了get 方法,可以直接获取。下面来看一下ServletRequestAttributes 里 setAttribute 的代码(get/ remove都大同小异)。

设置属性时可以通过 scope 判断是对 request 还是 session 进行设置,具体的设置方法非常简单,就是直接对 request 和 session 操作,sessionAttributesToUpdate 属性后面讲到 Session-AttributesHandler 的时候再介绍,这里可以先不考虑它。需要注意的是 isRequestActive方法当调用了 ServletRequestAttributes 的 requestCompleted方法后requestActive 就会变为false.执行之前是 true。这个很容易理解,request 执行完了,当然也就不能再对它进行操作了!你可能已经注意到,在刚才的finally块中已调用requestAttributes 的 requestCompleted方法。

现在大家对 LocaleContext和 RequestAttributes 已经有了大概的了解,前者可以获取Locale,后者用于管理request和 session 的属性。不过可能还是有种没有理解透的感觉因为还不知道它到底怎么用。不要着急,我们接下来看 LocaleContextHolder 和 RequestContextHolder,把这两个理解了也就全部明白了!

先来看LocaleContextHolder,这是一个abstract 类,不过里面的方法都是 static 的,可以直接调用,而且没有父类也没有子类!也就是说我们不能对它实例化,只能调用其定义的static方法。这种abstract 的使用方式也值得我们学习。在 LocaleContextHolder 中定义了两个static 的属性。

这两个属性都是 ThreadLocal<LocaleContex>类型的,LocaleContext 前面已经介绍了ThreadLocal大家应该也不陌生,很多地方都用了它。

LocaleContextHolder类里面封装了两个属性loaleContextHolder 和inheritableLocale.ContextHolder,它们都是 LocaleContext,其中第二个可以被子线程继承。Locale-ContextHolder还提供了 get/set方法,可以获取和设置 LocaleContext,另外还提供了get/setLocale 方法,可以直接操作 Locale,当然都是 static 的。这个使用起来非常方便!比如,在程序中需要用到 Locale 到时候,首先想到的可能是 request.getLocale(),这是最直接的方法。不过有时候在service 层需要用到 Locale 的时候,再用这种方法就不方便了,因为正常来说service层是没有request的,这时可能就需要在 controller 层将 Locale 拿出来,然后再传进去了!当然这也没什么,传一下就好了,但最重要的是怎么传呢?服务层的代码可能已经通过测试了,如果想将 Locale 传进去可能就需要改接口,而修改接口可能会引起很多问题!而有了LocaleContextHolder 就方便多了,只需要在 server 层直接调用一下 LocaleContextHoldergetLocale0 就可以了,它是静态方法,可以直接调用! 当然,在 Spring MVC 中 Locale 的值并不总是 request.getLocale0) 获取到的值,而是采用了非常灵活的机制,在后面的 LocaleResolver中再详细讲解。

RequestContextHolder也是一样的道理,里面封装了RequestAttributes,可以get/setremoveAttribute,而且因为实际封装的是 ServletRequestAttributes,所以还可以getRequestgetResponse、getSession!这样就可以在任何地方都能方便地获取这些对象了!另外,因为里面封装的其实是对象的引用,所以即使在 doService 方法里面设置的Attribute,使用RequestContextHolder 也一样可以获取到。

在方法最后的fnally 中调用resetContextHolders 方法将原来的 LocaleContext和 RequestAtributes 又恢复了。这是因为在 Sevlet 外面可能还有别的操作,如 Filter (Spring-MVC自己的 HandlerInterceptor 是在 doService 内部的)等,为了不影响那些操作,所以需要进行恢复。

最后就是 publishRequestHandledEvent ( request,response,startTime,failureCause) 发布消息了。在 publishRequestHandledEvent 内部发布了一个 ServletRequestHandledEvent 消息,代码如下:

当publishEvents 设置为 true 时,请求处理结束后就会发出这个消息,无论请求处理成功与否都会发布。publishEvents 可以在 web.xml文件中配置 Spring MVC的Servlet 时配置,默认为 true。我们可以通过监听这个事件来做一些事情,如记录日志。

下面就写一个记录日志的监听器。

我们可以看到,只要简单地继承ApplicationListener,并且把自已要做的事情写到onApplicationEvent里面就行了。很简单吧!当然要把它注册到 spring 容器里才能起作用,如果开启了注释,只要在类上面标注@Component 就可以了。

到现在为止 FrameworkServlet 就分析完了,我们再简单地回顾一下:首先是在 service 方法里添加了对PATCH的处理,并将所有需要自己处理的请求都集中到了 processRequest方法进行统一处理,这和 HttpServlet 里面根据 request 的类型将请求分配到各个不同的方法进行处理的过程正好相反。

然后就是 processRequest 方法,在 processRequest 里面主要的处理逻辑交给了 doService.这是一个模板方法,在子类具体实现,另外就是对使用当前 request 获取到的 LocaleContext和 RequestAtributes 进行了保存,以及处理完之后的恢复,在最后发布了 ServletRequest-HandledEvent 事件。

4  DispatcherServlet

DispatcherServlet 是 Spring MVC 最核心的类,整个处理过程的顶层设计都在这里面,所以我们一定要把这个类彻底弄明白。

通过之前的分析我们知道,DispatcherServlet 里面执行处理的入口方法应该是 doService,不过 doServic 并没有直接进行处理,而是交给了 doDispatch 进行具体的处理,在 doDispatch处理前 doServic 做了一些事情:首先判断是不是 include 请求,如果是则对 request 的 Attribute做个快照备份,等 doDispatch 处理完之后(如果不是异步调用且未完成)进行还原,在做完快照后又对 request 设置了一些属性,代码如下:

 

对request 设置的属性中,前面4个属性 webApplicationContext、localeResolver、theme-Resolver 和 themeSource 在之后介绍的 handler 和 view 中需要使用,到时候再作分析。后面三个属性都和 fashMap 相关,主要用于 Redirect 转发时参数的传递,比如,为了避免重复提交表单,可以在处理完 post 请求后 redirect 到一个 get 的请求,这样即使用户刷新也不会有重复提交的问题。不过这里有个问题,前面的 post 请求是提交订单,提交完后 redirect 到一个显示订单的页面,显然在显示订单的页面需要知道订单的一些信息,但 redirect 本身是没有传递参数的功能的,按普通的模式如果想传递参数,就只能将其写人 url 中,但是url 有长度限制另外有些场景中我们想传递的参数还不想暴露在url里,这时就可以用fashMap 来进行传递了,我们只需要在redirect之前将需要传递的参数写入OUTPUT FLASH MAPATTRIBUTE如下(这里使用了前面讲到的 RequestContextHolder):

这样在redirect 之后的 handle 中 spring 就会自动将其设置到 model里(先设置到INPUTFLASH MAP ATTRIBUTE 属性里,然后再放到model里)。当然这样操作还是有点麻烦spring还给我们提供了更加简单的操作方法,我们只需要在 handler 方法的参数中定义 Redire.ctAttributes类型的变量,然后把需要保存的属性设置到里面就行,之后的事情 spring 自动完成。RedirectAtributes 有两种设置参数的方法addAttribute ( key,value)和addFlashAttribute(key,value),用第一个方法设置的参数会拼接到 url 中,第二个方法设置的参数就是用我们刚才所讲的flashMap 保存的。比如,一个提交订单的Controller 可以这么写:

这里分别使用了三种方法来传递redirect参数:

  • 使用前面讲过的 RequestContextHolder 获取到request,并从其属性中拿到output-FlashMap,然后将属性放进去,当然 request 可以直接写到参数里让 Spring MVC给设置进来,这里主要是为了让大家看一下使用 RequestContextHolder 获取 request 的方法。
  • 通过传人的attr参数的addFlashAttribute 方法设置,这样也可以保存到output-FlashMap中,和第1种方法效果一样。
  • 通过传人的attr 参数的addAttribute 方法设置,这样设置的参数不会保存到 FlashMap而是会拼接到url中。

从Request获取outputFlashMap 除了直接获取 DispatcherServlet.OUTPUT FLASH MAPATTRIBUTE属性,还可以使用 RequestContextUtils 来操作: RequestContextUtils.getOutputFlashMap(request),这样也可以得到outputFlashMap,其实它内部还是从 Request 的属性获取的。

当用户提交 http://xxx/submit请求后浏览器地址栏会自动跳转到 http://xxx/showorders?Local=zh-cn链接,而在 showOrders 的 model里会存在[name""张三丰和["ordersId"”xxx” 两个属性,而且对客户端是透明的,用户并不知道。

这就是fashMap 的用法,inputFlashMap 用于保存上次请求中转发过来的属性,output-FlashMap 用于保存本次请求需要转发的属性,FlashMapManager 用于管理它们,后面会详细分析 FlashMapManager。

doService 就分析完了,在这里主要是对 request 设置了一些属性,如果是 include 请求还会对 request 当前的属性做快照备份,并在处理结束后恢复。最后将请求转发给 doDispatch方法。

doDispatch 方法也非常简洁,从顶层设计了整个请求处理的过程。doDispatch 中最核心的代码只要 4句,它们的任务分别是:D根据 request 找到 Handler;2根据 Handler 找到对应的HandlerAdapter;3用HandlerAdapter 处理 Handler;@调用 processDispatchResult 方法处理上面处理之后的结果(包含找到 View 并染输出给用户),对应的代码如下:

这里需要解释三个概念:HandlerMapping、Handler 和 HandlerAdapter。这三个概念的准确理解对于Spring MVC 的学习非常重要。如果对这三个概念理解得不够透彻,将会严重影响
对SpringMVC的理解。下面给大家解释一下:

Handler:也就是处理器,它直接对应着MVC 中的C也就是 Controller 层,它的具体表现形式有很多,可以是类,也可以是方法,如果你能想到别的表现形式也可以使用,它的类型是Object。我们前面例子中标注了 @RequestMapping 的所有方法都可以看成一个Handler。只要可以实际处理请求就可以是 Handler。

HandlerMapping:是用来查找 Handler 的,在Spring MVC中会处理很多请求,每个请求都需要一个 Handler 来处理,具体接收到一个请求后使用哪个 Handler 来处理呢?这就是HandlerMapping要做的事情。

通俗点的解释就是 Handler 是用来干活的工具,HandlerMapping 用于根据需要干的活找到相应的工具,HandlerAdapter 是使用工具干活的人。比如,Handler 就像车床、铣床、电火花之类的设备,HandlerMapping 的作用是根据加工的需求选择用什么设备进行加工,而HandlerAdapter 是具体操作设备的工人,不同的设备需要不同的工人去加工,车床需要车工,铣床需要铣工,如果让车工使用铣床干活就可能出问题,所以不同的 Handler 需要不同的HandlerAdapter 去使用。我们都知道在干活的时候人是柔性最强、灵活度最高的,同时也是问题最多、困难最多的。Spring MVC 中也一样,在九大组件中 HandlerAdapter 也是最复杂的所以在后面学习HandlerAdapter 的时候要多留心。

另外 View和 ViewResolver 的原理与 Handler 和HandlerMapping 的原理类似。View 是用来展示数据的,而 ViewResolver 用来查找 Vew。通俗地讲就是干完活后需要写报告,写报告又需要模板(比如,是调查报告还是验收报告或者是下一步工作的请示等),View 就是所需要的模板,模板就像公文里边的格式,内容就是 Model 里边的数据,ViewResolver 就是用来选择使用哪个模板的。

现在再回过头去看上面的四句代码应该就觉得很容易理解了,它们分别是:使用HandlerMapping 找到于活的 Handler,找到使用 Handler 的 HandlerAdapter,让 HandlerAdapter使用Handler 干活,干完活后将结果写个报告交上去(通过 View 展示给用户)。

5  doDispatch 结构

上边介绍了 doDispatch 做的4 件事,不过只是整体介绍,本节详细分析 doDispatch 内部的结构以及处理的流程。先来看 doDispatch 的代码:

 

doDispatch 大体可以分为两部分:处理请求和染页面。开头部分先定义了几个变量,在后面要用到,如下:

  • HttpServletRequest processedRequest:实际处理时所用的request,如果不是上传请求则直接使用接收到的request,否则封装为上传类型的request。
  • HandlerExecutionChain mappedHandler:处理请求的处理器链(包含处理器和对应的Interceptor)。
  • boolean multipartRequestParsed:是不是上传请求的标志。
  • ModelAndView mv:封装 Model和 View 的容器,此变量在整个 Spring MVC处理的过程中承担着非常重要角色,如果使用过 Spring MVC就不会对ModelAndView 陌生。
  • Exception dispatchException:处理请求过程中抛出的异常。需要注意的是它并不包含渲染过程抛出的异常。

doDispatch 中首先检查是不是上传请求,如果是上传请求,则将 request 转换为 Multi-partHttpServletRequest,并将 multipartRequestParsed标志设置为 true。其中使用到了 Multipart-Resolver。

然后通过 getHandler 方法获取 Handler 处理器链,其中使用到了 HandlerMapping,返回值为HandlerExecutionChain 类型,其中包含着与当前request 相匹配的Interceptor 和 Handler。getHandler代码如下:

方法结构非常简单,HandlerMapping 在后面详细讲解,HandlerExecutionChain 的类型类似于前面 Tomcat 中讲过的 Pipeline,Interceptor 和 Handler 相当于那里边的 Value 和 BaseValue,执行时先依次执行 Interceptor 的 preHandle 方法,最后执行 Handler,返回的时候按相反的顺序执行 Interceptor的 postHandle 方法。就好像要去一个地方,Interceptor 是要经过的收费站,Handler 是目的地,去的时候和返回的时候都要经过加油站,但两次所经过的顺序是相反的。

接下来是处理GET、HEAD请求的 Last-Modifed。当浏览器第一次跟服务器请求资源(GET、Head 请求)时,服务器在返回的请求头里面会包含一个Last-Modied 的属性,代表本资源最后是什么时候修改的。在浏览器以后发送请求时会同时发送之前接收到的 Last-Modifed,服务器接收到带 Last-Modied 的请求后会用其值和自已实际资源的最后修改时间做对比,如果资源过期了则返回新的资源(同时返回新的 Last-Modifed),否则直接返回 304 状态码表示资源未过期,浏览器直接使用之前缓存的结果。

接下来依次调用相应Interceptor的preHandle。

处理完Interceptor的 preHandle后就到了此方法最关键的地方一一让 HandlerAdapter 使用Handler处理请求,Controller 就是在这个地方执行的。这里主要使用了 HandlerAdapter,具体内容在后面详细讲解。
Handler 处理完请求后,如果需要异步处理,则直接返回,如果不需要异步处理,当 view为空时(如 Handler 返回值为 void),设置默认 view,然后执行相应 Interceptor 的 postHandle。设置默认 view的过程中使用到了 ViewNameTranslator。

到这里请求处理的内容就完成了,接下来使用 processDispatchResult方法处理前面返回的结果,其中包括处理异常、渲染页面、触发 Interceptor 的 afterCompletion 方法三部分内容。

我们先来说一下doDispatch 的异常处理结构。doDispatch 有两层异常捕获,内层是捕获在对请求进行处理的过程中抛出的异常,外层主要是在处理渲染页面时抛出的。内层的异常也就是执行请求处理时的异常会设置到 dispatchException 变量,然后在 processDispatchResult方法中进行处理,外层则是处理 processDispatchResult 方法抛出的异常。processDispatchResult代码如下:

可以看到 processDispatchResult 处理异常的方式其实就是将相应的错误页面设置到 View,在其中的 processHandlerException 方法中用到了 HandlerExceptionResolver。

渲染页面具体在render 方法中执行,render 中首先对response 设置了 Local,过程中使用到了LocaleResolver,然后判断 View 如果是 String类型则调用resolveViewName 方法使用VicwResolver得到实际的 View,最后调用 View 的render 方法对页面进行具体染,渲染的过程中使用到了ThemeResolver。
最后通过 mappedHandler 的 riggerAfterCompletion 方法触发Interceptor 的 afterCompletion方法,这里的Interceptor 也是按反方向执行的。到这里 processDispatchResult 方法就执行完了。
再返回 doDispatch 方法中,在最后的fnally 中判断是否请求启动了异步处理,如果启动了则调用相应异步处理的拦截器,否则如果是上传请求则删除上传请求过程中产生的临时资源。
doDispatch 方法就分析完了。可以看到 Spring MVC 的处理方式是先在顶层设计好整体结构,然后将具体的处理交给不同的组件具体去实现的。doDispatcher 的流程图如图 10-1所示,中间是 doDispatcher 的处理流程图,左边是 Interceptor 相关处理方法的调用位置,右边是 doDispatcher 方法处理过程中所涉及的组件。图中上半部分的处理请求对应着 MVC中的Controller 也就是C层,下半部分的 processDispatchResult 主要对应了 MVC 中的 View 也就是V层,M层也就是Model贯穿于整个过程中。

理解 doDispatcher 的结构之后,在开发过程中如果遇到问题,就可以知道是在哪部分出的问题,从而缩小查找范围,有的放矢地去解决。

6  小结

本章整体分析了 Spring MVC 中请求处理的过程。首先对三个 Servlet 进行了分析,然后单独分析了 DispatcherServlet 中的doDispatch 方法。

三个Servlet 的处理过程大致功能如下:

  • HttpServletBean:没有参与实际请求的处理。
  • FrameworkServlet:将不同类型的请求合并到了processRequest方法统一处理 processRequest 方法中做了三件事: 1、调用了doService模板方法具体处理请求 2、将当前请求的 LocaleContext 和 ServletRequestAttributes 在处理请求前设置到了LocaleContextHolder 和 RequestContextHolder,并在请求处理完成后恢复 3、请求处理完后发布了 ServletRequestHandledEvent 消息。

  • DispatcherServlet:doService 方法给request 设置了一些属性并将请求交给 doDispatch方法具体处理。

DispatcherServlet 中的 doDispatch 方法完成Spring MVC 中请求处理过程的顶层设计,它使用DispatcherServlet 中的九大组件完成了具体的请求处理。另外 HandlerMapping、Handler和HandlerAdapter 这三个概念的含义以及它们之间的关系也非常重要。

posted @ 2023-03-25 23:33  酷酷-  阅读(172)  评论(0编辑  收藏  举报