SpringMVC源码学习(三) - 请求处理的流程

时间:2022-07-26
本文章向大家介绍SpringMVC源码学习(三) - 请求处理的流程,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

在最近的一篇文章中我们大概了解了SpringMVC的九大组件,以及初始化的问题。根本初始化的发起是Spring的事件机制。而这九大组件是什么?我们先回顾一下,他们分别是文件处理器、语言处理器、主题处理器、控制器拦截器处理器、拦截器适配器、异常处理器、接口到视图(页面)处理器、视图处理器、放重复提交管理器。之前的两篇文章我们大概得了解了一下,但没有进行深入实现细节。所以今天我们试图通过一次请求来看看请求的调用链。

我们都知道Servlet容器调用Servlet都是通过Servlet的doGet和doPost方法处理的。至于为什么就变成了一种规范了。没有规范很多问题都会陷入百家争鸣的局面,所以最好的方式就是选一个优质的路然后赌下去。Java就是怎么干的,我觉得我们在生活或许是同样的道理,选好之后就要坚持下去。忠于选择、不悔选择。

通过上述描述我们就目标清楚了,Servlet就一个,那就是我们的DispatcherServlet,那咋就去找那两个方法吧!这里就有点小迷惑了,因为restful接口中有还有put、patch、delete请求,而我们Servlet规范中只有doGet和doPost是不是有点寡不敌众?在这里我们还是老办法:1.有根据的猜测。我们知道put、patch以及delete请求最终还是将数据放到url中所以最终还是应该还是get请求,但是我们知道浏览器中显示的确实不是get请求,那么是不是我们的DispatcherServlet做了兼容处理?如果老版本的DispatcherServlet没有这种兼容的话会不会出现一些问题?感觉越想越多,算了还是看看代码怎么写的吧!

在DispatcherServlet中我们并没有发现doGet和doPost方法,这里显然它继承的是FrameworkServlet,所以我们到它的父类中查找。

果然发现了doGet、doPost、doPut、doDelete方法,但是很遗憾我们没有发现doPatch方法。这里我突然想到了以前我写接口的时候用了Patch,然后前端反映接口调不通。最后将其变成了get请求的过程,这两者是否有关联?

我们跟踪一下这里的processRequest方法,从字面上理解就是对请求进行一些处理。

protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //开始时间
    long startTime = System.currentTimeMillis();
    //报错
    Throwable failureCause = null;
    //这里的语言采用的TreadLocal存储,这里拿到项目设置的语言
    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
    //拿到请求中携带的语言
    LocaleContext localeContext = this.buildLocaleContext(request);
    //这里通过threadLocal存储的ServletRequestAttributes 
    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    //这里获取ServletRequest
    ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
    //异步管理,处理是否使用异步,并将自己注入到request中
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    //这里注册了FramworkSerlvet的类,但是并没有执行哦
    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());
    //这里做了一些处理,主要是往ThreadLocal中设置值
    this.initContextHolders(request, localeContext, requestAttributes);
    try {
        //真正的处理请求
        this.doService(request, response);
    } catch (IOException | ServletException var16) {
        failureCause = var16;
        throw var16;
    } catch (Throwable var17) {
        failureCause = var17;
        throw new NestedServletException("Request processing failed", var17);
    } finally {
    //清除threadlocal中数据
        this.resetContextHolders(request, previousLocaleContext, previousAttributes);
        if (requestAttributes != null) {
            requestAttributes.requestCompleted();
        }
       //日志相关,debug开启
        this.logResult(request, response, (Throwable)failureCause, asyncManager);
        //请求事件广播
        this.publishRequestHandledEvent(request, response, startTime, (Throwable)failureCause);
    }
}

在首次debug代码中,我们发现DispatcherServlet居然可以对每个请求都打印日志,现在想我们当时还自己开发一个注解来收集日志的方式有点花蛇多足。这可能就是源码的作用,我们只需要做好学好Spring的消息的广播就能做到日志的请求日志的自动收集。这一招我学会了,你get到了么?

所以我们将重点放到this.doService(request, response);方法上。

protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
    //格式化打印日志
    this.logRequest(request);
    Map<String, Object> attributesSnapshot = null;
    if (WebUtils.isIncludeRequest(request)) {
        attributesSnapshot = new HashMap();
            Enumeration attrNames = request.getAttributeNames();
        label95:
        while(true) {
            String attrName;
            do {
                if (!attrNames.hasMoreElements()) {
                    break label95;
                    }
                attrName = (String)attrNames.nextElement();
            } while(!this.cleanupAfterInclude && !attrName.startsWith("org.springframework.web.servlet"));

            attributesSnapshot.put(attrName, request.getAttribute(attrName));
        }
        }
        //将SpringMVC的组件设置到request中
    request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.getWebApplicationContext());
    request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
    request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
    request.setAttribute(THEME_SOURCE_ATTRIBUTE, this.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 {
    //真实的数据处理
        this.doDispatch(request, response);
    } finally {
    //拿到attribute中异步处理器实体判断是否要执行了。
        if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) {
            this.restoreAttributesAfterInclude(request, attributesSnapshot);
            }
    }
}

