天天看点

深入理解spring mvc启动过程与原理

作者:实战Java
深入理解spring mvc启动过程与原理

spring mvc的启动,是跟随着tomcat启动的,所以要深入理解spring mvc的启动过程与原理,需要先了解下tomcat启动的一些关键过程。

1、tomcat web应用启动及初始化过程

参考官方文档,tomcat web应用启动过程是这样的:

深入理解spring mvc启动过程与原理

图1 tomcat web应用启动过程

大概意思就是,当一个Web应用部署到容器内时,在web应用开始执行用户请求前,会依次执行以下步骤:

  • 部署描述文件web.xml中<listener>元素标记的事件监听器会被创建和初始化;
  • 对于所有事件监听器,如果实现了ServletContextListener接口,将会执行其实现的contextInitialized()方法;
  • 部署描述文件中由<filter>元素标记的过滤器会被创建和初始化,并调用其init()方法;
  • 部署描述文件中由<servlet>元素标记的servlet会根据<load-on-startup>的权值按顺序创建和初始化,并调用其init()方法;

通过上述文档的描述,可知tomcat web应用启动初始化流程是这样的:

深入理解spring mvc启动过程与原理

图2 tomcat web应用初始化过程

可以看出,在tomcat web应用的初始化流程是,先初始化listener,接着初始化filter,最后初始化servlet。

2、spring mvc应用的启动初始化

做过spring mvc项目开发的伙伴,都会配置一个web.xml配置文件,内容一般是这样的:

<?xml version="1.0" encoding="UTF-8"?>
<web-app
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
    <!--全局变量配置-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:spring/spring-main.xml
    </param-value>
    </context-param>
    <!--监听器-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <listener>
        <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
    </listener>
    <!--解决乱码问题的filter-->
    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <!-- MVC Servlet -->
    <servlet>
        <servlet-name>springServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath*:spring/spring-mvc*.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>           

web.xml配置文件中也主要是配置了Listener,Filter,Servlet。

所以spring mvc应用启动的时候,主要是在这三大组件初始化的过程中完成对spring及spring mvc的初始化。

3、Listener与spring的初始化过程

web.xml配置文件中首先定义了<context-param>标签,用于配置一个全局变量,<context-param>标签的内容读取后会做为Web应用的全局变量使用。当Listener创建的时候,会使用到这个全局变量,因此,Web应用在容器中部署后,进行初始化时会先读取这个全局变量,之后再进行初始化过程。

接着定义了一个ContextLoaderListener类的Listener。查看ContextLoaderListener的类声明如图:

深入理解spring mvc启动过程与原理

图3 ContextLoaderListener类源码

ContextLoaderListener类继承自ContextLoader类,并实现了ServletContextListener接口。

深入理解spring mvc启动过程与原理

图4 ServletContextListener源码

ServletContextListener只有两个方法,contextInitialized和contextDestroyed,当Web应用初始化或销毁时会分别调用这两个方法。

ContextLoaderListener实现了ServletContextListener接口,因此在Web应用初始化时会调用contextInitialized方法,该方法的具体实现如下:

深入理解spring mvc启动过程与原理

图5 contextInitialized方法

ContextLoaderListener的contextInitialized()方法直接调用了initWebApplicationContext()方法,这个方法是继承自ContextLoader类,通过函数名可以知道,该方法是用于初始化web应用上下文,即IOC容器。

这里是spring mvc应用启动的第一个重点,就是在ContextLoaderListener初始化的时候,初始化了spring IOC容器。

我们继续看ContextLoader类的initWebApplicationContext()方法。

