IDEA 新建 Spring MVC 工程項(xiàng)目與 SpringMVC 運(yùn)行流程

前文

剛剛?cè)肼殻?xiàng)目大范圍的使用到了 Spring + SpringMVC + MyBatis 框架,對于一個(gè) Java 小白直接上手理解 Spring 還是十分困難的,而且只看書,不進(jìn)入代碼層面,理解并記憶 Spring 的宏大框架是在太困難了,所以用了很長時(shí)間寫了一篇破天荒長度的博客……
本篇文章時(shí)筆者這輩子寫的最累的一篇博客…… 晚上加班到十點(diǎn)回來后開始寫作,整整寫了兩個(gè)多星期加上一個(gè)端午節(jié),期間差點(diǎn)把房子都買了…… 最后終于在端午節(jié)晚上把文章寫了出來。感謝這段時(shí)間里同組同事們,明哥芳姐龍哥磊哥的工作中的幫助 ~
這篇文章寫下來,最大的感觸是:對于初學(xué) Spring 的人來說,理解其架構(gòu)實(shí)現(xiàn)真的很難,包括筆者寫了這么長的文章,中間也有很多內(nèi)容并沒有完全理解。對于像筆者一樣沒有使用經(jīng)驗(yàn)的開發(fā)者來說,一定要在一個(gè) SpringMVC 的工程之上使用單步調(diào)試的方法,逐步深入理解 Spring 的實(shí)現(xiàn),才能在腦海中構(gòu)建出基本的 Spring 框架。

本文通過 IDEA 安裝 Spring MVC 項(xiàng)目。首先在 IDEA 官網(wǎng)下載適合自己電腦配置版本的 Idea,然后進(jìn)行安裝,安裝過程省略。

一. IDEA 新建 Spring MVC 工程項(xiàng)目

1.1 新建工程

安裝 IDEA 成功后,選擇 File -> New -> Project,左邊欄中選擇 Maven,選擇 Create From archetype,然后選中 org.apache.maven.archetypes:maven-archetype-webapp,然后點(diǎn)擊下一步,如下圖 1.1 所示:

圖 1.1 New Project

然后填寫 GroupId 和 ArtifactId。GroupId 一般分為多個(gè)段,這里我只說兩段,第一段為域,第二段為公司名稱。域又分為 org, com, cn 等等許多,其中 org 為非營利組織,com 為商業(yè)組織。比如我創(chuàng)建一個(gè)項(xiàng)目,我一般會(huì)將 GroupId 設(shè)置 為 com.grq,com 表示域?yàn)楣?,grq 是我個(gè)人姓名縮寫,artifactId 設(shè)置為 MySpringMVC,表示你這個(gè)項(xiàng)目的名稱是 mySpringMVC,依照這個(gè)設(shè)置,你的包結(jié)構(gòu)最好是 com.grq.mySpringMVC 打頭的,如果有個(gè)StudentDao,它的全路徑就是 com.grq.mySpringMVC.StudentDao。

設(shè)置 GroupId 和 ArtifactId 的截圖如下所示:

圖 1.2 GroupId & ArtifactId

工程項(xiàng)目構(gòu)建完畢后,左側(cè)的 Project 欄顯示如下圖 1.3 所示:

圖 1.3 新建工程后的 Project 視圖

1.2 Maven 設(shè)置

接下來需要在 Idea 中通過設(shè)置 Maven 從網(wǎng)絡(luò)引入 Spring 與 SpringMVC 的依賴項(xiàng)。
筆者用的是 Mac OX 下的 Idea,該版本下打開設(shè)置的方法是 IntelliJ IDEA -> Preferences(Win7 版:File -> Setting)。打開設(shè)置面板后,在搜索框中輸入 "maven",就可以進(jìn)行 Maven 的設(shè)置如下圖 1.4 所示:

圖 1.4 Maven 設(shè)置

選擇如下設(shè)置,點(diǎn)擊 OK。

之后就可以通過連接網(wǎng)絡(luò)上的 Maven 庫,下載所需的依賴庫了。

注:
如果有本地 Maven 庫的話,可以設(shè)置圖 1.4 的 User settings file, Local repository,這樣就可以實(shí)現(xiàn)本地依賴庫的導(dǎo)入)

1.3 Maven 依賴庫內(nèi)容的填寫

第一步中的工程構(gòu)建完畢后,Project 列表中有一個(gè) pom.xml。其中 pom 是項(xiàng)目對象模型 (Project Object Model) 的簡稱,它是 Maven 項(xiàng)目中的文件,使用 xml 表示。它的作用類似 ant 的 build.xml 文件,功能更強(qiáng)大。該文件用于管理源代碼、配置文件、開發(fā)者的信息和角色、問題追蹤系統(tǒng)、組織信息、項(xiàng)目授權(quán)、項(xiàng)目的 url 、項(xiàng)目的依賴關(guān)系等等。事實(shí)上,在 Maven 世界中,project 可以什么都沒有,甚至沒有代碼,但是必須包含 pom.xml 文件。

在 pom.xml 文件中填入 Spring 與 SpringMVC 的依賴庫,最后 pom.xml 文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.grq.mySpringMVC</groupId>
<artifactId>mySpringMVC</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>4.3.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>4.3.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>4.3.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.3.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>4.3.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>4.3.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-expression</artifactId>
        <version>4.3.4.RELEASE</version>
    </dependency>
