1. 程式人生 > >Spring IOC容器啟動流程原始碼解析(一)——容器概念詳解及原始碼初探

Spring IOC容器啟動流程原始碼解析(一)——容器概念詳解及原始碼初探

目錄

1. 前言

1.1 IOC容器到底是什麼

IOC和AOP是Spring框架的核心功能,而IOC又是AOP實現的基礎,因而可以說IOC是整個Spring框架的基石。那麼什麼是IOC?IOC即控制反轉,通俗的說就是讓Spring框架來幫助我們完成物件的依賴管理和生命週期控制等等工作。從面向物件的角度來說,具有這種行為,完成這種工作的主體就可以形象的稱之為IOC容器。從程式碼角度來看,IOC容器不過是Spring中定義的具有IOC基本功能的一些類的統稱,這些類都遵循一些共同的介面規範,所以我們可以說實現某些介面的具體的實現類就是IOC容器。而IOC容器的啟動流程,說白了就是建立並初始化一個該實現類的例項的過程,在這個過程中要進行諸如配置檔案的載入解析,核心元件的註冊,bean 例項的建立等一系列繁瑣複雜的操作,因而整個過程顯得相對漫長,邏輯也相對複雜。

1.2 BeanFactory和ApplicationContext的聯絡以及區別

前面說到Spring中為容器類定義了一些介面規範,如下圖所示

具體而言,Spring中的容器類可以分為兩大類,一類是由BeanFactory介面定義的核心容器。BeanFactory位於整個容器類體系結構的頂端,其基本實現類為DefaultListableBeanFactory。之所以稱其為核心容器,是因為該類容器實現IOC的核心功能:比如配置檔案的載入解析,Bean依賴的注入以及生命週期的管理等。BeanFactory作為Spring框架的基礎設施,面向Spring框架本身,一般不會被使用者直接使用。 另一類則是由ApplicationContext介面定義的容器,通常譯為應用上下文,不過稱其為應用容器可能更形象些。它在BeanFactory提供的核心IOC功能之上作了擴充套件。通常ApplicationContext的實現類內部都持有一個BeanFactory的例項,IOC容器的核心功能會交由它去完成。而ApplicationContext本身,則專注於在應用層對BeanFactory作擴充套件,比如提供對國際化的支援,支援框架級的事件監聽機制以及增加了很多對應用環境的適配等。ApplicationContext面向的是使用Spring框架的開發者。開發中經常使用的ClassPathXmlApplicationContext就是典型的Spring的應用容器,也是標題中所指的IOC容器。

1.3 解讀IOC容器啟動流程的意義

  • 1.IOC模組是整個Spring框架的核心,是實現其他模組的基礎。IOC容器在啟動時會註冊並初始化Spring框架的所有基礎元件,這些元件不僅在IOC模組中被用到,也會被AOP等模組使用。因而熟悉IOC容器的啟動流程不僅是掌握IOC模組的關鍵,也是理解整個Spring框架的前提。

  • 2.Spring是個很靈活的框架,允許使用者在原有功能上進行擴充套件或者進行滿足業務需求的個性化設定,比如對容器和Bean的生命週期過程進行增強,進行事件監聽等等。要更好的使用Spring的這些特性,必須瞭解其工作原理,而答案就在IOC容器的啟動過程中。

  • 3.Spring框架在實現時使用了大量的設計模式,體現了很多優秀的設計思想。其IOC容器的啟動原始碼就是供開發者學習這種設計經驗的絕佳樣板。

長求總:為了更好的理解和使用Spring框架並從它優秀的設計和實現經驗中進行學習。

1.4 如何有效的閱讀原始碼

