DispatcherServlet 初始化过程

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

image.png

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

image.png

关于容器加载某个 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 初始化过程

image.png

第一,从继承关系上来看,GenericServlet 是 FrameworkServlet 的超类,FrameworkServlet 是 DispatcherServlet 超类。

image.png
  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

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

  • 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 是怎么串起来?待以后再继续分析。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,490评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,581评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,830评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,957评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,974评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,754评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,464评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,847评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,995评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,137评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,819评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,482评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,023评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,149评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,409评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,086评论 2 355

推荐阅读更多精彩内容