理解SpringMvc架构以及流程

本文是基于慕课网小马哥的 《Spring Boot 2.0深度实践之核心技术篇》的内容结合自己的需要和理解做的笔记。

理解 Spring Web MVC 架构

在解释Spring Web Mvc 的架构之前,首先我们要了解一下基于Servlet的基础架构之上构建的一种J2EE的设计模式--Front Controller(前端总控制器模式)

前端总控制器.png

通过流程图我们可以了解到:

  1. 当客户端发送请求到前端总控制器(Front Controller),在这里前端总控制器有两种实现《Servlet》和《JSP》。
  2. 前端总控制器通过委派发送给应用控制器(ApplicationController)。
  3. 应用控制器通过委派调用一个命令对象(Command),然后分发给对应的视图处理器(View)。

介绍完前端总控制器,接下来对于理解SpringMvc架构就很简单了。

mvc架构图.png

通过流程图我们可以了解到:

  1. 通过客户端发送的请求到前端总控制器(Front Controller) 也就是我们经常提到的DispatcherServlet。
  2. 前端总控制器委派给Controller,也就是我们常用的SpringMVC使用的控制层,也就是标注@Controller的业务模型。
  3. 然后通过DispatcherServlet将业务处理的结果委派给视图渲染模型。
  4. 最终返回视图对象给DispatcherServlet,响应结果。

SpringMvc核心组件以及交互流程

SpringMvc核心组件

  • 处理器管理
    • 映射:HandlerMapping
    • 适配:HandlerAdapter
    • 执行:HandlerExecutionChain
  • 渲染
    • 视图解析:ViewResolver
    • 国际化: LocaleResolver,LocaleContextResolver
    • 个性化:ThemeResolver
  • 异常处理
    • HandlerExceptionResolver

各个组件的介绍内容在Web on Servlet Stack中的 1.2.2. Special Bean Types章节中。

SpringMvc交互流程

交互流程.png

流程说明

  1. 用户发送请求至前端控制器 DispatcherServlet。
  2. DispatcherServlet 收到请求调用 HandlerMapping 处理器映射器。处理器映射器根据请求 url 找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给 DispatcherServlet。
  3. DispatcherServlet 通过 HandlerAdapter 处理器适配器调用处理器。
  4. HandlerAdapter 执行Controller层中的对应方法。
  5. Controller 执行完成返回 ModelAndView或ViewName。 HandlerAdapter 将 handler 执行结果 ModelAndView 返回给 DispatcherServlet。
  6. DispatcherServlet 将 ModelAndView 传给 ViewReslover 视图解析器。
  7. ViewReslover 解析后返回具体 View 对象。 DispatcherServlet 对 View 进行渲染视图(即将模型数据填充至视图中)。
  8. DispatcherServlet 响应用户。

源码理解交互流程

准备一个简单的SpringMVC项目

在Debugger源码之前,先让我们创建一个简单的spring项目,只需要一个简单的请求返回一个JSP页面就可以。

我创建的spring项目全部都使用了注解驱动(版本必须是Spring Framework 3.1 +,Servlet 3.0)。

在这里不阐述是如何实现自动装配Spring MVC项目的,后面会有单独的介绍,先简单给出代码让我们的程序跑起来,方便一步一步Debugger。

项目目录如下:

项目目录.png

pom.xml

<parent>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-parent</artifactId>
   <version>2.0.4.RELEASE</version>
   <relativePath/> <!-- lookup parent from repository -->
</parent>

这里需要注意一下 我使用的是IntelliJ IDEA 商业版,所以不需要内置tomcat插件。

WEB-INF/jsp/index.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<p>Hello World</p>

</body>
</html>

替换web.xml的配置类 --DefaultAnnotationConfigDispatcherServletInitializer

package com.web.servlet.support;

import com.web.configuration.DispatchServletConfiguration;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

/**
 * web.xml的配置
 */
public class DefaultAnnotationConfigDispatcherServletInitializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[0];
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {    //配置DispatcherServlet
        return new Class[]{DispatchServletConfiguration.class};
    }

    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

替换application-context.xml的配置类 --WebMvcConfig

package com.web.configuration;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.DispatcherServlet;

/**
 * {@link DispatcherServlet}
 *  扫描使用注解驱动的包  
 * 相当于 <context:component-scan base-package="com.web"/>
 */
@ComponentScan(basePackages = "com.web")  
public class DispatchServletConfiguration  {
}

以上的XML配置和java代码通过对应的注释已经一一对应解释了。

简单的Controller层 --HelloWorldController

package com.web.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * 简单controller
 */
@Controller
public class HelloWorldController {
    @RequestMapping("/index")
    public String index() {
        return "index";
    }
}

这里全部代码已经给出。下面让我们开始调试吧。让我们启动tomcat,访问http://localhost:8080/index

执行流程

流程的核心就是 org.springframework.web.servlet.DispatcherServlet这个类。

核心方法就是#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.
         // 确定当前请求的处理器也就是各种HandlerMapping
         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 (logger.isDebugEnabled()) {
               logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
            }
            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);
         }
      }
   }
}

根据上面的源码我们可以看到核心的过程

     // Determine handler for the current request.
     // 确定当前请求的处理器也就是各种HandlerMapping
     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());

现在让我们分步来debugger这几步,一点一点理解其流程。

1.org.springframework.web.servlet.DispatcherServlet#getHandler

gethandler.png

我们可以看到spring默认实现了 HandlerMapping的5个实现类,而第一个就是我们配置常用的 RequestMappingHandlerMapping

接着往下看,通过RequestMappingHandlerMapping 获取当前请求的处理程序执行链也就是 HandlerExecutionChain 包含当前处理程序对象和任何处理程序拦截器。

executionChain.png

我们可以清楚的看到,我们请求的URL路径直接映射到了public java.lang.String com.web.controller.HelloWorldController.index() 这个方法。

2.org.springframework.web.servlet.DispatcherServlet#getHandlerAdapter

gethandler.png

我们可以看到spring默认实现了 HandlerAdapter的3个实现类,而第一个就是我们配置常用的RequestMappingHandlerAdapter

然后往下Debugger

getHandlerAdapt2.png

这一步是判断当前的HandlerMethod是否是期望实现的处理程序。最后返回对应的适配处理器。

3.org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handle

handle.png

使用给定的适配处理程序来处理此请求,返回ModelAndView对象,这里viewName是 “index”,我们可以对应刚刚我们写的HelloWorldController#index()方法,确实方法返回的就是"index" 字符串。

4.org.springframework.web.servlet.DispatcherServlet#processDispatchResult

这个方法的目的就是 解析程序选择处理或者处理程序调用的结果无论是ModelAndView或者Exception。

render.png

最后通过render方法来渲染视图,我们在进入render方法中看一下就可以查找到我们想要的org.springframework.web.servlet.view.JstlView 和对应返回的资源URL。

view.png

至此,SpringMvc大致的执行流程已经介绍完了。我们通过Debugger源码再对应上面的交互流程图,就可以理解SpringMVC的架构和流程了。

DEMO地址

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

推荐阅读更多精彩内容