Spring 深入——IoC 容器 02

IoC容器的實現學習——02

回顧

前面學習了 IoC 模式的核心概念,使用場景,以及 Spring 對 IoC

具體實現的兩種系列:BeanFactoryApplicationContext

通過兩種系列的具體 IoC 容器來幫助我們了解了兩個不同的特點,以及面向不同的場景。有利有弊,在開發中需要根據具體需求選擇合適的 IoC 具體實現。

其中也通過對 Spring IoC 的具體實現的簡單分析,對 IoC 設計的有了初步的了解和想法。那麼現在就來開始了解 IoC 容器初始化的過程

IoC 容器的初始化過程:

前面在學習 FileSystemXmlApplicationContext 的時候,構造方法中通過此類調用了 refresh() 方法。IoC 容器的初始化實際上就是通過這個方法來啟動的,標誌着 IoC 容器正式啟動。

IoC 容器的啟動包括以下三個基本過程:

  1. BeanDefinition 資源的定位
  2. ~ 的載入
  3. ~ 的註冊

期間需要注意的是這是一個順序過程,同時指的是 IoC 容器的初始化,而 Bean 的依賴注入的實現,一般不包括其中,但是 BeanDefiniton 有一個 lazyinit 的屬性,用戶可以通過這個屬性改變 Bean 的依賴注入過程,eg:一般情況下 Bean 的注入需要在容器初始化之後,第一次調用 getBean() 時才會觸發,而通過 lazyinit 屬性可以讓 Bean 在 IoC 容器初始化時就預先完成了依賴注入。

BeanDefinition 的 Resource 定位

根據前面的學習,我們這一過程的表層應該不難知曉,就是通過定義一個 Resuorce 去定位容器使用的 BeanDefinition。eg:ClassPathResource() 這個類就是在項目中的類路徑中尋找以文件形式存在的 BeanDefinition。

應該注意的是,不能把 Resource 的定位 BeanDefinition 資源和 BD 的載入弄混淆了。只是定位資源而已,此時 IoC 容器還不能直接使用這些信息,這些信息是交由 BeanDefinitionReader 來對這些信息進行 BD 的載入處理。

相對於 DefaultListableBeanFactory 容器需要手動配置好特定的 Resource 讀取器,ApplicationContext 容器就準備好了一系列的讀取器。

但是使用 DefaultListableBeanFactory 這種底層容器可以根據業務定製 IoC 容器的靈活性,有利有弊。

還是通過 FileSystemXmlApplicationContext 這一具體容器來分析是如何完成 Resource 的定位過程。

繼承體系:

image-20221021164312140

主要兩個功能的源碼:

image-20221021164358242

上圖表明了 getResourceByPath() 是實現 Resource 定位的方法。但是並不是使用者調用的,查看該方法的調用鏈:

image-20221021165749280

上圖標註了該方法最初是由 refresh() 方法觸發的,而 refresh() 是在構造器中調用的。

我們需要通過這個方法來了解過程。

構造器調用的是超類 AbstractApplicationContext 中的 refresh(),查看源碼:

public void refresh() throws BeansException, IllegalStateException {
    synchronized(this.startupShutdownMonitor) {
        this.prepareRefresh();
        // 創建beanFactory以及掃描bean信息(beanDefinition),並通過BeanDefinitionRegistry 註冊到容器中。
        ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
        // 省略...

    }
}

obtainFreshBeanFactory() 源碼:

protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
    this.refreshBeanFactory();
    // 省略...
}

refreshBeanFactory() 是一個抽象方法,有兩個具體的實現類:

  • AbstractRefreshableApplicationContext
  • GenericApplicationContext

在這裡我們的 FSXAC 繼承了 AbstractRefreshableApplicationContext,所以我們看在這個類中 refreshBeanFactory() 的實現:

protected final void refreshBeanFactory() throws BeansException {
//...
    try {
        DefaultListableBeanFactory beanFactory = this.createBeanFactory(); // 1
        beanFactory.setSerializationId(this.getId());
        this.customizeBeanFactory(beanFactory);
        this.loadBeanDefinitions(beanFactory);// 2
///...
    }
}

我們抽出這兩行代碼進行分析:

  1. 創建 BeanFactory,以 DefaultListableBeanFactory 作為 IoC 容器。
  2. BD 的載入相關啟動。

我們到目前位置並沒有看到與之相關的 Resource 定位信息,只看到 BD 的載入啟動,所以針對 loadBeanDefinitions() 進行進一步分析。該方法調用的是本類的一個抽象方法loadBeanDefinitions(DefaultListableBeanFactory var1),此方法是模板方法,由子類具體實現:

image-20221023183708453

而 FSXAC 就是 AbstractXmlApplicationContext 的子類,所以進而分析這個類的具體實現。

image-20221023184006806

可以看到:

  1. 創建了 XmlBeanDefinitionReader 類,用於將 XML 文件中的 Bean 讀取出來並加載。

  2. 調用 XBDR 的 loadBeanDefinitions,開始啟動 BeanDefinition 的加載。

  3. 在具體的實現中,分別傳入不同的參數,但是在此方法中走判斷時,調用了 this.getConfigResources() 這個方法在此類中是返回的 Resource[]null,所以走第二個判斷,獲取以字符串數組,因為之前在 FSXAC 中就設置好了。

    image-20221023215532354

