IDEA 新建 Spring MVC 工程項目與 SpringMVC 運行流程

前文

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

本文通過 IDEA 安裝 Spring MVC 項目。首先在 IDEA 官網下載適合自己電腦配置版本的 Idea,然后進行安裝,安裝過程省略。

一. IDEA 新建 Spring MVC 工程項目

1.1 新建工程

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

圖 1.1 New Project

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

設置 GroupId 和 ArtifactId 的截圖如下所示:

圖 1.2 GroupId & ArtifactId

工程項目構建完畢后,左側的 Project 欄顯示如下圖 1.3 所示:

圖 1.3 新建工程后的 Project 視圖

1.2 Maven 設置

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

圖 1.4 Maven 設置

選擇如下設置,點擊 OK。

之后就可以通過連接網絡上的 Maven 庫,下載所需的依賴庫了。

注:
如果有本地 Maven 庫的話,可以設置圖 1.4 的 User settings file, Local repository,這樣就可以實現本地依賴庫的導入)

1.3 Maven 依賴庫內容的填寫

第一步中的工程構建完畢后,Project 列表中有一個 pom.xml。其中 pom 是項目對象模型 (Project Object Model) 的簡稱,它是 Maven 項目中的文件,使用 xml 表示。它的作用類似 ant 的 build.xml 文件,功能更強大。該文件用于管理源代碼、配置文件、開發者的信息和角色、問題追蹤系統、組織信息、項目授權、項目的 url 、項目的依賴關系等等。事實上,在 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>

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

圖 1.5 Maven Project 視圖下的導入 Maven 按鈕
圖 1.6 加載成功后的 Project 視圖

1.4 添加 Tomcat 依賴庫

1.4.1 Tomcat 作用

我們通常說到的 servlet 可以理解服務器端處理數據的 java 小程序,負責管理 servlet 就是 web 容器。它幫助我們管理著servlet等,使我們只需要將重心專注于業務邏輯。servlet 沒有 main 方法,那我們如何啟動一個 servlet,如何結束一個 servlet,如何尋找一個servlet 等等,都受控于另一個 java 應用,這個應用我們就稱之為 web 容器。或者可以理解成 servlet 只是一個規范,web 容器遵照這個規范,實現支持 servlet 規范的請求和應答。

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

1.4.2 添加 Tomcat 依賴庫

點擊菜單 File -> Project Structure,彈出設置對話框。選中左側欄 Project Settings 的 Libraries,點擊上面的加號"+",選擇 "Java" 選項,如圖 1.7 所示:

圖 1.7 Project Structure 添加 Libraries

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

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

1.5 項目中添加 Web 工程

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

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

圖 1.9 Project Structure -> Facets

此外在右下角有一個 Create Artifact 的按鈕,點擊后進入 Artifacts 標簽欄中,將右邊區域的所有內容添加到左區域,然后點擊 Ok 完成 Web 工程的添加。如圖 1.10 所示。

圖 1.10 Project Structure -> Artifacts

二. 填充 Spring MVC 內容

2.1 View 的編寫

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

<!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 的編寫

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

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 文件

接下來我們為父子容器添加 xml 文件。在這里,父容器指 Spring 容器,子容器指 SpringMVC 容器。

關于父子容器相關的內容,可以參考《spring的啟動過程——spring和springMVC父子容器的原理》
《Spring和SpringMVC父子容器關系初窺》

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

2.3.1 application-context.xml

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

<?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">
    <!-- 自動掃描該包下的 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。填寫內容如下:

<?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> 會自動注冊 RequestMappingHandlerMapping 與 RequestMappingHandlerAdapter 兩個 Bean,這兩個是 Spring MVC 為 @Controller 分發請求所必需的,并且提供了數據綁定支持

<mvc:default-servlet-handler />:

在 application-context-mvc.xml 中配置 <mvc:default-servlet-handler />后,會在Spring MVC上下文中定義一個 org.springframework.web.servlet.resource 包下的 DefaultServletHttpRequestHandler,它的作用類似于一個檢查員,對進入 DispatcherServlet 的 URL 進行篩查。如果發現是靜態資源的請求,就將該請求轉由 Web 應用服務器默認的 Servlet 處理;如果不是靜態資源的請求,才由 DispatcherServlet 繼續處理。

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

