详解Spring MVC:下

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

10.1 HttpServletBean

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

10.2 FrameworkServlet

 前面讲过Servlet的处理过程:首先是从Servlet接口的service方法开始,然后在HttpServlet的service方法中根据请求的类型不同将请求路由到了doGet、doHead、doPost、doPut、doDelete、doOptions和doTrace七个方法,并且做了doHead、doOptions和doTrace的默认实现,其中doHead调用doGet,然后返回只有header没有body的response。
 在FrameworkServlet中重写了service、doGet、doPost、doPut、doDelete、doOptions、doTrace方法(除了doHead的所有处理请求的方法)。在service方法中增加了对PATCH类型请求对处理,其他类型的请求交给父类进行处理;doOptions和doTrace方法可以通过设置dispatchOptionsRequest和dispatchTraceRequest参数决定是自己处理还是交给父类处理(默认都是交给父亲类处理,doOptions会在父类的处理结果中增加PATCH类型);doGet、doPost、doPut和doDelete都是自己处理。所有需要自己处理对请求都交给了processRequest方法进行统一处理。
 下面来看一下service和doGet的代码,别对需要自己处理对方法都和doGet类似。

@SuppressWarnings("serial")
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
    ...
    /**
     * Override the parent class implementation in order to intercept PATCH requests.
     */
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
        if (HttpMethod.PATCH == httpMethod || httpMethod == null) {
            processRequest(request, response);
        }
        else {
            super.service(request, response);
        }
    }
    
    ...
    
    /**
     * Delegate GET requests to processRequest/doService.
     * <p>Will also be invoked by HttpServlet's default implementation of {@code doHead},
     * with a {@code NoBodyResponse} that just captures the content length.
     * @see #doService
     * @see #doHead
     */
    @Override
    protected final void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        processRequest(request, response);
    }
    
    ...
    
}

 我们发现这里所做的事情跟HttpServlet里将不同类型的请求路由到不通方法进行处理的思路正好相反,这里又将所有的请求合并到了processRquest方法。当然并不是说Spring MVC中就不对request的类型进行分类,而全部执行相同对操作了,恰恰相反,Spring MVC中对不用类型请求的支持非常好,不过它是通过另外一种方式进行处理的,它将不同类型的请求用不同的Handler进行处理,后面再详细分析。
 可能有的读者会想,直接覆盖了service不是就可以了吗?HttpServlet是在service方法中将请求路由到不同的方法,如果在service中不再调用super.service(),而是直接将请求交给processRequest处理不是更简单吗?从现在的结构来看确实如此,不过那么做其实存在着一些问题。比如,我们为了某种特殊需求需要在Post请求处理前对request做一些处理,这时可能会新建一个继承自DispactherServlet的类,然后覆盖doPost方法,在里面先对request做处理,然后再调用super.doPost(),但是按正常的逻辑,调用doPost应该可以完成才合理,而且一般情况下开发者并不需要对Spring MVC的结构非常了解,所以Spring MVC的这种做法虽然看起来有点笨拙但是必要对。
 下面就来看processRequest方法,processRequest是FrameworkServlet类在处理请求中最核心对方法。

