Spring源碼分析專題 —— IOC容器啓動過程(上篇)

聲明
1.建議先閱讀《Spring源碼分析專題 —— 閱讀指引》
2.強烈建議閱讀過程當中要參照調用過程圖,每篇都有其對應的調用過程圖
3.寫文不易,轉載請標明出處html

前言

關於 IOC 容器啓動的內容不少,我將分上中下三篇講解,其中上篇相對簡單,中篇最爲複雜,請你們耐心閱讀。web

  • 上篇 - 主要是相關基礎說明和找到分析入口
  • 中篇 - 講解定位、加載、註冊的過程(實例化在依賴注入的章節再講)
  • 下篇 - 細節補充

調用過程圖

因爲篇幅問題,此處我只放個縮略圖,高清大圖請點擊連接☞ IOC容器啓動調用過程圖.jpg
請務必一邊對照圖片一邊閱讀文章。
spring

先放結論

此處先放結論,你們稍微記一記,後邊將展開詳解spring-mvc

  • Spring 的啓動流程主要是定位 -> 加載 -> 註冊 -> 實例化
    • 定位 - 獲取配置文件路徑
    • 加載 - 把配置文件讀取成 BeanDefinition
    • 註冊 - 存儲 BeanDefinition
    • 實例化 - 根據 BeanDefinition 建立實例
  • 所謂的IOC容器其實就是 BeanFactory , BeanFactory 是一個接口,有不少對應的實現類
  • IOC容器的關鍵入口方法是 refresh()
  • Web 應用中使用的容器是 XmlWebApplicationContext ,其類圖以下,能夠看出最終是一個實現了 BeanFactory 的類


IOC容器源碼的入口

咱們知道 Spring 框架不單單是面向 Web 應用,因此 Spring 中對應不一樣場景有許多 IOC 容器的實現類,其中有簡單的也有複雜的,在此咱們跳過簡單容器的講解,直接以咱們最熟悉、也是最感興趣的 Java Web 項目下手,尋找其對應的 IOC 容器實現類,同時一口氣尋找到 IOC 容器的關鍵入口方法 refresh() 。mvc

1. 尋找IOC容器實現類

如下是咱們熟知的 SpringMVC 項目中 web.xml 的基礎配置,其關鍵是要配置一個 ContextLoaderListener 和一個 DispatcherServletapp

<web-app>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring.xml</param-value>
    </context-param>

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

    <!-- DispatcherServlet -->
    <servlet>
        <description>spring mvc servlet</description>
        <servlet-name>springMvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
          <description>spring mvc</description>
          <param-name>contextConfigLocation</param-name>
          <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springMvc</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
</web-app>

咱們知道在 Java Web 容器中相關組件的啓動順序是 ServletContext -> listener -> filter -> servlet , listener 是優於 servlet 啓動的,因此咱們先看一看 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());
    }
}

根據 Java Web 容器的規範可知,當 Listener 啓動時會調用 contextInitialized 方法,而 ContextLoaderListener 中該方法的內容是繼續調用 initWebApplicationContext 方法,因而咱們再跟蹤 initWebApplicationContext
( ContextLoaderListener 是 ContextLoader 的子類,因此實際上是調用了父類的 initWebApplicationContext 方法)ide

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    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) {
                this.context = this.createWebApplicationContext(servletContext);
            }
    .
    .
    .
}

此處咱們關心的是 createWebApplicationContext 方法源碼分析

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {

    /** [note-by-leapmie] determineContextClass方法中獲取contextClass **/
    Class<?> contextClass = determineContextClass(sc);
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
        "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
    }
    /** [note-by-leapmie] 根據contextClass返回實例 */
    return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

從代碼可知,方法中的邏輯主要是調用 determineContextClass 獲取 contextClass ,而後根據 contextClass 建立 IOC 容器實例。因此, contextClass 的值將是關鍵。post

protected Class<?> determineContextClass(ServletContext servletContext) {
    String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
    if (contextClassName != null) {
        .
        .
        .
    }
    else {
        /**
         * [note-by-leapmie]
         * defaultStrategies的值是在本類中的static方法中注入的
         * 即該類加載過程當中defaultStrategies已經被賦值
         * 本類的開始部分有static代碼塊
         * **/
        contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
        try {
            return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
        }
        catch (ClassNotFoundException ex) {
            throw new ApplicationContextException(
                    "Failed to load default context class [" + contextClassName + "]", ex);
        }
    }
}

能夠看到, contextClassName 是從 defaultStrategies 中獲取的,而關於 defaultStrategies 的賦值須要追溯到 ContextLoader 類中的靜態代碼塊