<mvc:default-servlet-handler default-servlet-name="所使用的Web服務器默認使用的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">

    <!--業務層與模型層的 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 進行配置-->
        <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, 啟動加載級別等。對于一個 Web 項目,是可以沒有 web.xml 文件的。當你的 Web 工程沒用到這些時,你可以不用 web.xml 文件來配置你的 Application。也就是說,web.xml 文件并不是 Web 工程必須的
web.xml 的模式文件中定義的標簽并不是定死的,模式文件也是可以改變的。一般來說,隨著 web.xml 模式文件的版本升級,里面定義的功能會越來越復雜,標簽元素的種類肯定也會越來越多,但有些不是很常用的,我們只需記住一些常用的并知道怎么配置就可以了。

web.xml 相關內容參考:《springmvc配置文件web.xml詳解各方總結。》

5. 配置 Tomcat

配置 Tomcat 要通過 Edit Configuration 進行。選項的外形如下圖 2.1 所示:

圖 2.1 Edit Configuration 按鈕

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

圖 2.2 Tomcat Server 配置

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

圖 2.3 配置 Tomcat 路徑

三. Spring MVC 容器初始化

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

下面通過單步調試的方法,詳細解釋 SpringMVC 的初始化運行步驟。

Spring MVC 的核心在于 DispatcherServlet,觀察 DispatcherServlet 的繼承結構如下圖 3.1 所示:

圖 3.1 DispatcherServlet 繼承結構

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

1. HttpServlet.init()

Web 容器啟動時將調用它的 init 方法。源碼如下:

public final void init() throws ServletException {
    if (logger.isDebugEnabled()) {
        logger.debug("Initializing servlet '" + getServletName() + "'");
    }
 
    // 從初始化參數中設置 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;
    }
 
    // 子類調用初始化
    initServletBean();
 
    if (logger.isDebugEnabled()) {
        logger.debug("Servlet '" + getServletName() + "' configured successfully");
    }
}

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

2. HttpServletBean.initServletBean()

HttpServletBean 是 FrameworkServlet 的子類。接下來就關注一下調用子類初始化的 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 {
        // 內容 1: 完成了 Web 上下文的初始化工作;
        // ContextLoaderListener 加載了上下文將作為根上下文(DispatcherServlet 的父容器)
        this.webApplicationContext = initWebApplicationContext();
        // 內容 2: 提供給子類進行初始化的擴展點。行容器的一些初始化,這個方法由子類實現,來進行擴展;
        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() 方法的主要內容,分為兩個功能:一是完成 Web 上下文的初始化工作,這是主要內容。二是提供給子類,進行初始化
再接下來,需要進入 HttpServletBean 的 initWebApplicationContext()。

3. HttpServletBean.initWebApplicationContext()

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

protected WebApplicationContext initWebApplicationContext() {
    WebApplicationContext rootContext =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;
 
    if (this.webApplicationContext != null) {
        // 步驟 1. 在創建的時候注入根上下文
        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. 如果經過步驟 1,沒有注入上下文,則尋找上下文
    if (wac == null) {
        wac = findWebApplicationContext();
    }
    // 步驟 3. 如果沒有找到相應的上下文,則手動創建一個,并指定父親為其 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;
}

該過程中,經歷了四個步驟:

  1. 在創建時注入根上下文;
  2. 如果此時沒有注入上下文,則開始尋找上下文;
  3. 如果此時沒有注入上下文,則手動創建一個,并指定其父親為其 ContextLoaderListenr;
  4. 刷新上下文

四個步驟中,最重要的是手動創建上下文部分

4. FrameworkServlet.createWebApplicationContext

該部分可以手動創建一個上下文。主要源碼如下:

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());
        }
    }

    // 設置上下文參數
    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();
}

設置上下文參數的部分,獲取了所有 WebApplicationContext 相關的值,并將其注入了 WebApplicationContext 中。

5. DispatcherServlet.onRefresh()

最后調用 onRefresh() 方法。onRefresh() 方法是抽象基類 AbstractApplicationContext 的方法,實際運行時被 DispatcherServlet 所覆蓋,它在內部調用了 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);  
}  

進入 DispatcherServlet 的 initStrategies,此時所有的 bean 都已經加載好了;程序運行到這里,就已經實現了 DispatcherServlet 初始化的工作。后面進入 DispatcherServlet 處理用戶響應的過程。

四. SpringMVC 響應 —— doDispatch 的運行流程

