Spring MVC 中的核心问题:处理请求的过程 (essential questions series)

224 阅读7分钟

Spring MVC 处理请求的过程

涉及到的组件:

  1. DispatcherServlet

  2. HandlerMapping

  3. HandlerAdapter

关于这三个组件的详细内容,下面会单独介绍

处理请求过程

Step 1 : 经过过滤器链

目的:请求的过滤和预处理

常见的过滤器:强制指定请求和响应的编码格式 / 将post请求转化为别的请求(因为有的浏览器只支持Get/Post)/ 初始化RequestContext上下文,把当前请求和响应放入Spring MVC的请求上下文中。

Step 2 :到达Servlet组件

到达javax.servlet.http.HttpServlet,这是一个抽象类,执行的时候肯定是一个具体类,这个具体类就是 DispatcherServlet.

  1. 进入 HttpServlet 中的service方法

(1)将原始的Servlet请求转化为基于Http的Servlet请求和响应

类型转换: ServletRequest->HttpServletRequest

然后调用重载的service方法(同一个类中),传入的方法参数就是上述转化后的req,rep

  1. 同一个类中重载的service方法

image.png

根据请求类型(Get,Post,Delete)执行do*()方法,如doPost,doGet等*

分发片段

image.png

需要注意的的是,Spring MVC 作为对原生代码的增强,自然复用了这段代码,继承HttpServlet的同时(FrameworkServlet类,依旧是抽象类),并重写了doGet()、doPost()、..方法,在这些方法里面,代码都如下:

image.png

image.png

image.png

Step 3殊途同归:处理请求

定义在FrameworkServlet中的processRequest()方法,所有类型的请求都最终调用它。

  1. 完整方法

protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
      
    //-------------构建本地化的上下文,请求上下文,异步管理器----------------------//
   long startTime = System.currentTimeMillis();
   Throwable failureCause = null;

   LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
   LocaleContext localeContext = buildLocaleContext(request);

   RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
   ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
   //请求的处理和当前线程是异步关系
   WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
   asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

   initContextHolders(request, localeContext, requestAttributes);
   
   //-------------------------执行业务------------------------------------//

   try {
      doService(request, response);
   }
   
   //--------------------------异常捕获-----------------------------------//
   catch (ServletException | IOException ex) {
      failureCause = ex;
      throw ex;
   }
   catch (Throwable ex) {
      failureCause = ex;
      throw new NestedServletException("Request processing failed", ex);
   }

   finally {
      resetContextHolders(request, previousLocaleContext, previousAttributes);
      if (requestAttributes != null) {
         requestAttributes.requestCompleted();
      }
      logResult(request, response, failureCause, asyncManager);
      publishRequestHandledEvent(request, response, startTime, failureCause);
   }
}

Step 4 :执行服务

step3代码段中的doService()方法是FrameworkServlet类中的抽象方法,它被子类DispatchServlet重写,宣布请求正式被DispatchServlet接手

  1. 重写后的doService()方法:
 
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
   logRequest(request);

   // Keep a snapshot of the request attributes in case of an include,
   // to be able to restore the original attributes after the include.
   Map<String, Object> attributesSnapshot = null;
   if (WebUtils.isIncludeRequest(request)) {
      attributesSnapshot = new HashMap<>();
      Enumeration<?> attrNames = request.getAttributeNames();
      while (attrNames.hasMoreElements()) {
         String attrName = (String) attrNames.nextElement();
         if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
            attributesSnapshot.put(attrName, request.getAttribute(attrName));
         }
      }
   }

   // Make framework objects available to handlers and view objects.
   request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
   request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
   request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
   request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

   if (this.flashMapManager != null) {
      FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
      if (inputFlashMap != null) {
         request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
      }
      request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
      request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
   }

   try {
      doDispatch(request, response);
   }
   finally {
      if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
         // Restore the original attribute snapshot, in case of an include.
         if (attributesSnapshot != null) {
            restoreAttributesAfterInclude(request, attributesSnapshot);
         }
      }
   }
}
 
  1. 它做了什么?
  • 保存请求属性快照,以便后续恢复(针对include请求类型)

  • 将Spring MVC 的Web应用上下文放入请求属性(request.setAttributes())

  • 将本地化解析器放入请求属性

  • 主题解析器放入请求属性

  • 执行核心分发方法:doDispatch(request, response) 方法

3.解释

(1)Web应用上下文

为了执行的时候可以使用上下文中的组件

(2)本地化解析器

获取当前请求所关联的地理地区,可以针对性的返回对应的本地化视图(如针对不同国家返回不同语言)

(3)主题解析器

可以用来实现前端页面使用的切换主题功能(如返回不同的css)

在这里停一下,Step1 ~ Step4 收到请求,并完成原始Servlet和Spring MVC 的对接

对接完毕后,进入doDispatch方法,会执行所有的请求处理操作,包括请求的分发,响应的处理等核心操作。这也是整个Spring MVC中最复杂的部分

在正式开始剖析之前,看一眼doDispatch的流程图:

image.png