</dependencies>

</project>

粘貼結(jié)束后,按照順序 View -> Tool Windows -> Maven Project 打開 Maven 的管理頁,并在管理頁中點(diǎn)擊如下圖 1.5 的 Reimport Maven All Projects 按鈕,即可將 pom.xml 中的依賴項(xiàng)下載并加載進(jìn)入項(xiàng)目中,加載成功后的 Project 視圖如下圖 1.6 所示。

圖 1.5 Maven Project 視圖下的導(dǎo)入 Maven 按鈕
圖 1.6 加載成功后的 Project 視圖

1.4 添加 Tomcat 依賴庫

1.4.1 Tomcat 作用

我們通常說到的 servlet 可以理解服務(wù)器端處理數(shù)據(jù)的 java 小程序,負(fù)責(zé)管理 servlet 就是 web 容器。它幫助我們管理著servlet等,使我們只需要將重心專注于業(yè)務(wù)邏輯。servlet 沒有 main 方法,那我們?nèi)绾螁?dòng)一個(gè) servlet,如何結(jié)束一個(gè) servlet,如何尋找一個(gè)servlet 等等,都受控于另一個(gè) java 應(yīng)用,這個(gè)應(yīng)用我們就稱之為 web 容器。或者可以理解成 servlet 只是一個(gè)規(guī)范,web 容器遵照這個(gè)規(guī)范,實(shí)現(xiàn)支持 servlet 規(guī)范的請求和應(yīng)答。

我們最常見的 Tomcat 就是一個(gè) Web 容器。如果 Web 服務(wù)器應(yīng)用得到一個(gè)指向某 servlet 的請求,此時(shí)服務(wù)器不是把 servlet 交給 servlet 本身,而是交給部署該 servlet 的容器。要有容器向 servlet 提供 http 請求和響應(yīng),而且要由容器調(diào)用 servlet 的方法,如 doPost 或者 doGet。

1.4.2 添加 Tomcat 依賴庫

點(diǎn)擊菜單 File -> Project Structure,彈出設(shè)置對話框。選中左側(cè)欄 Project Settings 的 Libraries,點(diǎn)擊上面的加號(hào)"+",選擇 "Java" 選項(xiàng),如圖 1.7 所示:

圖 1.7 Project Structure 添加 Libraries

彈出的 Choose Modules 窗口中選擇當(dāng)前的 Module(即筆者的 mySpringMvc),OK 確認(rèn)。然后可以將 Libraries 的名稱改一下,筆者將其命名為 TomcatLibs。改完確定后,在 Project 視圖下會(huì)添加 TomcatLibs 的條目。如圖 1.8 所示:

圖 1.8 Tomcat 依賴庫添加到 External Libraries 中

1.5 項(xiàng)目中添加 Web 工程

現(xiàn)在我們只有 Spring 的框架,但 Spring MVC 必需的 Web 工程框架還沒有搭建,所以需要向項(xiàng)目中添加 Web 工程。

點(diǎn)擊菜單 File -> Project Structure,選中左側(cè)欄 Project Settings 的 Facets,在頂部的 "+" 中選擇 Web,并在彈出的 Choose Modules 窗口中選擇當(dāng)前 Module(即筆者的 mySpringMvc),Ok 確認(rèn)。筆者把 Name 改為 TomcatServer,這樣的名字更加直觀。此外,將 Web Resource Directory 的 ".../web" 改為 ".../webapp",筆者這樣的操作是為了與 Tomcat 的 webapp 路徑名稱對應(yīng)。如下圖 1.9 所示:

圖 1.9 Project Structure -> Facets

此外在右下角有一個(gè) Create Artifact 的按鈕,點(diǎn)擊后進(jìn)入 Artifacts 標(biāo)簽欄中,將右邊區(qū)域的所有內(nèi)容添加到左區(qū)域,然后點(diǎn)擊 Ok 完成 Web 工程的添加。如圖 1.10 所示。

圖 1.10 Project Structure -> Artifacts

二. 填充 Spring MVC 內(nèi)容

2.1 View 的編寫

在 webapp/WEB-INF 路徑上右擊 -> New -> Directory,輸入名稱:static/pages,新建的文件夾用于存放靜態(tài)的 HTML 文件。
然后在 static/pages 路徑上右擊 -> New -> HTML File,隨意填寫名稱,作為我們即將使用的視圖 View(筆者這里寫的名稱是 index)。
新建完畢之后,筆者隨便填寫了一些內(nèi)容如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Spring MVC Test</title>
</head>
<body>
Welcome to my Spring MVC Testing!!!
</body>
</html>

2.2 Controller 的編寫

接下來我們需要寫一個(gè) Controller。在路徑 src/main/java 下建包:右擊 src/main/java -> New -> Package,填寫合適的包名。筆者這里填寫的包名是 com.grq.springMvcTrain.controller。
在新建的 controller 包下再新建一個(gè) java 文件,筆者將其命名為 MvcController.java。然后在 MvcController.java 中填寫內(nèi)容如下:

package com.grq.springMvcTrain.controller;

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

@Controller
public class MvcController {
    @RequestMapping("/")
    public String index() {
        return "index";
    }
}