參考網址:
運行流程:
《第二章 Spring MVC入門 —— 跟開濤學SpringMVC》

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

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

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

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

由上面的源碼可以看出,DispatcherServlet 的核心是調用 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 進行映射
            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: 預處理
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }

            // 步驟 5: 由適配器執行處理器
            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);
            }
        }
    }
}

下面按照上述源碼中標注的步驟,進入 DispatcherServlet 的 doDispatch 方法并分析:

注:下面的步驟 3 至步驟 8 是一個核心運行流程,并將該核心流程的總結放在最后。

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;  
}  

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

圖 4.1 HandlerExecutionChain

HandlerExecutionChain 的主要功能是通過若干 HandlerInterceptor 實現的。HandlerInterceptor 主要方法如下:

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

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

3. 步驟 3: 處理器適配

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

該步驟中,核心方法是 getHandlerAdapter,它將我們的 Handler 包裝成相應適配器 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,并分別調用它們的 support(handler) 方法,直到 support 方法返回值為 true 為止(此時 HandlerAdapter 的類型為 RequestMappingHandlerAdapter)

4. 步驟 4: 預處理

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

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

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 方法層層攔截,若存在某個攔截器的 preHandle 返回 false,則該方法返回 false;此外只要有一個攔截器返回 false,相應的 doDispatch 方法也將返回結束。

5. 步驟 5: 由適配器執行處理器

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

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

該步驟中,核心方法是 handle 方法,它是 HandlerAdapter 的接口方法,具體需要按照不同的 HandlerAdapter 類型進行實現。
此前提到運行到這里,此時的 HandlerAdapter 類型是 RequestMappingHandlerAdapter。在 RequestMappingHandlerAdapter 中進行一些步驟的跳轉,會調用 AbstractHandlerMethodAdapter 抽象類的 handleInternal 方法,并用其子類的具體實現。
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),它的輸入參數 handlerMethod 中包含先前獲取到的方法相關信息(包括方法名 method 與參數 parameters),在該方法中調用 invokeHandlerMethod,返回值即為 ModelAndView 類型的數據。

6. 步驟 6: 后處理

mappedHandler.applyPostHandle(processedRequest, response, mv);

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

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 方法層層攔截,若存在某個攔截器的 postHandle 返回 false,則該方法返回 false;此外只要有一個攔截器返回 false,相應的 doDispatch 方法也將返回結束。

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

步驟 5 中獲得 ModelAndView 參數后,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 實現。具體解析的方法,是在容器中查找所有配置好的 List 類型的視圖解析器 (ViewResolver),然后進行遍歷,只要存在一個視圖解析器,就能解析出視圖。調用該視圖解析器的方法對 View 進行解析,最后返回該 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 是一個接口方法,具體需要由實現了 View 接口的類具體實現。接口方法如下:

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

View 接口主要由 AbstractView 抽象類繼承,在 AbstractView 中的 render 方法實現如下:

// 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);
  // 核心源碼,進行實時渲染
  renderMergedOutputModel(mergedModel, request, response);
}

render 方法為指定的模型指定視圖,如果有必要的話,在 createMergedOutputModel 方法中合并它靜態的屬性和 RequestContext 中的屬性,最后在核心源碼 renderMergedOutputModel() 中執行實際的渲染。
renderMergedOutputModel 也是一個抽象方法,由具體的視圖解析器具體實現。以 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 中填充數據,獲取了 RequestDispatcher 后,最后通過 include 或 forward 方法轉發,正常狀況下,運行到這里我們就會在瀏覽器上看到了頁面內容,如下圖 4.2 所示。

圖 4.2 訪問頁面

關于 include 與 forward 的區別,可參考:《SpringMVC——使用RequestDispatcher.include()和HttpServletResponseWrapper動態獲取jsp輸出內容》

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);
            }
        }
    }
}

整個請求處理完畢,即在視圖渲染完畢時回調 applyAfterConcurrentHandlingStarted 方法。該方法的核心在于 afterCompletion 方法,該方法無論視圖渲染成功,都會調用,但僅調用處理器執行鏈中 preHandle 返回 true 的攔截器的 afterCompletion。

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

9. 流程結束

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

圖 4.3 正常的 doDispatch 流程

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

至此,返回控制權給 DispatcherServlet,由 DispatcherServlet 返回相應給用戶,至此一個流程結束。

后記

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

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