SpringMVC应用是怎么启动的

384 阅读10分钟

我们使用Spring+SpringMVC时通常会有web.xml文件,一般内容是这样的:ContextLoaderListenerDispatcherServlet

这个配置文件来配置:listener、filter、servlet

 <?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">
 ​
      <!-- 配置ContextLoaderListener -->        
     <listener>
         <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
     </listener>
     <!-- 指定Spring配置文件所在位置 -->
     <context-param>
         <param-name>contextConfigLocation</param-name>
         <param-value>classpath:spring.xml</param-value>
     </context-param>
 ​
     <!-- 配置DispatcherServlet -->
     <servlet>
         <servlet-name>spring-mvc</servlet-name>
         <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
 ​
         <!-- <!-- 指定Spring MVC配置文件所在位置 --> -->
         <init-param>
             <param-name>contextConfigLocation</param-name>
             <param-value>classpath:spring-mvc.xml</param-value>
         </init-param>
     </servlet>
 ​
     <servlet-mapping>
         <servlet-name>spring-mvc</servlet-name>
         <url-pattern>*.do</url-pattern>
     </servlet-mapping>
 </web-app>

在Tomcat启动的时候会加载web.xml,根据启动的配置加载 listener、filter、servlet,然后加载对应的回调方法

顺序是这样的:listener ---> filter ---> servlet

在SpringMVC应用启动的时候,主要是对这三大组件的加载和对Spring和Spring MVC的IOC容器的初始化。

根IOC容器的初始化-ContextLoaderListener

在Tomcat启动时会调用contextInitialized()进行初始化根ioc容器

 public void contextInitialized(ServletContextEvent event) {
     // 根据ServletContext初始化根ioc容器
     initWebApplicationContext(event.getServletContext());
 }

ContextLoader#initWebApplicationContext()

 public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
     // 1、检查ServletContext中是否有根ioc容器
     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!");
     }
 ​
     ...
     
     long startTime = System.currentTimeMillis();
 ​
     try {
 ​
         // 2、如果没有根ioc容器,那么就创建一个
         if (this.context == null) {
             // 创建ioc容器
             this.context = createWebApplicationContext(servletContext);
         }
         if (this.context instanceof ConfigurableWebApplicationContext) {
             ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
             // 3、判断此ioc容器是否被刷新过 即refresh()是否被调用过
             if (!cwac.isActive()) {
                 // 3.1、是否有父ioc容器
                 if (cwac.getParent() == null) {
                     // 加载父ioc容器(默认是null)
                     ApplicationContext parent = loadParentContext(servletContext);
                     // 设置父容器
                     cwac.setParent(parent);
                 }
                 // 3.2、配置并且刷新ioc容器【重要】
                 configureAndRefreshWebApplicationContext(cwac, servletContext);
             }
         }
         // 4、将ioc容器绑定到 ServletContext 中
         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;
     }
 }
  1. 检查ServletContext中是否有根ioc容器(如果ioc容器创建过会被设置到ServletContext中)

  2. 如果根ioc容器没有被实例化过,那么就创建一个 createWebApplicationContext(servletContext)

  3. 判断此ioc容器是否被刷新过:refresh()是否被调用过

    1. 判断此ioc容器是否有父ioc容器,如果没有就尝试为它设置一个
    2. 配置并且刷新ioc容器configureAndRefreshWebApplicationContext()重要
  4. 将ioc容器绑定到ServletContext

1、检查ServletContext中是否有根ioc容器

 String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
 ​
 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!");
 }

image.png