2.3 添加父子容器的 xml 文件

接下來我們?yōu)楦缸尤萜魈砑?xml 文件。在這里,父容器指 Spring 容器,子容器指 SpringMVC 容器。

關(guān)于父子容器相關(guān)的內(nèi)容,可以參考《spring的啟動(dòng)過程——spring和springMVC父子容器的原理》
《Spring和SpringMVC父子容器關(guān)系初窺》。

xml 文件都添加到 src/resources 路徑下,可將父容器命名為 application-context.xml, 子容器命名為 application-context-mvc.xml。其中 application-context.xml 為業(yè)務(wù)層 Spring 容器,application-context-mvc.xml 為 Web 容器。

2.3.1 application-context.xml

在 src/resources 路徑下右擊 -> New -> XML Configuration File -> Spring Config,命名為 application-context.xml。并填寫內(nèi)容如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd 
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 自動(dòng)掃描該包下的 Bean 并裝載 -->
    <context:component-scan base-package="com.grq.springMvcTrain"/>
</beans>

2.3.2 application-context-mvc.xml

在 src/resources 路徑下右擊 -> New -> XML Configuration File -> Spring Config,命名為 application-context-mvc.xml。填寫內(nèi)容如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">


    <mvc:annotation-driven/>
    <mvc:default-servlet-handler/>
    <context:component-scan base-package="com.grq.springMvcTrain.controller"/>
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/static/pages/"/>
        <property name="suffix" value=".html"/>
        <property name="order" value="1"/>
    </bean>
</beans>

<mvc:annotation-driven>:

上面代碼中,<mvc:annotation-driven> 會(huì)自動(dòng)注冊 RequestMappingHandlerMapping 與 RequestMappingHandlerAdapter 兩個(gè) Bean,這兩個(gè)是 Spring MVC 為 @Controller 分發(fā)請求所必需的,并且提供了數(shù)據(jù)綁定支持。

<mvc:default-servlet-handler />:

在 application-context-mvc.xml 中配置 <mvc:default-servlet-handler />后,會(huì)在Spring MVC上下文中定義一個(gè) org.springframework.web.servlet.resource 包下的 DefaultServletHttpRequestHandler,它的作用類似于一個(gè)檢查員,對進(jìn)入 DispatcherServlet 的 URL 進(jìn)行篩查。如果發(fā)現(xiàn)是靜態(tài)資源的請求,就將該請求轉(zhuǎn)由 Web 應(yīng)用服務(wù)器默認(rèn)的 Servlet 處理;如果不是靜態(tài)資源的請求,才由 DispatcherServlet 繼續(xù)處理。

一般 Web 應(yīng)用服務(wù)器默認(rèn)的 Servlet 名稱是 "default",因此DefaultServletHttpRequestHandler 可以找到它。如果你所有的 Web 應(yīng)用服務(wù)器的默認(rèn) Servlet 名稱不是 "default",則需要通過 default-servlet-name 屬性顯示指定為:

<mvc:default-servlet-handler default-servlet-name="所使用的Web服務(wù)器默認(rèn)使用的Servlet名稱" />

suffix, prefix:

對于視圖解析器 InternalResourceViewResolver,suffix, prefix 是很重要的屬性,它是邏輯視圖名的前綴與后綴。例如:

  • 有 URL 地址:/WEB-INF/static/pages/index.html
    • 邏輯視圖名:index
    • 前綴:/WEB-INF/static/pages/
    • 后綴:.html

4. web.xml 填寫

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!--業(yè)務(wù)層與模型層的 Spring 配置文件,配置文件被父容器使用-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:application-context.xml</param-value>
    </context-param>

    <!--聲明 Servlet-->
    <servlet>
        <servlet-name>mySpringMvcServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--對 DispatcherServlet 進(jìn)行配置-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath*:application-context-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <!--DispatcherServlet 的 URL 模式-->
    <servlet-mapping>
        <servlet-name>mySpringMvcServlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
    
    <listener>
        <listener-class>org.springframework.web.context.ContextCleanupListener</listener-class>
    </listener>
</web-app>

web.xml 文件是用來初始化配置信息:比如 Welcome 頁面, servlet, servlet-mapping, filter, listener, 啟動(dòng)加載級別等。對于一個(gè) Web 項(xiàng)目,是可以沒有 web.xml 文件的。當(dāng)你的 Web 工程沒用到這些時(shí),你可以不用 web.xml 文件來配置你的 Application。也就是說,web.xml 文件并不是 Web 工程必須的
web.xml 的模式文件中定義的標(biāo)簽并不是定死的,模式文件也是可以改變的。一般來說,隨著 web.xml 模式文件的版本升級,里面定義的功能會(huì)越來越復(fù)雜,標(biāo)簽元素的種類肯定也會(huì)越來越多,但有些不是很常用的,我們只需記住一些常用的并知道怎么配置就可以了。

web.xml 相關(guān)內(nèi)容參考:《springmvc配置文件web.xml詳解各方總結(jié)。》

5. 配置 Tomcat

配置 Tomcat 要通過 Edit Configuration 進(jìn)行。選項(xiàng)的外形如下圖 2.1 所示:

圖 2.1 Edit Configuration 按鈕