static {
    try {
        /**
         * [note-by-leapmie]
         * DEFAULT_STRATEGIES_PATH的值是ContextLoader.properties
         */
        ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
        defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
    }
    catch (IOException ex) {
        throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
    }
}

defaultStrategies 是從 resource 中獲取的參數,而 resource 又是從 DEFAULT_STRATEGIES_PATH 中獲取,查看可知 DEFAULT_STRATEGIES_PATH 的值是 ContextLoader.properties ,經過全局查找到ContextLoader.properties文件,其中內容以下

org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

由此可知, SpringMVC 項目中使用到的 IOC 容器類型是 XmlWebApplicationContext。

2. 尋找關鍵入口方法refresh()

咱們回到 ContextLoader 的 initWebApplicationContext 方法,前邊咱們說到調用 createWebApplicationContext 方法建立容器,容器建立後咱們關注的下一個方法是 configureAndRefreshWebApplicationContext

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    .
    .
    .
    try {
        // Store context in local instance variable, to guarantee that
        // it is available on ServletContext shutdown.
        if (this.context == null) {
            /** [note-by-leapmie] 獲取SpringIOC容器類型 **/
            this.context = createWebApplicationContext(servletContext);
        }
        if (this.context instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
            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 ->
                    // determine parent for root web application context, if any.
                    ApplicationContext parent = loadParentContext(servletContext);
                    cwac.setParent(parent);
                }
                /** [note-by-leapmie] 配置和刷新容器 **/
                configureAndRefreshWebApplicationContext(cwac, servletContext);
            }
        }
    .
    .
    .
}

configureAndRefreshWebApplicationContext的代碼以下

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
    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
        String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
        if (idParam != null) {
            wac.setId(idParam);
        }
        else {
            // Generate default id...
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                    ObjectUtils.getDisplayString(sc.getContextPath()));
        }
    }

    wac.setServletContext(sc);
    String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
    if (configLocationParam != null) {
        wac.setConfigLocation(configLocationParam);
    }

    // The wac environment's #initPropertySources will be called in any case when the context
    // is refreshed; do it eagerly here to ensure servlet property sources are in place for
    // use in any post-processing or initialization that occurs below prior to #refresh
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
    }

    customizeContext(sc, wac);
    /** [note-by-leapmie] 調用容器的refresh()方法,此處wac對應的類是XmlWebApplicationContext **/
    wac.refresh();
}

在這裏咱們要關注的是最後一行 wac.refresh() ,意思是調用容器的 refresh() 方法,此處咱們的容器是XmlWebApplicationContext,對應的 refresh() 在其父類 AbstractApplicationContext

@Override
/** 核心過程 **/
public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        // Prepare this context for refreshing.
        prepareRefresh();

        // Tell the subclass to refresh the internal bean factory.
        /**
         * obtainFreshBeanFactory方法中會調用loadBeanDefinition方法,用於加載bean的定義
         */
        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.
            /** 初始化全部非lazy-init的bean **/
            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();
        }
    }
}

至此咱們已經找到了關鍵的入口 refresh() ,咱們看一下在調用過程圖中咱們所處的位置

refresh 方法是 Spring IOC 容器啓動過程的核心方法,方法中按順序調用了好幾個命名清晰的方法,其對應的都是 IOC 容器啓動過程的關鍵步驟,更多的細節咱們將在下一節繼續講解。

話癆一下
你們可能會以爲,在源碼分析過程當中一個方法中調用了不少方法,例如先執行方法 a() ,再執行方法 b() ,爲何咱們直接看方法 b() 而跳過了方法 a() ?
在這裏我想說的是,Spring的源碼量很龐大,若是每一個細節都去了解可能一年過去了都看不完,咱們應該先關注大流程,其餘的細枝末節能夠在瞭解了大流程後再慢慢深刻了解。
至於爲何是看方法 b() 而跳過方法 a() ,這些都是前人總結的經驗與心血,在學習過程當中我也是跟着別人的步伐在源碼中探索,中間有些缺失的路線我也花費大量時間去踩坑,最後繪製了每一份調用過程圖。在本專題中我能確保的是,只要跟着個人步伐,大家不會在源碼分析的路上迷路。


本文首發地址:https://blog.leapmie.com/archives/390/

[目錄]
[上一篇]Spring源碼分析專題 —— 閱讀指引
[下一篇]Spring源碼分析專題 —— IOC容器啓動過程(中篇)

相關文章
相關標籤/搜索