String[] 傳入調用的是 XmlBeanDefinitionReader 的基類 AbstractBeanDefinitionReader 的方法:

image-20221023220101615

這裡就是將 String 數組中的字符串,一個一個傳入調用本類重載方法,並且對其進行計數

public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException {
    return this.loadBeanDefinitions(location, (Set)null);
}

public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException {
    // 取得 ResourceLoader,使用的是 DeaultResourceLoader
    ResourceLoader resourceLoader = this.getResourceLoader(); // 關鍵代碼1
    if (resourceLoader == null) {
        //...略
    } else {
        int loadCount;
        // 調用 DefaultResourceLoader 的 getResource 完成具體的 Resource 定位
        if (!(resourceLoader instanceof ResourcePatternResolver)) { // 關鍵代碼2
            Resource resource = resourceLoader.getResource(location);
            loadCount = this.loadBeanDefinitions((Resource)resource);
            //... 略
        } else {
            // 調用 DefaultResourceLoader 的 getResources 完成具體的 Resource 定位
            try {
                Resource[] resources = ((ResourcePatternResolver)resourceLoader).getResources(location);
                loadCount = this.loadBeanDefinitions(resources);
                //... 略
            } 
            //... 略
        }
    }
}

可以看到最終調用的是兩個參數的方法:(String location, Set<Resource> actualResources),通過上面代碼的簡要分析,我們提取出兩個重要的信息:

  1. ResourceLoader 的作用?
  2. DefaultResourceLoader 的 getResource 完成了具體的 Resource 定位

首先第一個,Spring 將資源的定義和加載區分開來,這裡需要注意的是資源的加載也就是 Resource 的加載,而不是 BeanDefinition 的加載。Resource 定義了統一的資源(抽象並統一各種資源來源),ResourceLoader 定義了這些資源的統一加載。所以 BeanDefinition 資源的定位過程應該是:將不同 BD 資源獲取途徑經過 Spring 統一封裝為 Resource,再由 ResourceLoader 進行資源加載,獲取這些 Resource,給 BeanDefinition 的載入做準備。

而在這個 FSXAC 的例子中,這個 ResourceLoader 就是 DefaultResourceLoader,來看看是怎麼具體實現 getResource().

public Resource getResource(String location) {
    Assert.notNull(location, "Location must not be null");
    Iterator var2 = this.protocolResolvers.iterator();

    Resource resource;
    do {
        if (!var2.hasNext()) {
            if (location.startsWith("/")) {
                // 處理以 / 標識的 Resource 定位
                return this.getResourceByPath(location);
            }
			// 處理帶有 classpath 表示的 Resource 
            if (location.startsWith("classpath:")) {
                return new ClassPathResource(location.substring("classpath:".length()), this.getClassLoader());
            }

            try {
                // 處理 URL 表示的 Resource 定位
                URL url = new URL(location);
                return new UrlResource(url);
            } catch (MalformedURLException var5) {
                // 處理既不是 classpath 也不是 URL 標識的 Resource 定位
                // 則將 getResource 的責任交給 getResourceByPath(),這個方法時 protected,默認實現是得到一個 ClassPathContextResource 對象,通常會由子類實現該方法。
                return this.getResourceByPath(location);
            }
        }

        ProtocolResolver protocolResolver = (ProtocolResolver)var2.next();
        resource = protocolResolver.resolve(location, this);
    } while(resource == null);

    return resource;
}

通過上述分析,找到了熟悉的方法名: protected Resource getResourceByPath(String path){}

這個方法由子類 FSXAC 實現,這個方法返回的是:FileSystemResource 對象,通過這個對象,Spring 就可以進行相關的 I/O 操作,完成 BeanDefinition 定位。

實際上這麼多過程和細節,都是為了實現一個功能,對 path 進行解析,然後生成一個 FileSystemResource 對象,並返回,給 BeanDefinition 載入過程做準備。

實際上 Spring 針對不同類型的 Resource 都準備了對應的實現類,方便我們針對不同場景進行合適的使用,不同的 ApplicationContext 會對應生成其他的 Resource:ClassPathResource、ServletContextResource 等,而且 Resource 接口本身就是繼承了 InputStreamSource (這個抽象類唯一的方法是返回一個 InputStream),定義了很多的 I/O 相關的操作,其實現類也主要是針對不同的資源類型做出合適的實現。

小結:

通過 FileSystemXmlApplicationContext 這個 AC 實現原理為例子,初步的了解了 Resource 定位的解決方案,就是通過調用 getResourceByPath() 方法,重寫了父類 DefaultResourceLoader 的方法,最後得到了 FileSystemResource 這個類型的 Resource 的定位實現。那麼此時這個 Resource 的定位過程已經完成,為 BeanDefinition 的載入創造了 I/O 操作的條件,但是具體的數據還沒開始讀入。讀入就是 BeanDefinition 的載入和解析過程了。

其實 Resource 就是統一了資源的定義,各種 BeanDefinition 定義的資源(File,URL,XML…)都統一抽象成 Resource,所有實現類都需要實現相關的 I/O 操作。

而 ResourceLoader 就是根據某種匹配方式來創建匹配的 Resource,並返回。

將其過程多捋幾遍,初步理解其 BeanDefinition 的資源定位過程。下一步就是 BeanDefinition 的載入和解析過程。