Spring框架經過多年的發展,隨著功能特性的增加,其實現也越來越複雜和抽象,要徹底弄清楚框架實現的每一個細節並不是一件簡單的事。因而,對於Spring原始碼的解讀,不必死摳每個方法和實現細節,這樣太浪費時間,畢竟對於絕大分開發者而言,閱讀Spring原始碼並不是為了成為Spring框架的開發者,而是為了更好的理解和使用Spring框架,或者從更高的角度,學習Spring的設計經驗和思想,並將其運用到自己的專案實踐中。 由於Spring容器的啟動流程十分冗長,內容實在太多,全部放在一篇進行講解實在太臃腫,也十分影響閱讀體驗。因而採取化整為零的策略,將整個IOC容器的啟動流程劃分為若干個階段,每篇只對其中一個階段進行詳細講解,因而對於容器啟動原始碼的解讀,主要抓住以下兩個要點:

  • 1.對容器啟動流程的梳理 容器啟動流程分為哪幾個階段,在每個階段容器做了哪些工作,初始化了哪些元件,執行了哪些使用者自定義的回撥函式。

  • 2.對設計模式和設計思想的學習 在實現這個功能時採用了哪些設計模式,遵循了哪些設計思想,這麼做有哪些好處。

2. 初探IOC容器啟動原始碼

本次原始碼閱讀的Spring版本為4.3.10.RELEASE。

啟動Spring容器,本質上是建立並初始化一個具體的容器類的過程,以常見的容器類ClassPathXmlApplicationContext為例,啟動一個Spring容器可以用以下程式碼表示

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

儘管只有短短的一行程式碼,但已經建立並啟動了一個Spring的IOC容器。為了後面更好的理解,先來看下ClassPathXmlApplicationContext的類繼承結構

關鍵的幾個類已經用紅色箭頭標註了出來。

  • AbstractApplicationContext ApplicationContext介面的抽象實現類,能夠自動檢測並註冊各種後置處理器(PostProcessor)和事件監聽器(Listener),以模板方法模式定義了一些容器的通用方法,比如啟動容器的真正方法refresh()就是在該類中定義的。

  • AbstractRefreshableApplicationContext 繼承AbstractApplicationContext的抽象類。內部持有一個DefaultListableBeanFactory 的例項,使得繼承AbstractRefreshableApplicationContext的Spring的應用容器內部預設有一個Spring的核心容器,那麼Spring容器的一些核心功能就可以委託給內部的核心容器去完成。AbstractRefreshableApplicationContext在內部定義了建立,銷燬以及重新整理核心容器BeanFactory的方法。

  • ClassPathXmlApplicationContext 最常用的Spring的應用容器之一。在啟動時會載入類路徑下的xml檔案作為容器的配置資訊。

下面就正式開始容器啟動流程的原始碼閱讀 進入ClassPathXmlApplicationContext的構造方法,首先呼叫了過載建構函式

/**
 * Create a new ClassPathXmlApplicationContext, loading the definitions
 * from the given XML file and automatically refreshing the context.
 * @param configLocation resource location
 * @throws BeansException if context creation failed
 */
public ClassPathXmlApplicationContext(String configLocation) throws BeansException {
   this(new String[] {configLocation}, true, null);
}

這裡有兩點需要注意下:

  • 1.建立ClassPathXmlApplicationContext時需要指定xml檔案的路徑作為引數,儘管我們在建立時只指定了一個,但其實可以同時指定多個。
  • 2.Spring容器有父子容器的概念,通過HierarchicalBeanFactory介面定義了具有層級關係的容器體系。而在抽象實現類AbstractApplicationContext類的內部,有一個表示父容器的成員變數
/** Parent context */
private ApplicationContext parent;

過載函式的第三個引數即表示要建立的ClassPathXmlApplicationContext的父容器,不過這裡只需要設定為null。關於Spring的父子容器,還有一些獨特的訪問規則,子容器可以訪問父容器中的Bean,父容器不可以訪問子容器中的Bean。不知道這個規則在使用Spring做web開發時可能會碰到一些匪夷所思的問題。

繼續跟進原始碼

//設定父容器
super(parent);
//設定xml檔案的路徑引數
setConfigLocations(configLocations);
if (refresh) { //預設為true
    //啟動Spring容器
    refresh();
}

設定完父容器和xml檔案的路徑資訊後,終於看到了refresh()方法,正如前面提到的,這是真正啟動Spring容器的方法,想要知道Spring IOC容器的啟動流程,就要知道該方法內部都做了什麼。

2.1 啟動容器的真正入口refresh()

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.
      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();
      }
   }
}

