1. 程式人生 > >Spring容器啟動原始碼解析

Spring容器啟動原始碼解析

1. 前言

  最近搭建的工程都是基於SpringBoot,簡化配置的感覺真爽。但有個以前的專案還是用SpringMvc寫的,看到滿滿的配置xml檔案,卻有一種想去深入瞭解的衝動。折騰了好幾天,決心去寫這篇關於Spring啟動的部落格,自己是個剛入職的小白,技術水平有限,也是硬著頭皮看原始碼去Debug,很多不懂的地方還請諒解!

2. 概述

  先給出幾個讓我頭皮發麻的概念:web容器,Spring容器,SpringMvc容器

  容器就是管理物件的地方,例如web容器就是管理servlet的地方,Spring容器就是管理Service,dao等Bean的地方,SpringMvc就是管理Controller等bean的地方(下文會做解釋)。一個SpringMvc專案的啟動離不開上述三個容器。所以這就是這篇文章的講點,各個容器的啟動過程解析。

3. Web容器初始化過程

  官方文件是對於Web容器初始化時是這樣描述的(英文不懂,已翻譯成中文)

  1. 部署描述檔案(web.xml)中的<listener>標記的監聽器會被建立和初始化

  2. 對於實現了ServletContextListener的監聽器,會執行它的初始化方法 contextInitialized()

  3. 部署描述檔案中的<filter>標記的過濾器會被建立和初始化,呼叫其init()方法

  4. 部署描述檔案中的<servlet>標記的servlet會根據<load-on-startup>中的序號建立和初始化,呼叫init()方法

  

 

 

   大致流程瞭解之後,結合自己的SpringMvc專案一步步深入,先貼一下基本的web.xml檔案

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:web="http://java.sun.com/xml/ns/javaee" 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" id="WebApp_ID" version="2.5">
  <display-name>dmpserver</display-name>
  <welcome-file-list>
    <welcome-file>login.jsp</welcome-file>
  </welcome-file-list>
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring.xml</param-value>
  </context-param>
  <context-param>
    <param-name>log4jConfigLocation</param-name>
    <param-value>classpath:log4jConfig.xml</param-value>
  </context-param>

  <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>
  </filter>
  <filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>  
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
   <listener>
    <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
  </listener>
  <servlet>
    <description>spring mvc servlet</description>
    <servlet-name>rest</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:spring-mvc.xml
        </param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
 
</web-app>

  1. 容器會先解析<context-param>中的鍵值對(上述程式碼重點關注Spring配置檔案Spring.xml)

  2. 容器建立一個application內建物件servletContext(可以理解為servlet上下文或web容器),用於全域性變數共享

  3. 將解析的<context-param>鍵值對存放在application即servletContext中

  4. 讀取<listener>中的監聽器,一般會使用ContextLoaderListener類,呼叫其contextInitialized方法,建立IOC容器(Spring容器)webApplicationContext。將webApplication容器放入application(servlet上下文)中作為根IOC容器,鍵名為WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE    注意的是,webApplicationContext是全域性唯一的,一個web應用只能有一個根IOC容器。因為這個根IOC容器是讀取<context-param>配置的鍵值對來建立Bean,這個根IOC容器只能訪問spring.xml中配置的Bean,我們在Spring.xml中一般配置的是service,dao等Bean。所以根IOC容器(Spring容器)只能管理service,dao等Bean

  5. listener載入完畢後,載入filter過濾器

  6. 載入servlet,一般springMvc專案中會優先載入 DispatcherServlet(現在開始載入SpringMvc容器了)

  7. DispatcherServlet的父類FrameworkServlet重寫了其父類的initServletBean()方法,在初始化時呼叫initWebApplicationContext()方法和onRefresh()方法

  8. initWebApplicationContext()方法會在servletContext(即當前servlet上下文)建立一個子IOC容器(即SpringMvc容器),如果存在上述的根IOC容器,就設定根IOC容器作為父容器,如果不存在,就將父容器設定為NULL

  9. 讀取<servlet>標籤的<init-param>配置的xml檔案並載入相關Bean。此時載入的是Spring-mvc.xml配置檔案,管理的是Controller等Bean

   10.   onRefresh()載入其他元件

4. 啟動過程分析