@SuppressWarnings("serial")
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
    ...
    /**
     * Process this request, publishing an event regardless of the outcome.
     * <p>The actual event handling is performed by the abstract
     * {@link #doService} template method.
     */
    protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        long startTime = System.currentTimeMillis();
        Throwable failureCause = null;
        //获取LocaleContext中原来保存的LocaleContext
        LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
        //获取当前请求的LocaleContext
        LocaleContext localeContext = buildLocaleContext(request);
        //获取RequestContextHolder中原来保存的RequestAttributes
        RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
        //获取当前请求的ServletRequestAttributes
        ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
        //将当前请求的LocaleContext和ServletRequestAttributes设置到LocaleContextHolder和RequestContextHolder
        initContextHolders(request, localeContext, requestAttributes);

        try {
            //实际处理请求入口
            doService(request, response);
        }
        catch (ServletException ex) {
            failureCause = ex;
            throw ex;
        }
        catch (IOException ex) {
            failureCause = ex;
            throw ex;
        }
        catch (Throwable ex) {
            failureCause = ex;
            throw new NestedServletException("Request processing failed", ex);
        }

        finally {
            //恢复原来的LocaleContext和ServletAttributes到LocaleContextHolder和RequestContextHolder中
            resetContextHolders(request, previousLocaleContext, previousAttributes);
            if (requestAttributes != null) {
                requestAttributes.requestCompleted();
            }

            if (logger.isDebugEnabled()) {
                if (failureCause != null) {
                    this.logger.debug("Could not complete request", failureCause);
                }
                else {
                    if (asyncManager.isConcurrentHandlingStarted()) {
                        logger.debug("Leaving response open for concurrent processing");
                    }
                    else {
                        this.logger.debug("Successfully completed request");
                    }
                }
            }
            //发布ServletRequestHandlerEvent消息
            publishRequestHandledEvent(request, response, startTime, failureCause);
        }
    }
    
    ...
}

 processRequest方法中的核心语句是doService(request, response),这是一个模版方法,在DispatcherServlet中具体实现。在doService前后还做了一些事情(也就是大家熟悉到装饰模式):首先获取了LocaleContextHolder和RequestContextHolder中原来保存的LocaleContext和RequestAttributes并设置到了previousLocaleContext和previousAttributes临时属性,然后调用buildLocaleContext和buildReuqestAttributes方法获取到当前请求的LocaleContext和RequestAttributes,并通过initContextHolders方法将它们设置到LocaleContextHolder和RequestContextHolder中(处理完请求后再恢复到原来的值),接着使用request拿到异步处理管理器并设置了拦截器,做完这些后执行了doService方法,执行完后,最后(finally中)通过resetContextHolders方法将原来到previousLocaleContext和previousAttributes恢复到LocaleContextHolder和RequestContextHolder中,并调用publishReuqestHandledEvent方法发布了一个ServletRequestHandledEvent类型的消息。
 这里涉及了异步请求相关的内容,Spring MVC中异步请求的内容会在后面专门讲解。除了异步请求和调用doService方法具体处理请求,processRequest自己主要做了两件事情:1⃣️对LocaleContext和Requestttributes的设置及恢复;2⃣️处理完后发布了ServletRequestHandledEvent消息。
 首先来看一下LocaleContext和RequestAttributes。LocaleContext里面存放着Locale(本地化信息,如zh-cn等),RequestAttributes是spring的一个接口,通过它可以get/set/removeAttributes类,在ServletRequestAttributes里面还封装了request\response和session,而且都提供了get方法,可以直接获取。下面来看一下ServletRequestAttributes里setAttribute的代码(get/remove都大同小异)。

public class ServletRequestAttributes extends AbstractRequestAttributes {
    ...
    @Override
    public void setAttribute(String name, Object value, int scope) {
        if (scope == SCOPE_REQUEST) {
            if (!isRequestActive()) {
                throw new IllegalStateException(
                        "Cannot set request attribute - request is not active anymore!");
            }
            this.request.setAttribute(name, value);
        }
        else {
            HttpSession session = obtainSession();
            this.sessionAttributesToUpdate.remove(name);
            session.setAttribute(name, value);
        }
    }
    ...
}

 设置属性时可以通过scope判断是对request还是session进行设置,具体对设置方法非常简单,就是直接对request和session操作,sessionAttributesToUpdate属性后面讲到SessionAttributesHandler的时候再介绍,这里可以先不考虑它。需要注意的是isRequestActive方法,当调用了ServletRequestAttributes的requestCompleted方法后requestActive就会变称false,执行之前是true。这个很容易理解,request执行完了,当然也就不能再对它进行操作了!你可能已经注意到,在刚才的finally块中已调用requestAttributes的requestCompleted方法。
 现在大家对LocaleContext和RequestAttributes已经有了大概对了解,前者可以获取Locale,后者用于管理request和session的属性。不过可能还是有种没有理解透的感觉,因为还不知道它到底怎么用。不要着急,我们接下来看LocaleContextHolder和RequestContextHolder,把这两个理解了也就全明白了!
 先来看LocaleContextHolder,这是一个abstract类,不过里面对方法都是static的,可以直接调用,而且没有父类也没有子类!也就是说我们不能对它实例化,只能调用其定义的static方法。这种abstract的使用方式也值得我们学习。在LocaleContextHolder中定义了两个static的属性。