2、创建ioc容器

 protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
     // 1、找到Class
     Class<?> contextClass = determineContextClass(sc);
     if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
         throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
                                               "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
     }
     // 2、实例化ioc容器
     return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
 }
 ​
 private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";
 ​
 static {
 ​
     // 从 ContextLoader.properties 文件中加载默认的策略
     try {
         ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
         // 默认的策略为:XmlWebApplicationContext
         defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
     }
     ...
 }
 ​
 protected Class<?> determineContextClass(ServletContext servletContext) {
     String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
     if (contextClassName != null) {
         try {
             // 得到大的Class实例
             return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
         }
         catch (ClassNotFoundException ex) {
             throw new ApplicationContextException(
                 "Failed to load custom context class [" + contextClassName + "]", ex);
         }
     }
     else {
         // 拿到默认策略:xml版的ioc容器
         contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
         try {
             // 得到大的Class实例
             return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
         }
         catch (ClassNotFoundException ex) {
             throw new ApplicationContextException(
                 "Failed to load default context class [" + contextClassName + "]", ex);
         }
     }
 }
 ​

默认策略

image.png

刚创建出来的IOC容器:

image.png

3、检查此容器是否被刷新过

 if (this.context instanceof ConfigurableWebApplicationContext) {
     ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
     // 3、判断此ioc容器是否被刷新过:即refresh()是否被调用过
     if (!cwac.isActive()) {
         // 3.1、是否有父ioc容器
         if (cwac.getParent() == null) {
             // 得到父ioc容器,默认是null【模板方法】
             ApplicationContext parent = loadParentContext(servletContext);
             // 设置父容器
             cwac.setParent(parent);
         }
         // 3.2、配置并且刷新ioc容器【重要】
         configureAndRefreshWebApplicationContext(cwac, servletContext);
     }
 }

1、设置父容器

 // 3.1、是否有父ioc容器
 if (cwac.getParent() == null) {
     // 得到父ioc容器,默认是null【模板方法】
     ApplicationContext parent = loadParentContext(servletContext);
     // 设置父容器
     cwac.setParent(parent);
 }
 ​
 protected ApplicationContext loadParentContext(ServletContext servletContext) {
     return null;
 }

2、刷新容器

如果此容器没有被刷新过,那么就调用configureAndRefreshWebApplicationContext()进行刷新

 public static final String CONTEXT_ID_PARAM = "contextId";
 ​
 String APPLICATION_CONTEXT_ID_PREFIX = WebApplicationContext.class.getName() + ":";
 ​
 public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
 ​
 protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
     // 1、设置ioc容器的id
     if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
         // 情况1:使用 contextId 参数的值作为id
         String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
         if (idParam != null) {
             wac.setId(idParam);
         }
         else {
             // 情况2:使用全类型+:+项目的 contextPath 作为id
             wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                       ObjectUtils.getDisplayString(sc.getContextPath()));
         }
     }
 ​
     // 3、设置ServletContext
     wac.setServletContext(sc);
     // 4、设置spring配置文件的位置到ioc容器中(配置文件中 <context-param> 的值)
     String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
     if (configLocationParam != null) {
         wac.setConfigLocation(configLocationParam);
     }
 ​
     // 5、初始化PropertySource
     ConfigurableEnvironment env = wac.getEnvironment();
     if (env instanceof ConfigurableWebEnvironment) {
         ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
     }
 ​
     // 6、执行 ApplicationContextInitializer
     customizeContext(sc, wac);
     // 7、、刷新ioc容器
     wac.refresh();
 }

最重要的一步就是调用AbstractApplicationContext类的refresh()进行容器刷新。就是Spring IOC容器刷新的经典12大步了。这里就不分析这12步了。

还没刷新的IOC:

image.png

刷新后:

image.png

BeanFactory:

image.png

一级、二级、三级缓存:

image.png

环境信息:

image.png

4、绑定ioc容器到ServletContext中

 String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
 ​
 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

到这一步根容器就创建并刷新完成了。

SpringMVC IOC容器的初始化-DispatcherServlet

DispatcherServlet是一个ServletServlet第一次处理请求的时候会由Tomcat调用init()进行初始化

继承树:

 GenericServlet (javax.servlet)
     HttpServlet (javax.servlet.http)
         HttpServletBean (org.springframework.web.servlet)
             FrameworkServlet (org.springframework.web.servlet)
                 DispatcherServlet (org.springframework.web.servlet)

HttpServletBean

