• 详细解读 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配置与启动

  • 相关阅读:
    Scala程序设计(第2版)
    会声会影X6 DV影片制作--编辑--刻盘实战从入门到精通
    Perl进阶(第2版)
    网络知识与应用
    「C」 函数、运算、流程控制
    OC中属性readwrite,readonly,assign,retain,copy,nonatomic 各是什么作用,在那种情况下用?
    OC-nonatomic和atomic相关
    C++之类与对象(1)
    C++对C语言的非面向对象特性扩充(3)
    C++对C语言的非面向对象特性扩充(2)
  • 原文地址:https://www.cnblogs.com/kendoziyu/p/DispatcherServlet-load-on-startup.html
Copyright © 2020-2023  润新知