详细解读 DispatcherServlet 初始化过程(带时序图)

写在前面

接着上一篇文章 从源码分析 ContextLoaderListener 的加载过程(带时序图),我们为了解决定时任务执行两次的问题,我们继续 DispatcherServlet 初始化过程的研究。

Servlet 初始化概述

在说 DispatcherServlet 之前,我们先要知道一个 Java J2EE Servlet 的接口的 init(ServletConfig config) 方法。Servlet 容器调用 Servlet # init(ServletConfig config) 方法来指示正在初始化某一个具体的 Servlet 实例对象。

Servlet 容器读取到 webapp/WEB-INF/web.xml 文件中的 <servlet /> 标签时,会根据其中的 <load-on-startup > 的值做不同的处理。如下图所示:

关于容器加载某个 servlet 时机的选择:

A. 如果没有指定 <load-on-startup /> 容器在该 servlet 被选择时才加载。

B. 如果指定值 < 0, 情况同 A

C. 如果指定值 >= 0, 容器在 Web 应用启动时就加载该 servlet

容器在启动时,初始化多个 servlet 的优先级顺序:

1.首先加载 Servlet 容器自带的 servlet

2.然后优先加载 <load-on-startup> 为较小自然数的 servlet

3.相同 <load-on-startup> 值,优先加载 <web-app> 标签中更靠前的 servlet

FrameworkServlet 初始化过程

第一,从继承关系上来看,GenericServletFrameworkServlet 的超类,FrameworkServletDispatcherServlet 超类。

1. Servlet 容器主动调用 Servlet 实现类 GenericServlet 的 init 方法:

public abstract class GenericServlet implements Servlet {
      /**
       * Called by the servlet container to indicate to a servlet that the
       * servlet is being placed into service. 
       */
      public void init(ServletConfig config) throws ServletException {
            this.config = config;
            this.init();
      }
}
  

2. 调用 HttpServletBean 的 init 方法

public abstract class HttpServletBean extends HttpServlet {
        /**
         * 覆写了父类 GenericServlet#init() 方法
         */
	@Override
	public final void init() throws ServletException {
		// Set bean properties from init parameters.
		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);
                                /**
                                 * 如果你有设置 init-param 'contextConfigLocation',那么就会调用 DispatcherServlet#setContextConfigLocation 方法
                                 * 如果你有设置 init-param 'contextClass',就会调用 DispatcherServlet#setContextClass 方法
                                 * 如果你有设置 init-param 'contextInitializerClasses',就会调用 DispatcherServlet#setContextInitializerClasses 方法
                                 */
				bw.setPropertyValues(pvs, true);
			}
			catch (BeansException ex) {
				if (logger.isErrorEnabled()) {
					logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
				}
				throw ex;
			}
		}
		// Let subclasses do whatever initialization they like.
		initServletBean();
	}
}
  

3.FrameworkServlet # initWebApplicationContext

了解过 ContextLoaderListener 的朋友,应该熟悉 <context-param /> + <listener /> 的这套常规“组合拳”。

<web-app ....(此处省略命名空间)>
    <!--配置多个上下文会导致多次执行-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring/spring-mvc.xml</param-value>
    </context-param>

    <!-- ================================== listener ================================== -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
</web-app>

他们将为我们的 Web 应用程序创建一个“根”应用上下文。

设置 Web 应用“根”上下文的地方: ContextLoader # initWebApplicationContext

ContextLoader # initWebApplicationContext(ServletContext servletContext) 方法中,

把创建好的 WebApplicationContext 实例,通过调用 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context) 设置到 Servlet 容器上下文中。

键值为 org.springframework.web.context.WebApplicationContext.ROOT


获取 Web 应用“根”上下文的地方: FrameworkServlet # initWebApplicationContext

FrameworkServlet # initWebApplicationContext() 方法中, 调用 WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext()) 获取 Web 应用“根”上下文

4.为当前 DispatcherServlet 创建一个应用上下文

FrameworkServlet # initWebApplicationContext 方法中,

假如当前的 DispatcherServlet 还没有一个 webApplicationContext 成员变量,

调用 createWebApplicationContext 给自己创建一个 WebApplicationContext 实例。

5.实例化应用上下文对象

  • 选择应用上下文的类对象
// 获取需要实例化的应用上下文的类对象
Class<?> contextClass = getContextClass();
  • 这个类对象默认是 XmlWebApplicationContext.class,也可以通过 <init-param> 进行自定义指定。
<init-param>
    <param-name>contextClass</param-name>
    <param-value>org.springframework.web.context.support.XmlWebApplicationContext</param-value>
</init-param>
  • 类似的,还可以配置 contextConfigLocation
<init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring/spring-mvc.xml</param-value>
</init-param>

6.配置和初始化应用上下文对象

  • 但是如果你选择不填入路径,那么不会影响应用上下文的创建,但是无法加载 Bean。
<init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value></param-value>
</init-param>

因为 FrameworkServlet # createWebApplicationContext 对 contextConfigLocation 的判断是非 null

String configLocation = getContextConfigLocation();
// 值为空字符"",但不是 null,所以可以设置进去
if (configLocation != null) {
      wac.setConfigLocation(configLocation);
}
AbstractRefreshableConfigApplicationContext # setConfigLocation

// 传入空字符串"",得到的是长度为0的String数组
public void setConfigLocations(@Nullable String... locations) {
    if (locations != null) {
        Assert.noNullElements(locations, "Config locations must not be null");
        this.configLocations = new String[locations.length];
        for(int i = 0; i < locations.length; ++i) {
            this.configLocations[i] = this.resolvePath(locations[i]).trim();
        }
    } else {
        this.configLocations = null;
    }
}