4.1 listener初始化Spring容器

  tomcat啟動後,<context-param>標籤的內容讀取後會被放進application中,做為Web應用的全域性變數使用,接下來建立listener時會使用到這個全域性變數,因此,Web應用在容器中部署後,進行初始化時會先讀取這個全域性變數,之後再進行上述講解的初始化啟動過程。

  檢視ContextLoaderListener原始碼

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    public ContextLoaderListener() {
    }

    public ContextLoaderListener(WebApplicationContext context) {
        super(context);
    }

    public void contextInitialized(ServletContextEvent event) {
        this.initWebApplicationContext(event.getServletContext());
    }

    public void contextDestroyed(ServletContextEvent event) {
        this.closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
    }
}

  據官方文件說明,實現ServletContextListener介面,執行contextInitialized(),進入initWebApplicationContext方法。contextInitialized()和contextDestroyed()方法會在web容器啟動或銷燬時執行。網上查了下此處設計模式用到的是觀察者模式和代理模式,自己也不懂就不做詳解了

  檢視ContextLoader.class中的initWebApplicationContext方法

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
        /*
        首先通過WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
        這個String型別的靜態變數獲取一個根IoC容器,根IoC容器作為全域性變數
        儲存在application物件中,如果存在則有且只能有一個
        如果在初始化根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!");
        } else {
            Log logger = LogFactory.getLog(ContextLoader.class);
            servletContext.log("Initializing Spring root WebApplicationContext");
            if (logger.isInfoEnabled()) {
                logger.info("Root WebApplicationContext: initialization started");
            }

            long startTime = System.currentTimeMillis();

            try {
                if (this.context == null) {
                    // 建立一個根IOC容器
                    this.context = this.createWebApplicationContext(servletContext);
                }

                if (this.context instanceof ConfigurableWebApplicationContext) {
                    ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)this.context;
                    if (!cwac.isActive()) {
                        if (cwac.getParent() == null) {
                            // 為根IOC容器設定一個父容器
                            ApplicationContext parent = this.loadParentContext(servletContext);
                            cwac.setParent(parent);
                        }

                        this.configureAndRefreshWebApplicationContext(cwac, servletContext);
                    }
                }
                //將建立好的IoC容器放入到application物件中,並設定key為WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
                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.isDebugEnabled()) {
                    logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
                }

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

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

  initWebApplicationContext方法的主要目的是建立一個根IOC容器,並放入servlet上下文中。看上述原始碼可知,根IOC容器只能僅有一個,作為全域性變數儲存在servletContext中。將根IoC容器放入到application物件之前進行了IoC容器的配置和重新整理操作,呼叫了configureAndRefreshWebApplicationContext()方法,該方法原始碼如下:

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
        String configLocationParam;
        if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
            configLocationParam = sc.getInitParameter("contextId");
            if (configLocationParam != null) {
                wac.setId(configLocationParam);
            } else {
                wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath()));
            }
        }

        wac.setServletContext(sc);
        /*
            在容器啟動時,會把<context-param>中的內容放入servlet上下文的全域性變數中,
            此時獲取key為contextConfigLocation的變數,及Spring.xml配置檔案
            將其放入到webApplicationContext中
        */
        configLocationParam = sc.getInitParameter("contextConfigLocation");
        if (configLocationParam != null) {
            wac.setConfigLocation(configLocationParam);
        }

        ConfigurableEnvironment env = wac.getEnvironment();
        if (env instanceof ConfigurableWebEnvironment) {
            ((ConfigurableWebEnvironment)env).initPropertySources(sc, (ServletConfig)null);
        }

        this.customizeContext(sc, wac);
        wac.refresh();
    }

  configureAndRefreshWebApplicationContext方法比較重要的是把配置檔案資訊放入根IOC容器中。方法最後呼叫了refresh()方法,對配置檔案資訊(Bean)進行載入。因為refresh()這是個ConfigurableApplication-Context介面方法,想到了它的常用實現類ClassPathXmlApplicationContext,一層層進去找到了Abstract-ApplicationContext,實現了refresh(),見如下原始碼:

public void refresh() throws BeansException, IllegalStateException {
        Object var1 = this.startupShutdownMonitor;
        synchronized(this.startupShutdownMonitor) {
            this.prepareRefresh();
            ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
            this.prepareBeanFactory(beanFactory);

            try {
                this.postProcessBeanFactory(beanFactory);
                this.invokeBeanFactoryPostProcessors(beanFactory);
                this.registerBeanPostProcessors(beanFactory);
                this.initMessageSource();
                this.initApplicationEventMulticaster();
                this.onRefresh();
                this.registerListeners();
                this.finishBeanFactoryInitialization(beanFactory);
                this.finishRefresh();
            } catch (BeansException var9) {
                if (this.logger.isWarnEnabled()) {
                    this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
                }

                this.destroyBeans();
                this.cancelRefresh(var9);
                throw var9;
            } finally {
                this.resetCommonCaches();
            }

        }
    }

  該方法主要用於建立並初始化contextConfigLocation類配置的xml檔案中的Bean,因此,如果我們在配置Bean時出錯,在Web應用啟動時就會丟擲異常,而不是等到執行時才丟擲異常。因為技術能力有限加上此處方法太多,就不在一一解析了。到此為止,整個Spring容器載入完畢,下面開始載入SpringMVC容器

4.2 Filter初始化

  因為Filter的操作沒有涉及IOC容器,此處不做詳解,上面web.xml中配置的是一個UTF8編碼過濾器

5. 總結

  時間有限,只大致介紹了Spring容器的初始化,後面還沒來得及整理,對於springMvc容器的建立和初始化下篇文章見