//servletContext,servlet上下文
  public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
  //  首先通过WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
  //  这个String类型的静态变量获取一个根IoC容器,根IoC容器作为全局变量
  //  存储在servletContext对象中,如果存在则有且只能有一个
  //  如果在初始化根WebApplicationContext即根IoC容器时发现已经存在
  //  则直接抛出异常,因此web.xml中只允许存在一个ContextLoader类或其子类的对象
    if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
      throw new IllegalStateException(
          "Cannot initialize context because there is already a root application context present - " +
          "check whether you have multiple ContextLoader* definitions in your web.xml!");
    }


    servletContext.log("Initializing Spring root WebApplicationContext");
    Log logger = LogFactory.getLog(ContextLoader.class);
    if (logger.isInfoEnabled()) {
      logger.info("Root WebApplicationContext: initialization started");
    }
    long startTime = System.currentTimeMillis();


    try {
      // Store context in local instance variable, to guarantee that
      // it is available on ServletContext shutdown.
     // 如果当前成员变量中不存在WebApplicationContext则创建一个根WebApplicationContext
      if (this.context == null) {
        this.context = createWebApplicationContext(servletContext);
      }
      if (this.context instanceof ConfigurableWebApplicationContext) {
        ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
        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 ->
            // determine parent for root web application context, if any.
            // 为根WebApplicationContext设置一个父容器
            ApplicationContext parent = loadParentContext(servletContext);
            cwac.setParent(parent);
          }
          // 配置并刷新整个根IoC容器,在这里会进行Bean的创建和初始化
          configureAndRefreshWebApplicationContext(cwac, servletContext);
        }
      }
      // 将创建好的IoC容器放入到servletContext对象中,并设置key为WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
      // 因此,在SpringMVC开发中可以在jsp中通过该key在application对象中获取到根IoC容器,进而获取到相应的Ben
      servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);


      ClassLoader ccl = Thread.currentThread().getContextClassLoader();
      if (ccl == ContextLoader.class.getClassLoader()) {
        currentContext = this.context;
      }
      else if (ccl != null) {
        currentContextPerThread.put(ccl, this.context);
      }


      if (logger.isInfoEnabled()) {
        long elapsedTime = System.currentTimeMillis() - startTime;
        logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
      }


      return this.context;
    }
    catch (RuntimeException | Error ex) {
      logger.error("Context initialization failed", ex);
      servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
      throw ex;
    }
  }           

在jsp中,可以通过这两种方法获取到IOC容器:

WebApplicationContext applicationContext = (WebApplicationContext) servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);


WebApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());           

总的来说,initWebApplicationContext方法的主要目的是创建root WebApplicationContext对象,即根IOC容器。

如果整个Web应用存在根IOC容器则有且只能有一个,根IOC容器会作为全局变量存储在ServletContext对象中。然后将根IOC容器放入到ServletContext对象之前进行了IOC容器的配置和刷新操作,即调用configureAndRefreshWebApplicationContext()方法,该方法源码如下:

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
  if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
    // The application context id is still set to its original default value
    // -> assign a more useful id based on available information
    String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
    if (idParam != null) {
      wac.setId(idParam);
    }
    else {
      // Generate default id...
      wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
          ObjectUtils.getDisplayString(sc.getContextPath()));
    }
  }
  wac.setServletContext(sc);
  // 获取web.xml中<context-param>标签配置的全局变量,其中key为CONFIG_LOCATION_PARAM
  // 也就是我们配置的相应Bean的xml文件名,并将其放入到WebApplicationContext中
  String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
  if (configLocationParam != null) {
    wac.setConfigLocation(configLocationParam);
  }
  // The wac environment's #initPropertySources will be called in any case when the context
  // is refreshed; do it eagerly here to ensure servlet property sources are in place for
  // use in any post-processing or initialization that occurs below prior to #refresh
  ConfigurableEnvironment env = wac.getEnvironment();
  if (env instanceof ConfigurableWebEnvironment) {
    ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
  }
  customizeContext(sc, wac);
  wac.refresh();
}           

configureAndRefreshWebApplicationContext方法里获取了<context-param>标签配置的全局变量,并且在方法最后调用了refresh()方法。