進(jìn)入 Edit Configuration 后,點(diǎn)擊 "+",選擇 Tomcat Server -> Local。在命名框中隨意命名,筆者此處命名為 TomcatServer。
然后再 Deployment 標(biāo)簽頁點(diǎn)擊 "+",選擇添加 Artifact,將之前的 Web 工程加入,選擇 OK。

圖 2.2 Tomcat Server 配置

配置 Tomcat ,將路徑配置正確。如下圖 2.3 所示:

圖 2.3 配置 Tomcat 路徑

三. Spring MVC 容器初始化

參考網(wǎng)址:
《springMVC的容器初始化過程》
《Spring之SpringMVC(源碼)啟動(dòng)初始化過程分析》
《第二章 Spring MVC入門 —— 跟開濤學(xué)SpringMVC》

下面通過單步調(diào)試的方法,詳細(xì)解釋 SpringMVC 的初始化運(yùn)行步驟。

Spring MVC 的核心在于 DispatcherServlet,觀察 DispatcherServlet 的繼承結(jié)構(gòu)如下圖 3.1 所示:

圖 3.1 DispatcherServlet 繼承結(jié)構(gòu)

可以從圖 3.1 看到,DispatcherServlet 依次繼承了 GenericServlet, HttpServlet, HttpServletBean, FrameworkServlet。由于 DispatcherServlet 是繼承了 HttpServlet,所以它的初始化入口應(yīng)該是 HttpServlet 的 init() 方法。

1. HttpServlet.init()

Web 容器啟動(dòng)時(shí)將調(diào)用它的 init 方法。源碼如下:

public final void init() throws ServletException {
    if (logger.isDebugEnabled()) {
        logger.debug("Initializing servlet '" + getServletName() + "'");
    }
 
    // 從初始化參數(shù)中設(shè)置 Bean 屬性,讀取 web.xml 文件獲取 DispatcherServlet 基本信息
    try {
        PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
        BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
        ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
        bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
        initBeanWrapper(bw);
        bw.setPropertyValues(pvs, true);
    }
    catch (BeansException ex) {
        logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
        throw ex;
    }
 
    // 子類調(diào)用初始化
    initServletBean();
 
    if (logger.isDebugEnabled()) {
        logger.debug("Servlet '" + getServletName() + "' configured successfully");
    }
}

可以注意到,init() 方法是 final,不能夠被覆蓋,它位于 HttpServletBean 中。它完成的功能有兩個(gè),第一個(gè)將 Servlet 初始化參數(shù)設(shè)置到該 Servlet 中。在該部分中,可以讀取 web.xml 文件中的 DispatcherServlet 的相關(guān)信息,其中初始化內(nèi)容包括上下文信息所在路徑,即 web.xml 中的 classpath:*/application-context-mvc.xml。
第二個(gè)調(diào)用子類的初始化。完成該步驟的是 HttpServletBean 中的 initServletBean() 方法。

2. HttpServletBean.initServletBean()

HttpServletBean 是 FrameworkServlet 的子類。接下來就關(guān)注一下調(diào)用子類初始化的 FrameworkServlet 的 initServletBean() 方法:

protected final void initServletBean() throws ServletException {
    getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
    if (this.logger.isInfoEnabled()) {
        this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
    }
    long startTime = System.currentTimeMillis();
 
    try {
        // 內(nèi)容 1: 完成了 Web 上下文的初始化工作;
        // ContextLoaderListener 加載了上下文將作為根上下文(DispatcherServlet 的父容器)
        this.webApplicationContext = initWebApplicationContext();
        // 內(nèi)容 2: 提供給子類進(jìn)行初始化的擴(kuò)展點(diǎn)。行容器的一些初始化,這個(gè)方法由子類實(shí)現(xiàn),來進(jìn)行擴(kuò)展;
        initFrameworkServlet();
    }
    catch (ServletException ex) {
        this.logger.error("Context initialization failed", ex);
        throw ex;
    }
    catch (RuntimeException ex) {
        this.logger.error("Context initialization failed", ex);
        throw ex;
    }
 
    if (this.logger.isInfoEnabled()) {
        long elapsedTime = System.currentTimeMillis() - startTime;
        this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
                elapsedTime + " ms");
    }
}

在 try 代碼塊中的兩行代碼即為 initServletBean() 方法的主要內(nèi)容,分為兩個(gè)功能:一是完成 Web 上下文的初始化工作,這是主要內(nèi)容。二是提供給子類,進(jìn)行初始化
再接下來,需要進(jìn)入 HttpServletBean 的 initWebApplicationContext()。

3. HttpServletBean.initWebApplicationContext()

initWebApplicationContext() 方法的主要作用,就是從 web.xml 中讀取關(guān)于 Web 容器上下文的相關(guān)信息。該部分主要代碼如下:

