SpringMVC源码之DispatchServlet请求处理

  原帖地址:http://blog.csdn.net/roderick2015/article/details/52908594,转载请注明。

  在上篇帖子 SpringMVC源码之解读DispatcherServlet初始化流程 中介绍了DispatchServlet在为我们服务前做了哪些准备,今天接着看它是怎么处理Request请求的。

  首先回顾一下DispatchServlet的继承体系,在请求的处理过程中,涉及到了HttpServlet、FrameworkServlet和DispatchServlet,如下图所示。
这里写图片描述

HttpServlet

  我们编写Servlet处理Http请求时需要继承HttpServlet,因为在它的service方法中会把Web容器中获取到的HttpServletRequest和HttpServletResponse对象分发到doGet、doPost、doPut等七个方法中进行处理,我们只需覆写对应的方法即可,如覆写doGet方法来处理get类型的请求。

FrameworkServlet

  在FrameworkServlet中同样覆写了除doHead外的所有处理方法,但它还做了另外两件事:
  1.在覆写的service方法中添加了对patch类型请求的支持。
  2.所有类型的请求都交由processRequest方法统一处理。
  其中patch是对put类型的补充,侧重局部数据的更新,由于引入较晚在Java的Servlet中没有提供支持。

  这样做的意义是什么?个人认为HttpServlet中分成七个方法处理是为了给我们提供细粒度的扩展,可以单独对某一类型的方法进行扩展,而FrameworkServlet先对这几个方法覆写,再统一到processRequest中使用模板方式处理,是为了保持之前的扩展性不受影响,以及对用户的透明性(你该咋弄还是咋弄,方式不变)。

  接着看下processRequest的代码(只贴出部分流程代码,下同),如下所示。

    protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

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

        RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

        initContextHolders(request, localeContext, requestAttributes);

        /**
               * 上面第4~8行代码做了两件事:
               * 1.使用ThreadLocal获取当前线程的LocaleContext(本地化信息,如zh-CN)和
               * RequestAttributes(后面用来操作request和session的属性),保存起来等请求处理完毕后,
               * 在resetContextHolders方法中还原。
               * 2.创建当前单次请求的LocaleContext和RequestAttributes。
               * 
               * 第10行代码则是把刚创建的LocaleContext和RequestAttributes与当前线程绑定(ThreadLocal.set())
               * 
               * 这里面涉及到两个问题需要理解
               * 1.还原操作干嘛用的?
               * 首先ThreadLocal是跟线程绑定的,只要线程不销毁,数据就一直在。
               * 而线程从Tomcat的线程池中取出来,到干完活放回去的整个请求处理过程中,DispatchServlet只是其中的一环,
               * 期间可能会涉及到其他地方(比如常见的Filter)对这些信息的操作,所以这个信息对外部来说可能是有用的。
               * 粗暴点理解就是,我的地盘我做主,不影响别人的数据,别人也甭影响我,自己创建自己用,怎么来的还怎么走。
               * 
               * 2.为什么要线程绑定?
               * 因为Servlet在Tomcat中是单例的,所以是多个线程操作同一块代码,而想创建只对当前Request(处理线程)有效
               * 的数据,使用ThreadLocal是一个不错的选择。比如上面LocaleContextHolder的getLocaleContext方法是静态的,
               * 这就意味着你可以随时调用它来获取返回值,线程绑定后,你完全可以在Controller或者Service等处理逻辑代码
               * 的地方直接获取,给我们编码提供了便利性。
               */

        try {
            //抽象方法,由子类实现,典型的模板方法模式
            doService(request, response);
        }
        catch () {
        }
        finally {
            //还原操作,同时也起到了解除线程绑定的作用
            resetContextHolders(request, previousLocaleContext, previousAttributes);
        }
    }