对spring容器初始化有一定了解的同学都知道,这是初始化spring容器的入口方法,其最终调用的是AbstractApplicationContext类中的refresh方法。

refresh()方法主要是创建并初始化contextConfigLocation类配置的xml文件中的Bean。

具体代码不贴了,有兴趣的同学,可以自行查阅。

到此为止,整个ContextLoaderListener类的启动过程就结束了,可以发现,创建ContextLoaderListener是比较重要的一个步骤,主要做的事情就是创建根IOC容器,并使用特定的key将其放入到servletContext对象中,供整个Web应用使用。

由于在ContextLoaderListener类中构造的根IOC容器配置的Bean是全局共享的,因此,在<context-param>标识的contextConfigLocation的xml配置文件一般包括:数据库DataSource、DAO层、Service层、事务等相关Bean。

4、Filter与spring的初始化

在监听器Listener初始化完成后,接下来会进行Filter的初始化操作,Filter的创建和初始化没有涉及IOC容器的相关操作,因此不是本文讲解的重点。

5、Servlet与spring的初始化

web应用启动的最后一个步骤就是创建和初始化相关servlet,servlet最重要的方法就是:init(),service(),destroy();

深入理解spring mvc启动过程与原理

图6 Servlet源码

在spring mvc中,实现了一个非常重要的servlet,即DispatcherServlet。它是整个spring mvc应用的核心,用于获取分发用户请求并返回响应。

深入理解spring mvc启动过程与原理

图7 DispatcherServlet类图

DispatcherServlet本质上也是一个Servlet,其源码实现充分利用了模板模式,将不变的部分统一实现,将变化的部分留给子类实现,上层父类不同程度的实现了相关接口的部分方法,留出了相关方法由子类覆盖。

深入理解spring mvc启动过程与原理

图8 DispatcherServlet类初始化过程

web应用部署到容器启动后,进行Servlet的初始化时会调用相关的init(ServletConfig)方法,因此,DispatchServlet类的初始化过程也由该方法开始。

其中比较重要的是,FrameworkServlet类中的initServletBean()方法、initWebApplicationContext()方法以及DispatcherServlet类中的onRefresh()方法。

深入理解spring mvc启动过程与原理

图9 FrameworkServlet类中的initServletBean()方法

FrameworkServlet的父类HttpServletBean中的initServletBean()方法,HttpServletBean抽象类在执行init()方法时会调用initServletBean()方法。

该方法中比较重要的是initWebApplicationContext()方法,该方法仍由FrameworkServlet抽象类实现,继续查看其源码如下所示:

protected WebApplicationContext initWebApplicationContext() {
    // 获取由ContextLoaderListener创建的根IoC容器
    // 获取根IoC容器有两种方法,还可通过key直接获取
    WebApplicationContext rootContext =
        WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;


    if (this.webApplicationContext != null) {
      // A context instance was injected at construction time -> use it
      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
            // 如果当前Servelt存在一个WebApplicationContext即子IoC容器
            // 并且上文获取的根IoC容器存在,则将根IoC容器作为子IoC容器的父容器
            cwac.setParent(rootContext);
          }
          // 配置并刷新当前的子IoC容器,功能与前面讲解根IoC容器时的配置刷新一致,用于构建相关Bean
          configureAndRefreshWebApplicationContext(cwac);
        }
      }
    }
    if (wac == null) {
      // No context instance was injected at construction time -> see if one
      // has been registered in the servlet context. If one exists, it is assumed
      // that the parent context (if any) has already been set and that the
      // user has performed any initialization such as setting the context id
      // 如果仍旧没有查找到子IoC容器则创建一个子IoC容器
      wac = findWebApplicationContext();
    }
    if (wac == null) {
      // No context instance is defined for this servlet -> create a local one
      wac = createWebApplicationContext(rootContext);
    }


    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.
      synchronized (this.onRefreshMonitor) {
        // 调用子类覆盖的onRefresh方法完成“可变”的初始化过程
        onRefresh(wac);
      }
    }


    if (this.publishContext) {
      // Publish the context as a servlet context attribute.
      String attrName = getServletContextAttributeName();
      getServletContext().setAttribute(attrName, wac);
    }


    return wac;
  }           