protected WebApplicationContext initWebApplicationContext() {
    WebApplicationContext rootContext =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;
 
    if (this.webApplicationContext != null) {
        // 步驟 1. 在創(chuàng)建的時(shí)候注入根上下文
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            if (!cwac.isActive()) {
                // The context has not yet been refreshed -> provide services such as
                // setting the parent context, setting the application context id, etc
                if (cwac.getParent() == null) {
                    // The context instance was injected without an explicit parent -> set
                    // the root application context (if any; may be null) as the parent
                    cwac.setParent(rootContext);
                }
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    // 步驟 2. 如果經(jīng)過步驟 1,沒有注入上下文,則尋找上下文
    if (wac == null) {
        wac = findWebApplicationContext();
    }
    // 步驟 3. 如果沒有找到相應(yīng)的上下文,則手動(dòng)創(chuàng)建一個(gè),并指定父親為其 ContextLoaderListner
    if (wac == null) {
        wac = createWebApplicationContext(rootContext);
    }
 
    // 步驟 4. 刷新上下文
    if (!this.refreshEventReceived) {
        // Either the context is not a ConfigurableApplicationContext with refresh
        // support or the context injected at construction time had already been
        // refreshed -> trigger initial onRefresh manually here.
        onRefresh(wac);
    }
 
    if (this.publishContext) {
        // Publish the context as a servlet context attribute.
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
                    "' as ServletContext attribute with name [" + attrName + "]");
        }
    }
 
    return wac;
}

該過程中,經(jīng)歷了四個(gè)步驟:

  1. 在創(chuàng)建時(shí)注入根上下文;
  2. 如果此時(shí)沒有注入上下文,則開始尋找上下文;
  3. 如果此時(shí)沒有注入上下文,則手動(dòng)創(chuàng)建一個(gè),并指定其父親為其 ContextLoaderListenr;
  4. 刷新上下文;

四個(gè)步驟中,最重要的是手動(dòng)創(chuàng)建上下文部分。

4. FrameworkServlet.createWebApplicationContext

該部分可以手動(dòng)創(chuàng)建一個(gè)上下文。主要源碼如下:

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        if (this.contextId != null) {
            wac.setId(this.contextId);
        } else {
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(this.getServletContext().getContextPath()) + '/' + this.getServletName());
        }
    }

    // 設(shè)置上下文參數(shù)
    wac.setServletContext(this.getServletContext());
    wac.setServletConfig(this.getServletConfig());
    wac.setNamespace(this.getNamespace());
    wac.addApplicationListener(new SourceFilteringListener(wac, new FrameworkServlet.ContextRefreshListener()));

    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        ((ConfigurableWebEnvironment)env).initPropertySources(this.getServletContext(), this.getServletConfig());
    }

    this.postProcessWebApplicationContext(wac);
    this.applyInitializers(wac);
    wac.refresh();
}

設(shè)置上下文參數(shù)的部分,獲取了所有 WebApplicationContext 相關(guān)的值,并將其注入了 WebApplicationContext 中。

5. DispatcherServlet.onRefresh()

最后調(diào)用 onRefresh() 方法。onRefresh() 方法是抽象基類 AbstractApplicationContext 的方法,實(shí)際運(yùn)行時(shí)被 DispatcherServlet 所覆蓋,它在內(nèi)部調(diào)用了 initStrategies() 方法,作用是刷新上下文。

protected void onRefresh(ApplicationContext context) {
    this.initStrategies(context);
}

6. DispatcherServlet.initStrategies

initStrategies 方法源碼如下:

protected void initStrategies(ApplicationContext context) {  
    initMultipartResolver(context);  
    initLocaleResolver(context);  
    initThemeResolver(context);  
    initHandlerMappings(context);  
    initHandlerAdapters(context);  
    initHandlerExceptionResolvers(context);  
    initRequestToViewNameTranslator(context);  
    initViewResolvers(context);  
}  

進(jìn)入 DispatcherServlet 的 initStrategies,此時(shí)所有的 bean 都已經(jīng)加載好了;程序運(yùn)行到這里,就已經(jīng)實(shí)現(xiàn)了 DispatcherServlet 初始化的工作。后面進(jìn)入 DispatcherServlet 處理用戶響應(yīng)的過程。

四. SpringMVC 響應(yīng) —— doDispatch 的運(yùn)行流程

參考網(wǎng)址:
運(yùn)行流程:
《第二章 Spring MVC入門 —— 跟開濤學(xué)SpringMVC》

攔截器相關(guān):《SpringMVC源碼總結(jié)(十一)mvc:interceptors攔截器介紹》
《第五章 處理器攔截器詳解——跟著開濤學(xué)SpringMVC 》

視圖渲染:《SpringMVC核心——視圖渲染(包含視圖解析)問題》

運(yùn)行到上一步,DispathcerServlet 已經(jīng)在 Web 容器中運(yùn)行,程序等待瀏覽器客戶端的響應(yīng)。前面設(shè)定的端口號(hào)為 8080,此時(shí)在任意一個(gè)瀏覽器輸入地址:
http://localhost:8080
此時(shí)就相當(dāng)于從客戶端向該工程的 Servlet 發(fā)送了一個(gè)請求 request,程序也就進(jìn)入了 Servlet 的 doService() 方法。觀察到 DispatcherServelt 的 doService() 方法部分源碼如下所示:

protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
    ......
    try {
        this.doDispatch(request, response);
    } finally {
        ......
    } 
}