对应的源代码如下,与上图中复杂的流程相比,整个doDispatch显得并不是很长,因为每个方法都经历了独立的封装,你可以找到图中箭头和代码的对应关系

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
   HttpServletRequest processedRequest = request;
   HandlerExecutionChain mappedHandler = null;
   boolean multipartRequestParsed = false;

   WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

   try {
      ModelAndView mv = null;
      Exception dispatchException = null;

      try {
         processedRequest = checkMultipart(request);
         multipartRequestParsed = (processedRequest != request);

         // Determine handler for the current request.
         mappedHandler = getHandler(processedRequest);
         if (mappedHandler == null) {
            noHandlerFound(processedRequest, response);
            return;
         }

         // Determine handler adapter for the current request.
         HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

         // Process last-modified header, if supported by the handler.
         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;
            }
         }

         if (!mappedHandler.applyPreHandle(processedRequest, response)) {
            return;
         }

         // Actually invoke the handler.
         mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

         if (asyncManager.isConcurrentHandlingStarted()) {
            return;
         }

         applyDefaultViewName(processedRequest, mv);
         mappedHandler.applyPostHandle(processedRequest, response, mv);
      }
      catch (Exception ex) {
         dispatchException = ex;
      }
      catch (Throwable err) {
         // As of 4.3, we're processing Errors thrown from handler methods as well,
         // making them available for @ExceptionHandler methods and other scenarios.
         dispatchException = new NestedServletException("Handler dispatch failed", err);
      }
      processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
   }
   catch (Exception ex) {
      triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
   }
   catch (Throwable err) {
      triggerAfterCompletion(processedRequest, response, mappedHandler,
            new NestedServletException("Handler processing failed", err));
   }
   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);
         }
      }
   }
}

总结一下:

image.png

Step5 :(分发前)预处理多块请求

针对多块请求这个特殊情况下的预处理,主要为一些简单判断:判断请求是否为多块请求,判断方式是content-type是不是multipart开头的,如果是,执行多块请求解析

image.png

注:multipart请求在传文件的时候会经常用到

Step6 : 查找handler

Spring MVC 把 handler的查找handler的执行 分离

查找该请求对应的处理器执行链,查找结果被封装到一个 HandlerExecutionChain对象

查找方式(遇见HandlerMapping)

遍历全部处理器映射(this.handlerMappings),这个list是DispatchServlet的一个属性,

image.png

getHandler()会循环遍历它

image.png

HandlerMapping是今天三个最重要的组件之二,我们之所以看到上面的代码逻辑很简单,只需要一个for循环遍历handlerMappings找到handler,是因为核心逻辑都在HandlerMapping中

现在我们关注的是Dispatch的完整流程,至于HandlerMapping如何进行匹配的,我们会讲回来,现在我们继续抓住主线。

Step 7 :获取可以执行Handler的适配器(Adapter)

前面提到过,再重复一下这个重要概念:Spring MVC 对于Handler的查找与执行是分离的。在第七步之前我们已经找到了这个Handler。

根据Handler不同,需要使用不同的适配器去执行该处理器,这正是处理适配器 HandlerAdapter的作用

对应的doDispatch中的片段:

image.png

接下来一个小问题,为什么要适配器?

把请求参数适配为处理器需要的参数,把处理器的返回值适配为ModelAndView

Step 8 :处理HTTP缓存功能

HTTP缓存是HTTP协议标准中的定义,用于提高HTTP的效率。像JS,CSS,图片这种文件,下次浏览器发送相同请求的时候,会携带本地缓存的资源时间作为请求头传递给服务端。比如通过If-Modified-Since携带本地资源的最近一次修改时间,如果服务器判断缓存有效,就返回304,表示可以直接使用缓存中的资源。

额外注意一下HTTP中只有GET和HEAD支持缓存,这段代码如下;

image.png

Step 9 :执行前置拦截器链

请求处理器执行链中封装了“拦截器链”,用于在执行请求逻辑前执行,执行请求逻辑后添加后置处理,还可以拦截所有的请求处理,在所有处理完成时或发生异常时添加完成后操作

该过程会正序遍历执行所有拦截器。在拦截器链中,任何拦截器的prehandler方法返回false,均会中断后续执行,这种场景多用于用户登录校验

image.png

Step 10 :执行Handler

交给adapter执行整个流程中最重要的方法(真是千呼万唤始出来)

image.png

Step 11 :返回值视图名处理

image.png

Step 12 :执行后置拦截器链

该过程会倒序执行所有拦截器。

image.png

前置拦截器和后置拦截器的执行逻辑对比:

前置是正序:

image.png

后置是倒序执行:

image.png

Step 13 :处理返回值和响应

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

image.png

步骤:

先看看整个处理过程有没有造成异常:

如果有:

封装异常视图(把异常封装成一个ModelAndView)

如果没有:

第一步:看看处理结果ModelAndView是不是为空

如果为空,打印日志,不为空执行第二步

第二步:

render(mv, request, response);
  • 注:这里的render就是诸如Thymeleaf模版引擎起作用的地方

Step 14 :执行完成拦截器链

Step 15 :清理资源