啟動容器的方法之所以用refresh(重新整理)來命名,是為了形象的表達容器可以被重啟這層含義。為了防止併發環境下多個執行緒同時啟動IOC容器,整個過程使用同步程式碼塊來進行同步。容器的啟動從方法內容上來看並不複雜,流程也十分清晰,從方法名上大概就可以猜到每一步做了什麼。

2.2 容器啟動流程的不同階段

為了更好的進行講解,可以將容器啟動的整個流程劃分為以下五個階段

3 容器啟動前的準備工作

容器啟動前的準備工作定義在下面的方法中

prepareRefresh();

進去一探究竟

/**
 * Prepare this context for refreshing, setting its startup date and
 * active flag as well as performing any initialization of property sources.
 */
protected void prepareRefresh() {
    //記錄容器的啟動時間
    this.startupDate = System.currentTimeMillis();
    //將容器的關閉標誌置位false
    this.closed.set(false);
    //將容器的啟動標記置位true
    this.active.set(true);

    if (logger.isInfoEnabled()) {
        logger.info("Refreshing " + this);
    }

    // Initialize any placeholder property sources in the context environment
    //空實現的鉤子方法,供子類重寫
    initPropertySources();

    // Validate that all properties marked as required are resolvable
    // see ConfigurablePropertyResolver#setRequiredProperties
    //對必須的系統環境變數進行校驗,如果不存在將丟擲異常
    getEnvironment().validateRequiredProperties();

    // Allow for the collection of early ApplicationEvents,
    // to be published once the multicaster is available...
    this.earlyApplicationEvents = new LinkedHashSet<ApplicationEvent>();
}

首先記錄了容器的啟動時間和對容器的狀態進行了標記。之後來到了容器為使用者提供的第一個擴充套件點:

initPropertySources();
protected void initPropertySources() {
   // For subclasses: do nothing by default.
}

這是一個預設空實現的鉤子方法,使用者在自定義IOC容器時可以重寫,完成一些環境變數屬性的初始化工作。 之後會對一些必要的環境變數資訊進行校驗

getEnvironment().validateRequiredProperties();

如果必須的環境變數資訊不存在,則會丟擲異常

@Override
public void validateRequiredProperties() {
   MissingRequiredPropertiesException ex = new MissingRequiredPropertiesException(); //異常資訊集合
   for (String key : this.requiredProperties) {
      if (this.getProperty(key) == null) {
         ex.addMissingRequiredProperty(key); //加入異常資訊
      }
   }
   if (!ex.getMissingRequiredProperties().isEmpty()) {
      throw ex;  //丟擲異常資訊集合
   }
}

結合前面的鉤子initPropertySources(),使用者在自定義IOC容器時可以完成一些個性化需求,比如要求容器在啟動時必須從環境變數中載入某屬性值,若該屬性值不存在則啟動失敗。重寫initPropertySources()如下

@Override
protected void initPropertySources() {
    getEnvironment().setRequiredProperties("XXXX");
}

若環境變數不存在則會丟擲以下異常

總結下容器啟動前的準備工作:主要是對容器狀態進行標記,初始化環境變數資訊並對必須要的環境變數進行校驗。

4. 總結

這篇文章的主要內容

  • 1.講解IOC容器的概念和類結構
  • 2.找到容器啟動流程的真正入口refresh()方法,將容器啟動流程劃分為了5個階段:啟動前的準備階段,初始化核心容器階段,初始化基礎元件階段,建立單例項bean階段以及容器啟動的收尾階段
  • 3.對容器啟動前的準備階段進行了原始碼解讀

可以看到容器啟動原始碼中對模板方法模式的合理運用。容器啟動的流程以模板方法模式定義在了抽象容器類AbstractApplicationContext中,並留下了鉤子函式供子類重寫。使用者實現自定義容器時,可以通過繼承並重寫鉤子函式的方法對原有容器的功能進行擴充套件,而無需多做其他改動。這樣既為使用者擴充套件Spring容器開放了介面,又為使用者遮蔽了容器實現的複雜性,很好的實現了Spring容器通用性和擴充套件性的統一。