DispatchServlet

  DispatchServlet中涉及的核心方法只有两个:doService和doDispatch,先看doService方法,代码如下所示。

    protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //下面就是向request中放入容器和组件,给后面的请求处理做准备。
        //可以看出SpringMVC的数据传递就是围绕request和response来设计的,包括之前的线程绑定
        //这也是为什么我们可以在逻辑层使用request和response做很多事情。
        request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
        request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
        request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
        //默认情况下getThemeSource()调用的是getWebApplicationContext()
        request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

        //这部分是给redirect重定向传参准备的,详见文末扩展部分。
        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 {

        }
    }

  接下来是doDispatch方法,在正式看代码前得先弄清下面几个概念:
  1.Handler
  处理器,request请求最终由它来处理,比如我们在Controller层中用@RequestMapping注释的方法就属于Handler,在SpringMVC中Handler是非常灵活的,它可以是方法也可以是类,甚至是其它的形式,因为它的类型是Object — 所有类的父类,就是说Handler可以是任何类型,比常用的接口抽象要更进一步。

  2.HandlerInterceptor
  Handler拦截器,它定义了三个方法:preHandle、postHandle和afterCompletion,也就是在执行Handler之前、之后及处理完成后分别调用这三个方法做统一的处理,比如SpringMVC会默认添加ConversionServiceExposingInterceptor,在执行Handler之前将request中的参数转换成我们在方法中定义的类型,类似于Spring中的AOP,这么好用的东西当然不会忘了用户啦,我们可以在配置文件中注册自己的拦截器。

  3.HandlerAdapter
  Handler适配器,刚刚提到Handler非常灵活,它可以是任意类型,但也不能无法无天啊,整个流程的结构可是固定的,那谁来约束它呢?这就是HandlerAdapter要做的事情,由HandlerAdapter调用Handler来处理request。把Handler比作工具的话,那HandlerAdapter就是使用它的人,工具再灵活再好用,咱用的不顺手那也白瞎。

  4.ModelAndView
  如果把Handler对应MVC模式中的C,那ModelAndView对应的就是M和V,它包含了数据和视图两部分。在HandlerAdapter调用Handler处理完请求后,返回的就是ModelAndView对象,这也意味着处理方法的返回值类型,可以是视图(如JSP页面名称)或者数据(如JSON数据),甚至视图和数据(创建ModelAndView对象)一起返回,为我们处理返回结果提供了很大的灵活性。

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

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

            try {
                //处理文件上传请求
                processedRequest = checkMultipart(request);
                multipartRequestParsed = (processedRequest != request);

                //找到处理当前request的Handler
                mappedHandler = getHandler(processedRequest);

                //再根据Handler找到HandlerAdapter
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

                String method = request.getMethod();
                boolean isGet = "GET".equals(method);
                if (isGet || "HEAD".equals(method)) {
                    //根据文件的最近修改时间判断是否返回304状态码,默认修改时间返回-1即不支持该缓存方式。
                    //如果想使用需要另外配置
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }

                //处理前先执行拦截器的preHandle方法,该方法会返回一个boolean值
                //如果是true,则交给后面的Interceptor继续处理,返回false则表示自己处理完了,
                //包括response也搞定了,后面的不用麻烦了直接做中断处理。
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                //HandlerAdapter调用Handler处理请求
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

                //判断是否需要viewNameTranslator从request里获取view的名字(针对在handler的返回值里没有给出的情况)
                applyDefaultViewName(request, mv);
                //处理完后执行拦截器的PostHandle方法
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            }
            catch (Exception ex) {
                dispatchException = ex;
            }
            //做最后的处理工作,比如页面渲染,调用拦截器的afterCompletion方法等
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        }
        catch (Exception ex) {
        }
        finally {
            //清理文件上传留下的临时文件
            if (multipartRequestParsed) {
                cleanupMultipart(processedRequest);
            }
        }
    }

  看完代码我们再梳理一下在doDispatch方法中做了哪些事情,而DispatchServlet中初始化的九大组件又是如何使用的,每个步骤对应的组件关系如下图所示。
SpringMVC源码之DispatchServlet请求处理_第1张图片

  另外还使用了HandlerExceptionResolver来处理流程中抛出的异常,而FlashMapManager在涉及到重定向处理时都有使用,这样一看是不是九大组件都到齐了。doDispatch方法中的代码并不多,而且步骤清晰容易理解,但它却包含了request处理的整个流程。
  我们知道九大组件都是接口,也就是说doDispatch方法中定义的是一套结构固定的抽象流程,而每个接口有多少实现类,之间又存在什么联系有什么功能都由具体的对象去处理,从DispatchServlet的初始化到请求的处理,设计思想都是一致的,提供丰富功能的同时也提供了灵活的扩展以及一个职责分明、步骤清晰的顶层脉络,这种设计方式非常值得我们学习。

  再进一步就是看这些组件具体是怎么工作的,我会在下面的帖子中对几个比较关键的组件进行分析。
  《SpringMVC源码之HandlerMapping与HandlerAdapter组件分析》(还没写。。。)
  《SpringMVC源码之ViewResolver组件分析》(还没写。。。)