public abstract class LocaleContextHolder {

    private static final ThreadLocal<LocaleContext> localeContextHolder =
            new NamedThreadLocal<>("LocaleContext");

    private static final ThreadLocal<LocaleContext> inheritableLocaleContextHolder =
            new NamedInheritableThreadLocal<>("LocaleContext");
    ...
}

 这两个属性都是ThreadLocal<LocaleContext>类型的,LocaleContext前面已经介绍了,ThreadLocal大家应该也不陌生,很多地方都用了它。
 LocaleContextHolder类里面封装了两个属性localeContextHodler和inheritableLocaleContextHolder,它们都是LocaleContext,其中第二个可以被子线程继承。LocaleContextHolder还提供get/set方法,可以获取和设置LocaleContext,另外还提供了get/setLocale方法,可以直接操作Locale,当然都是static的。这个使用起来非常方便!比如,在程序中需要用到Locale的时候,首先想到的可能是request.getLocale(),这是最直接的方法。不过有时候在service层需要用到Locale的时候,再用这种方法就不方便了,因为正常来说service层是没有request的,这时可能就需要在controller层将Locale拿出来,然后再传进去了!当然这也没什么,传一下就好了,但最重要的是怎么传呢?服务层的代码可能已经通过测试了,如果想将Locale传进行可能就需要改接口,而修改接口可能会引起很多问题!而有getLocale()就可以了,它是静态方法,可以直接调用!当然,在Spring MVC中Locale的值并不总是request.getLocale()获取到的值,而是采用了非常灵活的机制,在后面的LocaleResolver中再详细讲解。
 RequestContextHolder也是一样的道理,里面封装了RequestAttributes,可以get/set/removeAttribute,而且因为实际封装的是ServletRequestAttributes,所以还可以getRequest、getResponse、getSession!这样就可以在任何地方都能方便地获取这些对象了!另外,因为里面封装的其实是对象都引用,所以即使在doService方法里面设置Attribute,使用RequestContextHoler也一样可以获取到。
 在方法最后的finally中调用resetContextHolders方法将原来的LocaleContext和RequestAttributes又恢复了。这是因为在Servlet外面可能还有别的操作,如Filter(Spring-MVC自己的HandlerInterceptor是在doService内部的)等,为了不影响那些操作,所以需要进行恢复。
 最后就是publishRequestHandledEvent(request, response, startTime, failureCause)发布消息了。在publishRequestHandledEvent内部发布了一个ServletRequestHandledEvent消息,代码如下:

@SuppressWarnings("serial")
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
    ...
    private void publishRequestHandledEvent(
            HttpServletRequest request, HttpServletResponse response, long startTime, Throwable failureCause) {
        //publishEvents可以在配置Servlet时设置,默认为true
        if (this.publishEvents) {
            //无论请求是否执行成功都发布消息
            // Whether or not we succeeded, publish an event.
            long processingTime = System.currentTimeMillis() - startTime;
            int statusCode = (responseGetStatusAvailable ? response.getStatus() : -1);
            this.webApplicationContext.publishEvent(
                    new ServletRequestHandledEvent(this,
                            request.getRequestURI(), request.getRemoteAddr(),
                            request.getMethod(), getServletConfig().getServletName(),
                            WebUtils.getSessionId(request), getUsernameForRequest(request),
                            processingTime, failureCause, statusCode));
        }
    }
}

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