public final void init() throws ServletException {

    // 1、获取 DispatcherServlet 的配置
    PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
    if (!pvs.isEmpty()) {
        try {
            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) {
            if (logger.isErrorEnabled()) {
                logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
            }
            throw ex;
        }
    }

    // 2、初始化Servlet【模板方法,由子类实现】
    initServletBean();
}

FrameworkServlet

protected final void initServletBean() throws ServletException {

    // 1、初始化ioc容器
    this.webApplicationContext = initWebApplicationContext();
    // 2、初始化FrameworkServlet【模板方法,由子类实现】
    initFrameworkServlet();


    // 省略代码
    ...
}

initWebApplicationContext():初始化ioc容器

 protected WebApplicationContext initWebApplicationContext() {
     // 1、从ServletContext中获取根容器
     WebApplicationContext rootContext =
         WebApplicationContextUtils.getWebApplicationContext(getServletContext());
     WebApplicationContext wac = null;
 ​
     // 情况1:使用传入的。如果在实例化时注入了一个,它将使用这个ioc容器
     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
                     cwac.setParent(rootContext);
                 }
                 configureAndRefreshWebApplicationContext(cwac);
             }
         }
     }
     // 情况2:查找。如果构造时没有注入ioc容器,那么会从 ServletContext 中找一个已经注册的ioc容器
     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
         wac = findWebApplicationContext();
     }
     // 情况3:创建。把父容器传入,然后构造一个返回
     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(wac);
         }
     }
 ​
     if (this.publishContext) {
         // 保存ioc容器到ServletContext上下文中
         // 默认attrName = org.springframework.web.servlet.FrameworkServlet.CONTEXT.spring-mvc
         String attrName = getServletContextAttributeName();
         getServletContext().setAttribute(attrName, wac);
     }
 ​
     return wac;
 }

获取一个ioc容器分3种方式:

  • 使用实例化时通过构造方法传入的:如果在构造时注入了一个,它将使用这个ioc容器
  • 查找:如果构造时没有注入ioc容器,那么会从 ServletContext 中找一个已经注册的ioc容器
  • 创建:如果前2步都获取不到,那把父容器传入,然后构造一个返回

1、创建ioc容器

 protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
     // 1、获取大的Class实例
     Class<?> contextClass = getContextClass();
     if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
         throw new ApplicationContextException(
             "Fatal initialization error in servlet with name '" + getServletName() +
             "': custom WebApplicationContext class [" + contextClass.getName() +
             "] is not of type ConfigurableWebApplicationContext");
     }
     // 2、创建ioc容器
     ConfigurableWebApplicationContext wac =
         (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
 ​
     // 设置环境
     wac.setEnvironment(getEnvironment());
     // 设置父容器
     wac.setParent(parent);
     // 设置SpringMVC配置文件所在的位置(读取配置文件中 <init-param> 的值)
     String configLocation = getContextConfigLocation();
     if (configLocation != null) {
         wac.setConfigLocation(configLocation);
     }
     // 3、配置并且刷新ioc容器
     configureAndRefreshWebApplicationContext(wac);
 ​
     return wac;
 }

2、配置并且刷新ioc容器

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
    // 1、设置容器id
    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
        if (this.contextId != null) {
            wac.setId(this.contextId);
        }
        else {
            // Generate default id...
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                      ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
        }
    }

    // 设置ioc容器的ServletContext
    wac.setServletContext(getServletContext());
    // 设置ioc容器的Servlet的配置信息
    wac.setServletConfig(getServletConfig());
    // 设置ioc容器的命名空间
    wac.setNamespace(getNamespace());
    // 给ioc容器添加Listener
    wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

    // 初始化PropertySource
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
    }

    // ioc容器的后置处理
    postProcessWebApplicationContext(wac);
    // 执行初始化器
    applyInitializers(wac);
    // 刷新ioc容器【】
    wac.refresh();
}

刷新前:

image.png 刷新后:

image.png 到这一步SpringMVC的ioc容器也就创建和刷新完成了。