由上面的源碼可以看出,DispatcherServlet 的核心是調(diào)用 doDispatch 方法。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 {
            // 步驟 1: 檢查是否為 multipart
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            // 步驟 2: 請求到處理器 (DispatcherServlet) 的映射,通過 HandlerMapping 進(jìn)行映射
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            // 步驟 3: 處理器適配
            HandlerAdapter ha = 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 (logger.isDebugEnabled()) {
                    logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                }
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                    return;
                }
            }

            // 步驟 4: 預(yù)處理
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }

            // 步驟 5: 由適配器執(zhí)行處理器
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }
            applyDefaultViewName(processedRequest, mv);
           
            // 步驟 6: 后處理
            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);
        }
        // 步驟 7: 解析、渲染 View
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    // 步驟 8: 完成后處理
    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);
            }
        }
    }
}

下面按照上述源碼中標(biāo)注的步驟,進(jìn)入 DispatcherServlet 的 doDispatch 方法并分析:

注:下面的步驟 3 至步驟 8 是一個(gè)核心運(yùn)行流程,并將該核心流程的總結(jié)放在最后。

1. 步驟 1: 檢查是否為 multipart

由于筆者是 SpringMVC 的新手,所以該步驟筆者并不能完全理解。主要作用是檢查 request 是否為多部分 request (checkMultipart),比如檢測是否要用于文件上傳。

2. 步驟 2: 請求到 DispatcherServlet 的映射

通過 HandleMapping 映射,請求到 DispatcherServlet 的映射。該部分源碼如下:

// 步驟 2
mappedHandler = getHandler(processedRequest, false);  
if (mappedHandler == null || mappedHandler.getHandler() == null) {  
    noHandlerFound(processedRequest, response);  
    return;  
}  

進(jìn)入 getHandler 方法源碼,可以觀察到,getHandler 內(nèi)部遍歷了 DispatcherServlet 的 HandlerMapping 集合,直到訪問到了 Handler,就將其作為 HandlerExecutionChain。
HandlerExecutionChain 即 Handler 執(zhí)行鏈,它包含一個(gè)處理器 (HandlerMethod),若干個(gè)攔截器 (HandlerInterceptor)。結(jié)構(gòu)如下圖 4.1 所示:

圖 4.1 HandlerExecutionChain

HandlerExecutionChain 的主要功能是通過若干 HandlerInterceptor 實(shí)現(xiàn)的。HandlerInterceptor 主要方法如下:

  • boolean preHandle(...):該方法是一個(gè)前置方法,請求到達(dá) Handler 之前,先執(zhí)行該前置處理方法。如果該方法返回 false,則請求直接返回;如果返回 true,才會(huì)傳遞給下一個(gè)處理節(jié)點(diǎn)。
  • void postHandle(...):在請求被 HandlerAdapter 執(zhí)行之后,執(zhí)行該后置處理方法。

如果遍歷過程中找到 Handler,則將當(dāng)前 handler 作為 HandlerExecutionChain 并返回到 DispatcherServlet,否則判斷退出。
在步驟 2 中,經(jīng)過了 handler = hm.getHandler(request) 語句后(此時(shí)的 hm 類型為 HandlerMapping 的子類 RequestMappingHandlerMapping),此時(shí) mapperdHandler = com.grq.example.controller.UserController.index()。

3. 步驟 3: 處理器適配

// 步驟 3: 處理器適配
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

該步驟中,核心方法是 getHandlerAdapter,它將我們的 Handler 包裝成相應(yīng)適配器 HandlerAdapter。該方法的代碼如下:

protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
    Iterator var2 = this.handlerAdapters.iterator();

    HandlerAdapter ha;
    do {
        if (!var2.hasNext()) {
            throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
        }

        ha = (HandlerAdapter)var2.next();
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Testing handler adapter [" + ha + "]");
        }
    } while(!ha.supports(handler));

    return ha;
}

由代碼段可知,getHandlerAdapter 方法遍歷眾多的 HandlerApapter,并分別調(diào)用它們的 support(handler) 方法,直到 support 方法返回值為 true 為止(此時(shí) HandlerAdapter 的類型為 RequestMappingHandlerAdapter)

4. 步驟 4: 預(yù)處理

// 預(yù)處理
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    return;
}

該部分只有一個(gè) applyPreHandle 方法,即為 HandlerExecutionChain 執(zhí)行鏈中若干攔截器的作用部分。代碼如下所示:

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HandlerInterceptor[] interceptors = this.getInterceptors();
    if (!ObjectUtils.isEmpty(interceptors)) {
        for(int i = 0; i < interceptors.length; this.interceptorIndex = i++) {
            HandlerInterceptor interceptor = interceptors[i];
            if (!interceptor.preHandle(request, response, this.handler)) {
                this.triggerAfterCompletion(request, response, (Exception)null);
                return false;
            }
        }
    }
    return true;
}

若干攔截器使用 preHandle 方法層層攔截,若存在某個(gè)攔截器的 preHandle 返回 false,則該方法返回 false;此外只要有一個(gè)攔截器返回 false,相應(yīng)的 doDispatch 方法也將返回結(jié)束。

5. 步驟 5: 由適配器執(zhí)行處理器

// 步驟 5: 由適配器執(zhí)行處理器
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

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

該步驟中,核心方法是 handle 方法,它是 HandlerAdapter 的接口方法,具體需要按照不同的 HandlerAdapter 類型進(jìn)行實(shí)現(xiàn)。
此前提到運(yùn)行到這里,此時(shí)的 HandlerAdapter 類型是 RequestMappingHandlerAdapter。在 RequestMappingHandlerAdapter 中進(jìn)行一些步驟的跳轉(zhuǎn),會(huì)調(diào)用 AbstractHandlerMethodAdapter 抽象類的 handleInternal 方法,并用其子類的具體實(shí)現(xiàn)。
RequestMappingHandlerAdapter 的 handleInternal 方法代碼如下:

protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
    this.checkRequest(request);
    ModelAndView mav;
    if (this.synchronizeOnSession) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            Object mutex = WebUtils.getSessionMutex(session);
            synchronized(mutex) {
                mav = this.invokeHandlerMethod(request, response, handlerMethod);
            }
        } else {
            mav = this.invokeHandlerMethod(request, response, handlerMethod);
        }
    } else {
        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;
}

可以看出,該方法的核心語句是 mav = this.invokeHandlerMethod(request, response, handlerMethod),它的輸入?yún)?shù) handlerMethod 中包含先前獲取到的方法相關(guān)信息(包括方法名 method 與參數(shù) parameters),在該方法中調(diào)用 invokeHandlerMethod,返回值即為 ModelAndView 類型的數(shù)據(jù)。

6. 步驟 6: 后處理

mappedHandler.applyPostHandle(processedRequest, response, mv);

該部分只有一個(gè) applyPostHandle 方法,即為 HandlerExecutionChain 執(zhí)行鏈中若干攔截器的作用部分。代碼如下所示:

void applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) throws Exception {
    HandlerInterceptor[] interceptors = this.getInterceptors();
    if (!ObjectUtils.isEmpty(interceptors)) {
        for(int i = interceptors.length - 1; i >= 0; --i) {
            HandlerInterceptor interceptor = interceptors[i];
            interceptor.postHandle(request, response, this.handler, mv);
        }
    }
}

若干攔截器使用 preHandle 方法層層攔截,若存在某個(gè)攔截器的 postHandle 返回 false,則該方法返回 false;此外只要有一個(gè)攔截器返回 false,相應(yīng)的 doDispatch 方法也將返回結(jié)束。

7. 步驟 7: 解析、渲染 View

步驟 5 中獲得 ModelAndView 參數(shù)后,doDispatch 方法使用 processDispatchResult 方法解析 View。processDispatchResult 方法源碼如下:

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) {
            this.logger.debug("ModelAndViewDefiningException encountered", exception);
            mv = ((ModelAndViewDefiningException)exception).getModelAndView();
        } else {
            Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
            mv = this.processHandlerException(request, response, handler, exception);
            errorView = mv != null;
        }
    }

    // 渲染 View
    if (mv != null && !mv.wasCleared()) {
        // 核心源碼,解析 View
        this.render(mv, request, response);
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    } else if (this.logger.isDebugEnabled()) {
        this.logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + this.getServletName() + "': assuming HandlerAdapter completed request handling");
    }

    if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
        if (mappedHandler != null) {
            mappedHandler.triggerAfterCompletion(request, response, (Exception)null);
        }
    }
}

上面的源碼中,核心的源碼在于 render 方法,它用來解析、渲染 View。render 方法的源碼如下:

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    Locale locale = this.localeResolver.resolveLocale(request);
    response.setLocale(locale);

    // 解析 View
    View view;
    if (mv.isReference()) {
        // We need to resolve the view name.
        view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
        if (view == null) {
            throw new ServletException(
                    "Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" +
                            getServletName() + "'");
        }
    }
    else {
        // No need to lookup: the ModelAndView object contains the actual View object.
        view = mv.getView();
        if (view == null) {
            throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
                    "View object in servlet with name '" + getServletName() + "'");
        }
    }

    // Delegate to the View object for rendering.
    if (logger.isDebugEnabled()) {
        logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'");
    }
    
    // 渲染 View
    try {
        view.render(mv.getModelInternal(), request, response);
    }
    catch (Exception ex) {
        if (logger.isDebugEnabled()) {
            logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '"
                    + getServletName() + "'", ex);
        }
        throw ex;
    }
}

解析視圖:

解析視圖的功能由方法 resolveViewName 實(shí)現(xiàn)。具體解析的方法,是在容器中查找所有配置好的 List 類型的視圖解析器 (ViewResolver),然后進(jìn)行遍歷,只要存在一個(gè)視圖解析器,就能解析出視圖。調(diào)用該視圖解析器的方法對 View 進(jìn)行解析,最后返回該 View 值。方法源碼如下:

// org.springframework.web.servlet.DispatcherServlet # resolveViewName
protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale,
            HttpServletRequest request) throws Exception {

  for (ViewResolver viewResolver : this.viewResolvers) {
    View view = viewResolver.resolveViewName(viewName, locale);
    if (view != null) {
      return view;
    }
  }
  return null;
}

渲染視圖:

render 是一個(gè)接口方法,具體需要由實(shí)現(xiàn)了 View 接口的類具體實(shí)現(xiàn)。接口方法如下:

void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;

View 接口主要由 AbstractView 抽象類繼承,在 AbstractView 中的 render 方法實(shí)現(xiàn)如下:

// org.springframework.web.servlet.view.AbstractView # render
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
  if (logger.isTraceEnabled()) {
    logger.trace("Rendering view with name '" + this.beanName + "' with model " + model +
      " and static attributes " + this.staticAttributes);
  }

  Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);

  prepareResponse(request, response);
  // 核心源碼,進(jìn)行實(shí)時(shí)渲染
  renderMergedOutputModel(mergedModel, request, response);
}