通过函数名不难发现,该方法的主要作用同样是创建一个WebApplicationContext对象,即IOC容器,不过这里创建的是spring mvc容器,是前面ContextLoadListener创建的IOC容器的子容器。

为什么需要多个IOC容器?还是父子容器?这就要说明下父子IOC容器的访问特性了。

父子容器类似于类的继承关系,子类可以访问父类中的成员变量,而父类不可访问子类的成员变量,同样的,子容器可以访问父容器中定义的Bean,但父容器无法访问子容器定义的Bean。

创建多个容器的目的是,父IOC容器做为全局共享的IOC容器,存放Web应用共享的Bean,比如Service,DAO。而子IOC容器根据需求的不同,放入不同的Bean,比如Conroller,这样能够做到隔离,保证系统的安全性。

如果你看过sprin cloud netflix系列源码,比如spring-cloud-netflix-ribbon,spring-cloud-openfeign,就会发现它里边创建了很多父子容器,作用与这里是一样的,容器与容器相互隔离,保证系统的安全性。

我们继续讲解DispatcherServlet类的子IOC容器创建过程,如果当前Servlet存在一个IOC容器则为其设置根IOC容器作为其父类,并刷新该容器,用于初始化定义的Bean。

这里的方法与前文讲述的根IOC容器类似,同样会读取用户在web.xml中配置的<servlet>中的<init-param>值,用于查找相关的xml配置文件来创建定义的Bean。如果当前Servlet不存在一个子IoC容器就去查找一个,如果没有查找到,则调用createWebApplicationContext()方法去创建一个。

深入理解spring mvc启动过程与原理

图10 createWebApplicationContext源码

该方法用于创建一个子IOC容器并将根IOC容器做为其父容器,接着执行配置和刷新操作构建相关的Bean。

至此,根IOC容器以及相关Servlet的子IOC容器已经初始化完成了,子容器中管理的Bean一般只被该Servlet使用,比如SpringMVC中需要的各种重要组件,包括Controller、Interceptor、Converter、ExceptionResolver等。

spring mvc父子容器之间的关系,可以用下图来描述:

深入理解spring mvc启动过程与原理

图11 spring mvc父子容器

当IOC子容器构造完成后调用了onRefresh()方法,其实现是在子类DispatcherServlet中,查看DispatcherServletBean类的onRefresh()方法源码如下:

深入理解spring mvc启动过程与原理

图12 onRefresh方法

onRefresh()方法调用了initStrategies()方法,通过函数名可以判断,该方法主要初始化创建multipartResovle来支持图片等文件的上传、本地化解析器、主题解析器、HandlerMapping处理器映射器、HandlerAdapter处理器适配器、异常解析器、视图解析器、flashMap管理器等。这些组件都是SpringMVC开发中的重要组件,相关组件的初始化创建过程均在此完成,所以在这里最终完成了spring mvc组件的初始化。

至此,整个spring mvc应用启动的原理以及过程就讲完了,总的来说,spring以及spring mvc的初始化过程是跟随在tomcat的Listener、Filter、Servlet的初始化过程完成的。

6、总结

spring以及spring mvc应用初始化过程还是比较清晰明了的,本文做了完整的分析记录。spring mvc也支持无web.xml配置文件的方式开发web应用,其原理是类似的,只不过是用代码的方式来创建了web.xml中需要的组件。

后续会分析spring boot应用的启动以及初始化过程分析,spring boot是在spring及spring mvc的基础上做了更深的封装,所以看起来也更复杂。spring boot应用既可以以war包的方式运行在tomcat上,也可以以jar的方式运行再内嵌的tomcat上,两种方式启动的源码有所不同,还需要分别分析。