web.xml版的启动到这里也就结束了。

配置类+注解版

SPI

先说一下SPI是什么,全程是 Service Provider Interface 服务提供者接口。

接口:调用方调用提供方的接口,那么也就是说接口的规范是由提供方规定的。

SPI是这样的:调用方定义接口规范,提供方实现接口。只要按照SPI规定的规则做,那么它就能找到你

SPI是Java里的规范,它会加载 META-INF/services包下的文件:文件名应是全类名,文件中的值是此接口的实现的全类名

ServletContainerInitializer

这是servlet规范中的接口,它会在web应用启动后调用onStartup()

Tomcat在启动时,会利用SPI机制去classpath查找其ServletContainerInitializer的实现类,调用其onStartup()进行处理。

如果有找到的类标注了@HandlesTypes(),那么Tomcat会把实现了WebApplicationInitializer接口的类传递给onStartup()的参数。

Spring中实现了此接口来完成初始化工作。SpringServletContainerInitializer

SpringServletContainerInitializer

这个类上标注了@HandlesTypes()注解,Tomcat会把实现了WebApplicationInitializer接口的类传递给onStartup()的参数。

我们注解版的需要继承AbstractAnnotationConfigDispatcherServletInitializer来实现。

image.png


 @HandlesTypes(WebApplicationInitializer.class)  
 public class SpringServletContainerInitializer implements ServletContainerInitializer {
 ​
 ​
     @Override
     public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
             throws ServletException {
 ​
         List<WebApplicationInitializer> initializers = new LinkedList<>();
 ​
         if (webAppInitializerClasses != null) {
             for (Class<?> waiClass : webAppInitializerClasses) {
                 // 1、判断此类不是接口、也不是抽象类
                 if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                         WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                     try {
                         // 2、把此类实例化添加到集合中
                         initializers.add((WebApplicationInitializer)
                                 ReflectionUtils.accessibleConstructor(waiClass).newInstance());
                     }
                     catch (Throwable ex) {
                         throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                     }
                 }
             }
         }
 ​
         if (initializers.isEmpty()) {
             servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
             return;
         }
 ​
         servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
         // 3、排序 @Order注解、Order接口
         AnnotationAwareOrderComparator.sort(initializers);
         // 4、遍历所有WebApplicationInitializer,调用其onStartup()
         for (WebApplicationInitializer initializer : initializers) {
             initializer.onStartup(servletContext);
         }
     }
 ​
 }

总结

就是根据两个回调方法来完成ioc容器的创建和刷新。 在tomcat启动时会执行这两个回调方法

类图:

image.png

xml版的启动流程

  • Tomcat启动时加载web.xml,依次加载listener、filter、servlet
  • 回调ContextLoaderListenercontextInitialized()来完成根IOC容器的创建与刷新
  • 回调DispatcherServletinit()来完成IOC容器的创建与刷新

配置类+注解版的启动流程

  • Tomcat启动时使用SPI机制加载ServletContainerInitializer的实现类,并检查类上是否标注了@HandlerTypes()注解,如果标注了,那么Tomcat会把实现了WebApplicationInitializer接口的类传递给onStartup()的参数。

  • 通过SpringServletContainerInitializer来加载我们自定义的初始化器,并调用其onStartup()来处理

    • 创建一个ContextLoaderListener,利用其contextInitialized()完成根IOC容器的创建与刷新
    • 创建一个DispatcherServlet,利用其init()完成IOC容器的创建与刷新
    • 回调时机就是在tomcat启动后
    • 接下来的流程就和xml版一样了

虽然xml版也会利用SPI去找ServletContainerInitializer类,但是是没有任何一个实现的。所以下面的xml版的流程图是那么画的,是为了好理解,希望大家不要误解了。

还可以按照这样分:

  • 利用SpringMVC框架来帮我们创建DispatcherServlet
  • 自己手动创建DispatcherServlet

流程图:

Tomcat下SpringMVC的启动过程.drawio.png