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

推荐阅读更多精彩内容

  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong阅读 22,363评论 1 92
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,778评论 6 342
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • Spring MVC一、什么是 Spring MVCSpring MVC 属于 SpringFrameWork 的...
    任任任任师艳阅读 3,374评论 0 32
  • 南唐李后主的“春花秋月何时了,往事知多少”,“问君能有几多愁,恰似一江春水向东流”流传甚广,也许你不知道李煜,但也...
    游夏阅读 1,204评论 6 9