XmlWebApplicationContext # loadBeanDefinitions

protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException {
	String[] configLocations = getConfigLocations();
	if (configLocations != null) {
                // 配置数组长度为0时,不会进入函数体内部,就不会执行 reader.loadBeanDefinitions
		for (String configLocation : configLocations) {
			reader.loadBeanDefinitions(configLocation);
		}
	}
}

7.刷新上下文

AbstractApplicationContext # refresh

public void refresh() throws BeansException, IllegalStateException {
	synchronized (this.startupShutdownMonitor) {
		// Prepare this context for refreshing.
		prepareRefresh();
		// Tell the subclass to refresh the internal bean factory.
		ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
		// Prepare the bean factory for use in this context.
		prepareBeanFactory(beanFactory);
		try {
			// Allows post-processing of the bean factory in context subclasses.
			postProcessBeanFactory(beanFactory);
			// Invoke factory processors registered as beans in the context.
			invokeBeanFactoryPostProcessors(beanFactory);
			// Register bean processors that intercept bean creation.
			registerBeanPostProcessors(beanFactory);
			// Initialize message source for this context.
			initMessageSource();
			// Initialize event multicaster for this context.
			initApplicationEventMulticaster();
			// Initialize other special beans in specific context subclasses.
			onRefresh();
			// Check for listener beans and register them.
			registerListeners();
			// Instantiate all remaining (non-lazy-init) singletons.
			finishBeanFactoryInitialization(beanFactory);
			// Last step: publish corresponding event.
			finishRefresh();
		}
		catch (BeansException ex) {
			if (logger.isWarnEnabled()) {
				logger.warn("Exception encountered during context initialization - " +
						"cancelling refresh attempt: " + ex);
			}
			// Destroy already created singletons to avoid dangling resources.
			destroyBeans();
			// Reset 'active' flag.
			cancelRefresh(ex);
			// Propagate exception to caller.
			throw ex;
		}
		finally {
			// Reset common introspection caches in Spring's core, since we
			// might not ever need metadata for singleton beans anymore...
			resetCommonCaches();
		}
	}
}

8.完成刷新发布事件

AbstractApplicationContext # finishRefresh

protected void finishRefresh() {
	// Clear context-level resource caches (such as ASM metadata from scanni
	clearResourceCaches();
	// Initialize lifecycle processor for this context.
	initLifecycleProcessor();
	// Propagate refresh to lifecycle processor first.
	getLifecycleProcessor().onRefresh();
        // 这个事件会被 FrameworkServlet 接收和处理
	// Publish the final event.
	publishEvent(new ContextRefreshedEvent(this));
	// Participate in LiveBeansView MBean, if active.
	LiveBeansView.registerApplicationContext(this);
}

注册事件的位置:FrameworkServlet # configureAndRefreshWebApplicationContext

wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

接收和处理事件的位置:FrameworkServlet.ContextRefreshListener

private class ContextRefreshListener implements ApplicationListener {
	@Override
	public void onApplicationEvent(ContextRefreshedEvent event) {
		FrameworkServlet.this.onApplicationEvent(event);
	}
}

到此为止,上半篇就算结束了,通过最后一步的事件接收,我们将进入 DispatcherServlet # initStrategies

DispatcherServlet 初始化过程

DispatcherServlet # initStrategies

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

首先了解一下默认配置 defaultStrategies:

public class DispatcherServlet extends FrameworkServlet {
      private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";
      
      private static final Properties defaultStrategies;

      static {
            // Load default strategy implementations from properties file.
            // This is currently strictly internal and not meant to be customized
            // by application developers.
            try {
                  ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
                  defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
            }
            catch (IOException ex) {
                  throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
            }
      }
}
DispatcherServlet.properties

该配置文件和 DispatcherServlet.class 在同一文件夹下,key 为接口类名称,value 为逗号分隔的实现类名称


我简单说一下常用的几个初始化方法:

  • initHandlerMappings url模式和“Handler”的映射关系。
  • initHandlerAdapters 初始化“Handler”适配器
  • initHandlerExceptionResolvers “Handler”执行发生异常的异常解析器
  • initViewResolvers 解析 View 对象的视图解析器

结语

  • 如果配置了监听器 ContextLoaderListener ,那么会创建一个 “root WebApplicationContext”。
  • 如果配置了多个 FrameworkServlet 及其子类,会为它们中的每一个创建一个 “local WebApplicationContext”
  • <context-param> 和 <init-param>(<servlet>标签的子标签)分别可以设置不同范围的 WebApplicationContext 的 contextId,contextClass,contextConfigLocation 属性。
  • ContextLoader 和 FrameworkServlet 的 initWebApplicationContext 方法,都是主要分为 “选择类对象与实例化” 和 “配置与刷新” 两部分

在整理本文的过程中,又带出了 ApplicationListener 是如何工作的?以及 BeanWrapper 是如何工作的?DispatcherServlet 初始化的这些 HandlerMapping,HandlerAdapter,HandlerExceptionResolves,ViewResolvers 是怎么串起来?等等这些疑问,待以后再继续分析。

参考文献

【Spring MVC】DispatcherServlet详解(容器初始化超详细过程源码分析)

servlet的init方法初始化方式:“Servlet 初始化概述”参考了这篇文章

第三章 DispatcherServlet详解 ——跟开涛学SpringMVC

springMVC servlet配置与启动

原文地址:https://www.cnblogs.com/kendoziyu/p/DispatcherServlet-load-on-startup.html