总结

  首先请求会在HttpServlet中分发到七个doXXX()方法中处理,而FrameworkServlet覆写了这些方法并在processRequest中统一处理。processRequest方法执行了请求数据还原、线程绑定等操作,接着调用DispatchServlet的doService方法,添加了webContext、Resolver以及FlashMapManager以备后面使用,最后调用了doDispatch方法。doDispatch定义了处理请求的抽象流程,调用九大组件执行具体的操作。

扩展

  在DispatchServlet类的doService方法中涉及到了重定向参数的处理,既然碰到了咱就来了解FlashMapManager组件是如何处理重定向的。
  先看下FlashMapManager长什么样,代码如下所示。

public interface FlashMapManager {
    //这两个方法都是围绕FlashMap来操作的,FlashMap就是一个HashMap,用来存储重定向参数的。

    //从session中取出参数并放入FlashMap中
    FlashMap retrieveAndUpdate(HttpServletRequest request, HttpServletResponse response);

    //将FlashMap中的参数放入session中
    void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest request, HttpServletResponse response);
}

  也就是说FlashMapManager只是定义了两个保存和取出重定向参数的方法,并通过session作为中介存储。流程就是先把参数以固定的key存入session,然后把这个session的id作为cookie返回给浏览器, 浏览器在下次跳转请求的时候把id还给你,FlashMapManager再通过这个固定的key从session中取出参数,这些很好理解,下面我们来更进一步了解里面的操作。

  回到DispatcherServlet的doService方法,redirect重定向代码部分如下所示。

        //所以这句代码就是取出重定向参数,而且在取出后会将该参数从session中移除,
        //取参的固定key是FLASH_MAPS_SESSION_ATTRIBUTE
        FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
        if (inputFlashMap != null) {
            //以INPUT_FLASH_MAP_ATTRIBUTE为key作为request的属性传递,最后会被放入Model中供我们使用
            request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
        }
        //这里的key与上句代码正好相反。
        request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
        request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);

  下面我们在Controller类中加上下面两个方法,实现跳转功能:让请求从welcome方法跳转到newPage方法中处理。

    @RequestMapping(value={"/"}, method = RequestMethod.GET)
    public String welcome(RedirectAttributes redirectAttributes, HttpServletRequest request, HttpServletResponse response) {
        //添加跳转参数,传递到newPage方法中接收
        redirectAttributes.addAttribute("title", "july");
        redirectAttributes.addFlashAttribute("msg", "跳转页面");
        return "redirect:newPage";
    }

    @RequestMapping(value={"/newPage"}, method = RequestMethod.GET)
    public String newPage(String title, Model model, HttpServletRequest request) {
        Map map = model.asMap();
        return "newPage";
    }

  在welcome方法中使用了两种方式来传递参数,其中addAttribute添加的title会拼接到URL中传递,而addFlashAttribute添加的msg才会放入session中作为重定向参数传递,那具体是怎么实现的呢?

  首先看看RedirectAttributes接口的继承体系,如下图所示。
SpringMVC源码之DispatchServlet请求处理_第2张图片

  其中Model接口定义了对Attribute的操作方法,而实现类RedirectAttributesModelMap则继承了LinkedHashMap,这样就好理解了,addAttribute和addFlashAttribute不过是使用HashMap添加了两个键值对,但是它俩添加的值可没有放在一个地方。如下图所示,在类中定了一个私有对象flashAttributes,使用addFlashAttribute方法添加的值放在了这个Map中,只有接口RedirectAttributes定义的getFlashAttributes方法才能获取这个map的值。
SpringMVC源码之DispatchServlet请求处理_第3张图片

  之后title作为普通参数放入ModelAndView传递,并在RedirectView类的createTargetUrl方法中进行拼接,由于需要在URL中拼接,所以传入的value必须能转换为String类型,否则将抛出异常,部分代码截图如下所示。
SpringMVC源码之DispatchServlet请求处理_第4张图片

  msg则放入了request的属性中(key值为OUTPUT_FLASH_MAP_ATTRIBUTE),在AbstractFlashMapManager类的saveOutputFlashMap方法中以List< FlashMap >的格式放进了Session中(key值为FLASH_MAPS_SESSION_ATTRIBUTE),部分代码截图如下所示。
SpringMVC源码之DispatchServlet请求处理_第5张图片

  最后在sendRedirect方法中向浏览器返回响应,回过头看下过程中涉及的key值,就能对上了。

你可能感兴趣的:(源码分析)