/**
 * 记录日志的监听器
 *
 * @version
 * @author kyle 2018年9月15日上午11:08:53
 * @since 1.8
 */
@Component
public class ServletRequestHandledEventListener implements ApplicationListener<ServletRequestHandledEvent> {
    final static Logger logger = LoggerFactory.getLogger("RequestProcessLog");

    @Override
    public void onApplicationEvent(ServletRequestHandledEvent event) {
        logger.info(event.getDescription());
    }

}

 我们可以看到,只要简单地继承ApplicationListener,并且把自己要做的事情写到onApplicationEvent里面就行了。很简单吧!当然要把它注册到spring容器里才能起作用,如果开启了注释,只要在类上面标注@Component就可以了。
 到现在为止FrameworkServlet就分析完了,我们再简单地回顾一下:首先是在service方法里添加了对PATCH的处理,并将所有需要自己处理的请求都集中到了processRequest方法进行统一处理,这和HttpServlet里面根据request的类型将请求分配到各个不同的方法进行处理的过程正好相反。
 然后就是processRequest方法,在processRequest里面主要的处理逻辑交给了doService,这是一个模版方法,在子类中具体实现,另外就是对使用当前request获取到的LocaleContext和RequestAttributes进行了保存,以及处理完之后的恢复,在最后发布了ServletRequestHandledEvent事件。

10.3 DispatcherServlet

 DispatcherServlet是Spring MVC最核心的类,整个处理过程的顶层设计都在这里,所以我们一定要把这个类彻底弄明白。
 通过之前的分析我们知道,DispatcherServlet里面执行处理的入口方法应该是doService,不过doService并没有直接进行处理,而是交给了doDispatch进行具体的处理,在doDispacth处理前doService做了一些事情:首先判断是不是include请求,如果是则对request的Attribute做一个快照备份,等doDispacth处理完之后(如果不是异步调用且未完成)进行还原,在做完快照又对request设置了一些属性,代码如下:

@SuppressWarnings("serial")
public class DispatcherServlet extends FrameworkServlet {
    ...
    /**
     * Exposes the DispatcherServlet-specific request attributes and delegates to {@link #doDispatch}
     * for the actual dispatching.
     */
    @Override
    protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (logger.isDebugEnabled()) {
            String resumed = WebAsyncUtils.getAsyncManager(request).hasConcurrentResult() ? " resumed" : "";
            logger.debug("DispatcherServlet with name '" + getServletName() + "'" + resumed +
                    " processing " + request.getMethod() + " request for [" + getRequestUri(request) + "]");
        }

        // Keep a snapshot of the request attributes in case of an include,
        // to be able to restore the original attributes after the include.
        //当include请求时对request的Attribute做快照备份
        Map<String, Object> attributesSnapshot = null;
        if (WebUtils.isIncludeRequest(request)) {
            attributesSnapshot = new HashMap<String, Object>();
            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设置一些属性
        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());

        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.
                //还原request快照的属性
                if (attributesSnapshot != null) {
                    restoreAttributesAfterInclude(request, attributesSnapshot);
                }
            }
        }
    }
    ...
}

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

    ((FlashMap)((ServletRequestAttributes)(RequestContextHolder.getRequestAttributes())).getRequest().getAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE)).put("name","张三丰");

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