那么我们看看this.doDispatch(request, response);做了哪些事情;

//看样子这里是真正搞事情的地方
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
  HttpServletRequest processedRequest = request;
  //这个应该就是控制器了
  HandlerExecutionChain mappedHandler = null;
  boolean multipartRequestParsed = false;
  //先亮出异步调用管理器的法器
  WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

  try {
    try {
    //拿到视图
      ModelAndView mv = null;
      Object dispatchException = null;
      try {
      //检查是否为文件上传
        processedRequest = this.checkMultipart(request);
        //判断会否发生了变化
        multipartRequestParsed = processedRequest != request;
        //获取与之相应的控制器
        mappedHandler = this.getHandler(processedRequest);
        if (mappedHandler == null) {
          this.noHandlerFound(processedRequest, response);
          return;
        }
       //传入所有的handler,然后通过适配器进行校验。然后返回第一个合适的handler
        HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
        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;
        }
       //执行控制器
        mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
        if (asyncManager.isConcurrentHandlingStarted()) {
          return;
        }
      //处理请求返回一个页面的问题
        this.applyDefaultViewName(processedRequest, mv);
        mappedHandler.applyPostHandle(processedRequest, response, mv);
      } catch (Exception var20) {
        dispatchException = var20;
      } catch (Throwable var21) {
        dispatchException = new NestedServletException("Handler dispatch failed", var21);
      }
      //视图解析等
      this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
    } catch (Exception var22) {
      this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
    } catch (Throwable var23) {
      this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
    }
  } finally {
    if (asyncManager.isConcurrentHandlingStarted()) {
      if (mappedHandler != null) {
        mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
      }
    } else if (multipartRequestParsed) {
      this.cleanupMultipart(processedRequest);
    }
  }
}

那我们再看看拦截器是怎么执行的,发现是全部的拦截器都执行了。但是我们好像记得拦截器可以设置拦截路径的。

通过跟踪,发现我们在项目中自定义的拦截器其实被包裹成了。而spring真正处理的是MappedInterceptor;

通过debug我们发现handler其实包含了控制器的类路径已经具体的方法。

    protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
        //检测方法是否支持
        this.checkRequest(request);
        ModelAndView mav;
        //这里是否让session加锁
        if (this.synchronizeOnSession) {
        //通过jsessionId获取session
            HttpSession session = request.getSession(false);
            if (session != null) {
            //拿到自己的session,然后加锁。防止一个人高并发的发送接口。加在这里也有一个好处就是
            //如果一个用户大量发送接口也不会让请求进入,会在这里加锁。 
                Object mutex = WebUtils.getSessionMutex(session);
                synchronized(mutex) {
                //具体执行代码
                    mav = this.invokeHandlerMethod(request, response, handlerMethod);
                }
            } else {
            //如果没有session的话就 执行这个。
                mav = this.invokeHandlerMethod(request, response, handlerMethod);
            }
        } else {
        //如果添加了是异步的获取session的话就调这个 
            mav = this.invokeHandlerMethod(request, response, handlerMethod);
        }
  //看看是否有缓冲设置
        if (!response.containsHeader("Cache-Control")) {
        //设置缓存的过期时间这些
            if (this.getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
                this.applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
            } else {
                this.prepareResponse(response);
            }
        }
        return mav;
    }

在具体执行的时候,我们发现了线程管理器的踪迹。那么这个线程管理器到底干了什么?通过逐步跟踪发现并没有执行。可能我们目前对SpringMVC的使用还比较浅显吧。所以目前还没有发现。我们后期再看这个问题。

如图上图所示就是真正的控制器调用了。我乱了.....

不知道是IDEA反编译不对还是怎么回事,反正我debug了好久了。还是没看到invoke的那部分代码,貌似有一个select的部分,是不是选择需要执行的方法然后再invoke一下了的。算了,感觉好复杂。一不小心写了快3000字了。今天先到这里了,基本的调用流程已经基本已经清楚了。下次我们就整合项目的具体代码来看SpringBoot如何整合的。就像拦截器是外边套了一层。那么其他的静态资源拦截器、跨域什么的都是怎么整合的。晚安朋友们!