render 方法為指定的模型指定視圖,如果有必要的話,在 createMergedOutputModel 方法中合并它靜態(tài)的屬性和 RequestContext 中的屬性,最后在核心源碼 renderMergedOutputModel() 中執(zhí)行實(shí)際的渲染。
renderMergedOutputModel 也是一個(gè)抽象方法,由具體的視圖解析器具體實(shí)現(xiàn)。以 InternalResourceView 的 renderMergedOutputModel() 方法為例,源碼如下:

@Override
protected void renderMergedOutputModel(
        Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

  // Determine which request handle to expose to the RequestDispatcher.
  HttpServletRequest requestToExpose = getRequestToExpose(request);

  // Expose the model object as request attributes.
  exposeModelAsRequestAttributes(model, requestToExpose);

   // Expose helpers as request attributes, if any.
  exposeHelpers(requestToExpose);

  // Determine the path for the request dispatcher.
  String dispatcherPath = prepareForRendering(requestToExpose, response);

  // Obtain a RequestDispatcher for the target resource (typically a JSP).
  RequestDispatcher rd = getRequestDispatcher(requestToExpose, dispatcherPath);
  if (rd == null) {
        throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
                "]: Check that the corresponding file exists within your web application archive!");
   }

  // If already included or response already committed, perform include, else forward.
  if (useInclude(requestToExpose, response)) {
       response.setContentType(getContentType());
       if (logger.isDebugEnabled()) {
           logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
       }
       rd.include(requestToExpose, response);
  }
  else {
       // Note: The forwarded resource is supposed to determine the content type itself.
       if (logger.isDebugEnabled()) {
           logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
       }
       rd.forward(requestToExpose, response);
   }
}

之前的步驟都是向該方法中的 RequestDispatcher 中填充數(shù)據(jù),獲取了 RequestDispatcher 后,最后通過 include 或 forward 方法轉(zhuǎn)發(fā),正常狀況下,運(yùn)行到這里我們就會(huì)在瀏覽器上看到了頁面內(nèi)容,如下圖 4.2 所示。

圖 4.2 訪問頁面

關(guān)于 include 與 forward 的區(qū)別,可參考:《SpringMVC——使用RequestDispatcher.include()和HttpServletResponseWrapper動(dòng)態(tài)獲取jsp輸出內(nèi)容》

8. 步驟 8: 完成后處理

    // 步驟 8: 完成后處理
    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);
            }
        }
    }
}

整個(gè)請求處理完畢,即在視圖渲染完畢時(shí)回調(diào) applyAfterConcurrentHandlingStarted 方法。該方法的核心在于 afterCompletion 方法,該方法無論視圖渲染成功,都會(huì)調(diào)用,但僅調(diào)用處理器執(zhí)行鏈中 preHandle 返回 true 的攔截器的 afterCompletion。

該方法在初學(xué)時(shí)一般不會(huì)在意,但依舊有很大的應(yīng)用空間,如性能監(jiān)控中我們可以在此記錄結(jié)束時(shí)間并輸出消耗時(shí)間;另外還可以進(jìn)行一些資源清理,類似于 try-catch-finally 中的 finally。

9. 流程結(jié)束

上述步驟 3 至 步驟 8,是一個(gè)正常結(jié)束的 doDispatch 流程,即攔截器返回值全部為 true。它的流程順序如下圖 4.3 所示:

圖 4.3 正常的 doDispatch 流程

當(dāng)然也存在 doDispatch 的中斷流程,該部分的具體細(xì)節(jié),可以參閱博客《第五章 處理器攔截器詳解——跟著開濤學(xué)SpringMVC 》。

至此,返回控制權(quán)給 DispatcherServlet,由 DispatcherServlet 返回相應(yīng)給用戶,至此一個(gè)流程結(jié)束。

后記

紙上得來終覺淺,絕知此事要躬行。
筆者入職之后看了好幾天的關(guān)于 Spring 的各類各樣的書,書上講的倒還好,但是自己就是記不住。畢竟如果只是看書,幾乎是不能記住如此抽象的知識(shí)內(nèi)容的。所以,面對 Spring 與 Spring MVC 這種體系龐大的框架,一定要通過單步調(diào)試的方法慢慢體會(huì)并總結(jié),這樣才有可能將整個(gè)流程較為穩(wěn)妥的記在心里。
但筆者對 Spring 的學(xué)習(xí)也尚未滿一個(gè)月,所以本文的解釋不夠詳細(xì),也許會(huì)有些啰嗦不知所以然,請各位讀者諒解,并希望各位能夠在閱讀后提出寶貴意見,可以使我們共同進(jìn)步。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong閱讀 22,931評論 1 92
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,253評論 6 342
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,502評論 19 139
  • Spring MVC一、什么是 Spring MVCSpring MVC 屬于 SpringFrameWork 的...
    任任任任師艷閱讀 3,532評論 0 32
  • 南唐李后主的“春花秋月何時(shí)了,往事知多少”,“問君能有幾多愁,恰似一江春水向東流”流傳甚廣,也許你不知道李煜,但也...
    游夏閱讀 1,999評論 6 9

友情鏈接更多精彩內(nèi)容