@RequestMapping(value = "/submit", method = RequestMethod.POST)
public String submit(RedirectAttributes attr) throws IOException {
    ((FlashMap) ((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getRequest()
            .getAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE)).put("name", "张三丰");
    attr.addFlashAttribute("ordersId", "xxx");
    attr.addAttribute("local", "zh-cn");
    return "redirect:showorders";
}

@RequestMapping(value = "/showorders", method = RequestMethod.GET)
public String showOrders(Model model) throws IOException {
    // doSomething...
    return "orders";
}

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

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

 从Request获取outputFlashMap除了直接获取DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE属性,还可以使用RequestContextUtils来操作:RequestContextUtils.getOutputFlashMap(request),这样也可以得到outputFlashMap,其实它内部还是从Request的属性获取的。
 当用户提交http://xxx/submit请求后浏览器地址栏会自动跳转到http://xxx/showorders?Local=zh-cn链接,而在showOrders的model里会存在["name","张三丰"]和["ordersId","xxx"]两个属性,而且对客户端是透明的,用户并不知道。
 这就是flashMap的用法,inputFlashMap用于保存上次请求中转发过来的属性,outputFlashMap用于保存本次请求需求转发的属性,FlashMapManager用于管理它们,后面会详细分析FlashMapManager。
 这就是flashMap的用法,inputFlashMap用于保存上次请求中转发过来的属性,outputFlashMap用于保存本次请求需要转发的属性,FlashMapManager用于管理它们,后面会详细分析FlashMapManager。
 doService就分析完了,在这里主要是对request设置了一些属性,如果是include请求还会对request当前的属性做快照备份,并在处理结束后恢复。最后将请求转发给doDispatch方法。
 doDispatch方法非常简单,从顶层设计了整个请求处理的过程。doDispatch中最核心的代码只要4句,它们的任务分别是:

  1. 根据request找到Handler;
  2. 根据Handler找到对应的HandlerAdapter;
  3. 用HadnlerAdapter处理Handler;
  4. 调用processDispatchResultResult方法处理上面处理之后的结果(包含找到View并渲染输出给用户)

 对应的代码如下:

// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

 这里需要解释三个概念:HandlerMapping、Handler和HandlerAdapter。这三个概念的准确理解对于Spring MVC的学习非常重要。如果对这三个概念理解得不够透彻,将会严重影响对Spring MVC的理解。下面给大家解释一下:
 ▪️HandlerMapping:是用来查找Handler的,在Spring MVC中会处理很多请求,没个请求都需要一个Handler来处理,具体接收到一个请求后使用哪个Handler来处理呢?这就是HandlerMapping要做的事情。
 ▪️HandlerAdapter:很多人对这个的理解都不准确,其实从名字上就可以看出它是一个Adapter,也就是适配器。因为Spring MVC中的Handler可以是任意的形式,只要能处理请求就OK,但是Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法(如doService方法)。怎么让固定的Servlet处理方法调用灵活的Handler来进行处理呢?这就是HandlerAdapter要做的事情。
 通俗点的解释就是Handler是用来干活的工具,HandlerMapping用于根据需要干的活找到相应的工具,HandlerAdapter是使用工具干活的人。比如,Handler就像车床、铣床、电火花之类的设备,HandlerMapping的作用是根据加工的需求选择用什么设备进行加工,而HandlerAdapter是具体操作设备的工人,不同的设备需要不同的工人去加工,车床需要车工,铣床需要铣工,如果让车工使用铣床干活就可能出问题,所以不同的Handler需要不同的HandlerAdapter去使用。我们都知道在干活的时候人是柔性最强、灵活度最高的,同时也是问题最多的、困难最多的。Spring MVC中也一样,在九大组件中HandlerAdapter也是最复杂的,所以在后面学习HandlerAdapter的时候要多留心。
 另外View和ViewResolver的原理与Handler和HandlerMapping的原理类似。View是用来展示数据的,而ViewResolver用来查找View。通俗地讲就是干完活后需要写报告的,写报告又需要模版(比如,是调查报告还是验收报告或者是下一步工作的请示等),View就是所需要的模版,模版就像公文里边的格式,内容就是Model里面的数据,ViewResolver就是用来选择使用哪个模版的。
 现在再回过头去看上面的四句代码应该就觉得很容易理解了,它们分别是:使用HandlerMapping找到干活Handler,找到使用Handler的HandlerAdapter,让HandlerAdapter使用Handler干活,干完活后将结果写个报告交上去(通过View展示给用户)。

10.4 doDispatch结构

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

@SuppressWarnings("serial")
public class DispatcherServlet extends FrameworkServlet {
    ...
    
    /**
     * Process the actual dispatching to the handler.
     * <p>The handler will be obtained by applying the servlet's HandlerMappings in order.
     * The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters
     * to find the first that supports the handler class.
     * <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers
     * themselves to decide which methods are acceptable.
     * @param request current HTTP request
     * @param response current HTTP response
     * @throws Exception in case of any kind of processing failure
     */
    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);
                
                //根据request找的handler  
                // Determine handler for the current request.
                mappedHandler = getHandler(processedRequest);
                if (mappedHandler == null || mappedHandler.getHandler() == null) {
                    noHandlerFound(processedRequest, response);
                    return;
                }
                
                //根据handler找到HandlerAdapter
                // Determine handler adapter for the current request.
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
                
                //处理GET、HEAD请求的Last-modified
                // 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 (logger.isDebugEnabled()) {
                        logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                    }
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }
                //之下相应Interceptor的preHandle
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }
                //HandlerAdapter使用Handler处理请求
                // Actually invoke the handler.
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                //如果需要异步处理,直接返回
                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }
                //当view为空的时(比如,Handler返回值为void),根据request设置默认view
                applyDefaultViewName(processedRequest, mv);
                //执行相应Interceptor的postHandle
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            }
            catch (Exception ex) {
                dispatchException = ex;
            }
            catch (Throwable err) {
                //spring 4.3版本开始,当遇到Throwable异常,使用@ExceptionHandler抛出异常  
                // 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);
            }
            //处理返回结果。包括处理异常、渲染页面、发出完成通知触发Interceptor的afterCompletion
            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);
                }
            }
        }
    }
    
    ...
}

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

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

 doDispatch中首先检查是不是上传请求,如果是上传请求,则将request转换为MultipartHttpServletRequest,并将multipartRequestParsed标志设置为true。其中使用到了MultipartResolver。
 然后通过getHandler方法获取Handler处理器链,其中使用到了HandlerMapping,返回值为HandlerExceptionChain类型,其中包含着与当前request相匹配的Interceptor和Handler。getHandler代码如下:

@SuppressWarnings("serial")
public class DispatcherServlet extends FrameworkServlet {
    ...

    /**
     * Return the HandlerExecutionChain for this request.
     * <p>Tries all handler mappings in order.
     * @param request current HTTP request
     * @return the HandlerExecutionChain, or {@code null} if no handler could be found
     */
    protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        for (HandlerMapping hm : this.handlerMappings) {
            if (logger.isTraceEnabled()) {
                logger.trace(
                        "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
            }
            HandlerExecutionChain handler = hm.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
        return null;
    }
    
    ...
}

 方法结构非常简单,HandlerMapping在后面详细讲解,HandlerExceptionChain的类型类似于前面Tomcat中讲过的Pipeline,Interceptor和Handler相当于哪里边的Value和BaseValue,执行时先依次执行Interceptor的preHandle方法,最后执行Handler,返回的时候按钮相反的顺序执行Interceptor的postHandle方法。就好像要去一个地方,Interceptor是要经过的收费站,Handler是目的地,去的时候和返回的时候都要经过加油站,但两次所经过的顺序是相反的。
 接下来是处理GET、HEAD请求的Last-Modifed。当浏览器第一次跟服务器请求资源(GET、HEAD请求)时,服务器在返回的请求头里面会包含一个Last-Modified的属性,代表本资源最后时什么时候修改的。在浏览器以后发送请求时会同时发送之前接受到的Last-Modified,服务器接收到带Last-Modified的请求后会用其值和自己实际资源带最后修改时间做对比,如果资源过期了则返回新的资源(同时返回新的Last-Modified),否则直接返回304状态码表示资源未过期,浏览器直接使用之前缓存的结果。
 接下来依次调用相应Interceptor的preHandle。
 处理完Interceptor的preHandler后就到了此方法最关键的地方---让HandlerAdapter使用Handler处理请求,Controller就是再这里执行的。这里主要使用了HandlerAdapter,具体内容在后面详细讲解。
 Handler处理完请求后,如果需要异步处理,则直接返回,如果不需要异步处理,当view为空时(如Handler返回值为void),设置默认view,然后执行相应Interceptor的postHandle。设置默认view的过程中使用到了ViewNameTranslator。
 到这里请求处理的内容就完成了,接下来使用processDispatchResult方法处理前面返回的结果,其中包括处理异常、渲染页面触发Interceptor的afterCompletion方法三部分内容。
 我们先来说一下doDispatch的异常处理结构。doDispatch有两层异常捕获,内层是捕获在对请求进行处理的过程中抛出的异常,外层主要是再处理渲染页面时抛出的。内层的一场,也就是执行请求处理时的异常会设置到dispatchException变量,然后在processDispatchResult方法中进行处理,外层则是处理processDispatchResult方法抛出的异常。processDispatchResult代码如下:

@SuppressWarnings("serial")
public class DispatcherServlet extends FrameworkServlet {
    ...

