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中的容器類能夠分爲兩大類。app

  • 一類是由BeanFactory接口定義的核心容器。BeanFactory位於整個容器類體系結構的頂端,其基本實現類爲DefaultListableBeanFactory。之因此稱其爲核心容器,是由於該類容器實現IOC的核心功能:好比配置文件的加載解析,Bean依賴的注入以及生命週期的管理等。BeanFactory做爲Spring框架的基礎設施,面向Spring框架自己,通常不會被用戶直接使用。框架

  • 另外一類則是由ApplicationContext接口定義的容器,一般譯爲應用上下文,不過稱其爲應用容器可能更形象些。它在BeanFactory提供的核心IOC功能之上做了擴展。一般ApplicationContext的實現類內部都持有一個BeanFactory的實例,IOC容器的核心功能會交由它去完成。而ApplicationContext自己,則專一於在應用層對BeanFactory做擴展,好比提供對國際化的支持,支持框架級的事件監聽機制以及增長了不少對應用環境的適配等。ApplicationContext面向的是使用Spring框架的開發者。開發中常用的ClassPathXmlApplicationContext就是典型的Spring的應用容器,也是要進行解讀的IOC容器。ide

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

  • 1.IOC模塊是整個Spring框架的核心,是實現其餘模塊的基礎。IOC容器在啓動時會註冊並初始化Spring框架的全部基礎組件,這些組件不只在IOC模塊中被用到,也會被AOP等模塊使用。於是熟悉IOC容器的啓動流程不只是掌握IOC模塊的關鍵,也是理解整個Spring框架的前提。函數

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

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

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

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容器通用性和擴展性的統一。

相關文章
相關標籤/搜索