    /**
     * Handle the result of handler selection and handler invocation, which is
     * either a ModelAndView or an Exception to be resolved to a ModelAndView.
     */
    private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
            HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {

        boolean errorView = false;
        //如果请求处理的过程中有异常抛出则处理异常
        if (exception != null) {
            if (exception instanceof ModelAndViewDefiningException) {
                logger.debug("ModelAndViewDefiningException encountered", exception);
                mv = ((ModelAndViewDefiningException) exception).getModelAndView();
            }
            else {
                Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
                mv = processHandlerException(request, response, handler, exception);
                errorView = (mv != null);
            }
        }
        //渲染页面
        // Did the handler return a view to render?
        if (mv != null && !mv.wasCleared()) {
            render(mv, request, response);
            if (errorView) {
                WebUtils.clearErrorRequestAttributes(request);
            }
        }
        else {
            if (logger.isDebugEnabled()) {
                logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
                        "': assuming HandlerAdapter completed request handling");
            }
        }

        if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
            //如果启动了异步处理则直接返回
            // Concurrent handling started during a forward
            return;
        }
        
        //发出请求处理完到通知,触发Interceptor的afterCompletion
        if (mappedHandler != null) {
            mappedHandler.triggerAfterCompletion(request, response, null);
        }
    }

    ...
}

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

doDispatch方法处理流程图.png

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

10.5 小结

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

  • HttpServletBean:没有参与实际请求的处理。
  • FrameworkServlet:将不同类型的请求合并到了processRequest方法统一处理,processRequest方法中做了三件事:
    • 调用了doService模版方法具体处理请求。
    • 将当前请求的LocaleContext和ServletRequestAttributes在处理请求前设置到了LocaleContextHolder和RequestContextHolder,并在请求处理完成后恢复。
    • 请求处理完后发布了ServletRequestHandleEvent消息。
  • DispatcherServlet:doService方法给request设置了一些属性并将请求交给doDispatch方法具体处理。

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

结语

 终于写完了《详解Spring MVC 上下》两篇博文,一共一万字,希望对大家有所帮助,在面试的时候能够震慑住面试,顺利应聘理想的职务。

《详解Servlet》
《详解Spring MVC:上》

如果需要給我修改意见的发送邮箱:erghjmncq6643981@163.com
资料参考:《看透Spring MVC-源代码分析与实践》
转发博客,请注明,谢谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容