Spring Ioc源碼分析系列–Ioc的基礎知識準備

Spring Ioc源碼分析系列–Ioc的基礎知識準備

本系列文章程式碼基於Spring Framework 5.2.x

Ioc的概念

在Spring里,Ioc的定義為The IoC Container,翻譯過來也就是Ioc容器。為什麼會被叫做容器呢?我們來比對一下日常生活中的容器,也就是那些瓶瓶罐罐。假設我們有個大米缸,裡面提前放好了米,等我們需要米的時候,我們就可以到大米缸裡面取。那麼Ioc也是一樣的道理,裡面有一個容器singletonObjects(提前透露這裡容器的類型是ConcurrentHashMap),裡面放好了各種初始化好的bean,當我們程式碼需要使用的時候,就到裡面去取。

藉助一張圖來看一下Spring Ioc的工作流程。整個過程就如同上面描述類似,把業務類pojo和一些元數據配置資訊Configuration Metadata提供到Ioc,Ioc會根據你給的資訊生成可以使用的Bean,這裡生成的bean是可以直接使用的,Ioc是不是替我們省去了一大堆new的工作。當然這裡面涉及非常多的細節,例如怎麼獲取元數據,怎麼根據元數據生成想要的bean,這些都會在後續解答。

1651805989772

那麼問題來了,為什麼需要一個容器,我隨手new個對象不香嗎?要討論這個問題,可以對比有容器和沒有容器的區別,我個人認為有以下比較顯著的優點

  • 方便管理。容器提供了一個集中化的管理,方便進行其他的操作,例如Aop相關的功能實現。無容器的無法集中管理bean,所有bean散落到項目的各個角落,如果要進行一些額外的調整需要改動的點非常多。
  • 性能節省。容器只需初始化一次bean,後續使用只需要直接獲取。而無容器需要每次new對象,開銷相比較而言肯定會更大。
  • 程式碼美觀。容器屏蔽了複雜對象的構造過程,對於使用而言只需要直接去獲取,無容器需要每次構造複雜對象,程式碼重複率非常高,想想你的項目里充滿了各種new對象的程式碼,是不是就已經讓你很頭疼。

那麼一個東西不可能只有優點而沒有缺點,任何事物都需要辯證地去看待,那麼提供容器後的缺點是什麼?個人認為有如下比較顯著的缺點

  • 並發安全。提供了一個集中式的容器管理,不可避免得在多執行緒情況下出現並發訪問的情況,那麼在保證執行緒安全的時候需要付出額外的性能開銷。
  • 啟動緩慢。同理,提供了一個集中式的容器管理,那麼就需要在啟動之初就把需要的各種bean初始化好,放入容器中,儘管這些bean不一定會被用到。如果沒有指定初始化時機,那麼這部分沒有使用的bean也會在啟動之初就進行初始化,這相比使用時再創建當然會消耗了額外的性能。
  • 記憶體隱患。由於對象都放在容器里,那麼在有許多大對象或者對象的生命周期都非常的長的時候,需要考慮對象太多造成的記憶體開銷。

這裡簡單分析了一下優缺點,當然這只是一家之言,有錯漏補充歡迎指出。目前來看,Spring的優點遠遠大於其缺點,這也是Spring經久不衰的原因。

經過上面的介紹,我相信你已經對Ioc有個初步的整體認識。即這是一個容器,裡面放好了可以使用的bean請牢記這個結論。那麼接下來會介紹Ioc的一些知識體系,留下個整體輪廓就行,不涉及太多了源碼分析。

BeanFactory 還是 ApplicationContext

本節說明 BeanFactoryApplicationContext 容器級別之間的差異以及對使用Ioc的影響。 相信嘗試看過Ioc源碼的人都會被這兩個迷惑過,BeanFactoryApplicationContext提供的功能看起來似乎是類似的,那麼這兩個玩意有啥聯繫和區別呢?

我們通常推薦使用ApplicationContext,除非有充分的理由不這樣做,否則應該使用 ApplicationContext,通常將 GenericApplicationContext 及其子類 AnnotationConfigApplicationContext 作為自定義引導的常見實現。這些是 Spring 核心容器的主要入口點,用於所有常見目的:載入配置文件、觸發類路徑掃描、以編程方式註冊 bean 定義和帶注釋的類,以及(從 5.0 開始)註冊功能 bean 定義。

因為 ApplicationContext 包含 BeanFactory 的所有功能,所以通常建議使用 ApplicationContext ,除非需要完全控制 bean 處理的場景。在 ApplicationContext(例如 GenericApplicationContext 實現)中,按照約定(即按 bean 名稱或按 bean 類型 —特別是後處理器)檢測幾種 bean,而普通的 DefaultListableBeanFactory 不知道任何特殊的 bean

對於許多擴展容器特性,例如註解處理和 AOP 代理,BeanPostProcessor 擴展點是必不可少的。如果你僅使用普通的 DefaultListableBeanFactory,則默認情況下不會檢測和激活此類後處理器。這種情況可能會令人困惑,因為您的 bean 配置實際上沒有任何問題。相反,在這種情況下,需要通過額外的設置來完全引導容器。

下表列出了 BeanFactoryApplicationContext 介面和實現提供的功能。

特性 BeanFactory ApplicationContext
Bean實例化/注入 Yes Yes
集成的生命周期管理 No Yes
自動 BeanPostProcessor 註冊 No Yes
自動 BeanFactoryPostProcessor 註冊 No Yes
方便的 MessageSource 訪問(用於國際化) No Yes
內置ApplicationEvent發布機制 No Yes

要使用 DefaultListableBeanFactory 顯式註冊 bean 後處理器,您需要以編程方式調用 addBeanPostProcessor(),如以下示例所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// 用 bean 定義填充工廠

// 現在註冊任何需要的 BeanPostProcessor 實例
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());

// 現在開始使用工廠

要將 BeanFactoryPostProcessor 應用於普通的 DefaultListableBeanFactory,您需要調用其 postProcessBeanFactory() 方法,如以下示例所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));

// 從屬性文件中引入一些屬性值
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));

// 現在實際進行替換
cfg.postProcessBeanFactory(factory);

在這兩種情況下,顯式註冊步驟都不方便,這就是為什麼在 Spring 支援的應用程式中各種 ApplicationContext 變體優於普通 DefaultListableBeanFactory 的原因,尤其是在典型企業設置中依賴 BeanFactoryPostProcessorBeanPostProcessor 實例來擴展容器功能時。

AnnotationConfigApplicationContext 註冊了所有常見的注釋後處理器,並且可以通過配置注釋(例如@EnableTransactionManagement)在幕後引入額外的處理器。在 Spring 的基於註解的配置模型的抽象級別上,bean 後置處理器的概念變成了純粹的內部容器細節。

Spring的統一資源載入策略

資源抽象Resource

在Spring里, org.springframework.core.io.Resource 為 Spring 框架所有資源的抽象和訪問介面,它繼承 org.springframework.core.io.InputStreamSource介面。作為所有資源的統一抽象,Resource 定義了一些通用的方法,由子類 AbstractResource 提供統一的默認實現。定義如下:

public interface Resource extends InputStreamSource {

	/**
	 * 資源是否存在
	 */
	boolean exists();

	/**
	 * 資源是否可讀
	 */
	default boolean isReadable() {
		return true;
	}

	/**
	 * 資源所代表的句柄是否被一個 stream 打開了
	 */
	default boolean isOpen() {
		return false;
	}

	/**
	 * 是否為 File
	 */
	default boolean isFile() {
		return false;
	}

	/**
	 * 返回資源的 URL 的句柄
	 */
	URL getURL() throws IOException;

	/**
	 * 返回資源的 URI 的句柄
	 */
	URI getURI() throws IOException;

	/**
	 * 返回資源的 File 的句柄
	 */
	File getFile() throws IOException;

	/**
	 * 返回 ReadableByteChannel
	 */
	default ReadableByteChannel readableChannel() throws IOException {
		return java.nio.channels.Channels.newChannel(getInputStream());
	}

	/**
	 * 資源內容的長度
	 */
	long contentLength() throws IOException;

	/**
	 * 資源最後的修改時間
	 */
	long lastModified() throws IOException;

	/**
	 * 根據資源的相對路徑創建新資源
	 */
	Resource createRelative(String relativePath) throws IOException;

	/**
	 * 資源的文件名
	 */
	@Nullable
	String getFilename();

	/**
	 * 資源的描述
	 */
	String getDescription();

}

子類結構如下:

1651819272690

從上圖可以看到,Resource 根據資源的不同類型提供不同的具體實現,如下:

  • FileSystemResource :對 java.io.File 類型資源的封裝,只要是跟 File 打交道的,基本上與 FileSystemResource 也可以打交道。支援文件和 URL 的形式,實現 WritableResource 介面,且從 Spring Framework 5.0 開始,FileSystemResource 使用 NIO2 API進行讀/寫交互。
  • ByteArrayResource :對位元組數組提供的數據的封裝。如果通過 InputStream 形式訪問該類型的資源,該實現會根據位元組數組的數據構造一個相應的 ByteArrayInputStream。
  • UrlResource :對 java.net.URL類型資源的封裝。內部委派 URL 進行具體的資源操作。
  • ClassPathResource :class path 類型資源的實現。使用給定的 ClassLoader 或者給定的 Class 來載入資源。
  • InputStreamResource :將給定的 InputStream 作為一種資源的 Resource 的實現類。

org.springframework.core.io.AbstractResource ,為 Resource 介面的默認抽象實現。它實現了 Resource 介面的大部分的公共實現

資源定位ResourceLoader

Spring 將資源的定義和資源的載入區分開了,Resource 定義了統一的資源,那資源的載入則由 ResourceLoader 來統一定義

org.springframework.core.io.ResourceLoader 為 Spring 資源載入的統一抽象,具體的資源載入則由相應的實現類來完成,所以我們可以將 ResourceLoader 稱作為統一資源定位器。其定義如下:


/**
 * 用於載入資源(例如類路徑或文件系統資源)的策略介面。
 * 需要 {@link org.springframework.context.ApplicationContext} 來提供此功能,
 * 以及擴展的 {@link org.springframework.core.io.support.ResourcePatternResolver} 支援。 
 * <p>{@link DefaultResourceLoader} 是一個獨立的實現,可以在 ApplicationContext 之外使用,也被 {@link ResourceEditor} 使用。 
 * <p>在 ApplicationContext 中運行時,可以使用特定上下文的資源載入策略從字元串中填充類型為 Resource 和 Resource 數組的 Bean 屬性。
 *
 */
public interface ResourceLoader {

	/** Pseudo URL prefix for loading from the class path: "classpath:". */
	String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;


	/**
	 * Return a Resource handle for the specified resource location.
	 * <p>The handle should always be a reusable resource descriptor,
	 * allowing for multiple {@link Resource#getInputStream()} calls.
	 * <p><ul>
	 * <li>Must support fully qualified URLs, e.g. "file:C:/test.dat".
	 * <li>Must support classpath pseudo-URLs, e.g. "classpath:test.dat".
	 * <li>Should support relative file paths, e.g. "WEB-INF/test.dat".
	 * (This will be implementation-specific, typically provided by an
	 * ApplicationContext implementation.)
	 * </ul>
	 * <p>Note that a Resource handle does not imply an existing resource;
	 * you need to invoke {@link Resource#exists} to check for existence.
	 * @param location the resource location
	 * @return a corresponding Resource handle (never {@code null})
	 * @see #CLASSPATH_URL_PREFIX
	 * @see Resource#exists()
	 * @see Resource#getInputStream()
	 */
	Resource getResource(String location);

	/**
	 * Expose the ClassLoader used by this ResourceLoader.
	 * <p>Clients which need to access the ClassLoader directly can do so
	 * in a uniform manner with the ResourceLoader, rather than relying
	 * on the thread context ClassLoader.
	 * @return the ClassLoader
	 * (only {@code null} if even the system ClassLoader isn't accessible)
	 * @see org.springframework.util.ClassUtils#getDefaultClassLoader()
	 * @see org.springframework.util.ClassUtils#forName(String, ClassLoader)
	 */
	@Nullable
	ClassLoader getClassLoader();

}
  • #getResource(String location) 方法,根據所提供資源的路徑 location 返回 Resource 實例,但是它不確保該 Resource 一定存在,需要調用 Resource#exist() 方法來判斷。

  • 該方法支援以下模式的資源載入:

    • URL位置資源,如 "file:C:/test.dat"
    • ClassPath位置資源,如 "classpath:test.dat
    • 相對路徑資源,如 "WEB-INF/test.dat" ,此時返回的Resource 實例,根據實現不同而不同。
  • 該方法的主要實現是在其子類 DefaultResourceLoader 中實現,具體過程我們在分析 DefaultResourceLoader 時做詳細說明。

  • #getClassLoader() 方法,返回 ClassLoader 實例,對於想要獲取 ResourceLoader 使用的 ClassLoader 用戶來說,可以直接調用該方法來獲取。在分析 Resource 時,提到了一個類 ClassPathResource ,這個類是可以根據指定的 ClassLoader 來載入資源的。

子類結構如下:

1651820093602

  • DefaultResourceLoaderAbstractResource 相似,org.springframework.core.io.DefaultResourceLoaderResourceLoader 的默認實現。

  • FileSystemResourceLoader繼承 DefaultResourceLoader ,且覆寫了 #getResourceByPath(String) 方法,使之從文件系統載入資源並以 FileSystemResource 類型返回,這樣我們就可以得到想要的資源類型。

  • ClassRelativeResourceLoaderDefaultResourceLoader 的另一個子類的實現。和 FileSystemResourceLoader 類似,在實現程式碼的結構上類似,也是覆寫 #getResourceByPath(String path) 方法,並返回其對應的 ClassRelativeContextResource 的資源類型。

  • PathMatchingResourcePatternResolverResourcePatternResolver 最常用的子類,它除了支援 ResourceLoaderResourcePatternResolver 新增的 classpath*: 前綴外,還支援 Ant 風格的路徑匹配模式(類似於 "**/*.xml")。

至此 Spring 整個資源記載過程已經分析完畢。下面簡要總結下:

  • Spring 提供了 ResourceResourceLoader 來統一抽象整個資源及其定位。使得資源與資源的定位有了一個更加清晰的界限,並且提供了合適的 Default 類,使得自定義實現更加方便和清晰。
  • AbstractResource 為 Resource 的默認抽象實現,它對 Resource 介面做了一個統一的實現,子類繼承該類後只需要覆蓋相應的方法即可,同時對於自定義的 Resource 我們也是繼承該類。
  • DefaultResourceLoader 同樣也是 ResourceLoader 的默認實現,在自定 ResourceLoader 的時候我們除了可以繼承該類外還可以實現 ProtocolResolver 介面來實現自定資源載入協議。
  • DefaultResourceLoader 每次只能返回單一的資源,所以 Spring 針對這個提供了另外一個介面 ResourcePatternResolver ,該介面提供了根據指定的 locationPattern 返回多個資源的策略。其子類 PathMatchingResourcePatternResolver 是一個集大成者的 ResourceLoader ,因為它即實現了 Resource getResource(String location) 方法,也實現了 Resource[] getResources(String locationPattern) 方法。

BeanFactory與ApplicationContext體系

BeanFactory體系

下面來介紹一下Ioc的核心實現有哪些重要的類,先看BeanFactory的體系,類結構如下,這裡把spring-context部分的實現去掉了。

1651822615122

可以看到裡面的類還是比較多的,但是各司其職,每個類都有自己對應的職責,下面來介紹幾個比較重點的類。

  • AutowireCapableBeanFactory介面提供了對現有bean進行自動裝配的能力,設計目的不是為了用於一般的應用程式碼中,對於一般的應用程式碼應該使用BeanFactoryListableBeanFactory。其他框架的程式碼集成可以利用這個介面去裝配和填充現有的bean的實例,但是Spring不會控制這些現有bean的生命周期。

  • ConfigurableBeanFactory提供了bean工廠的配置機制(除了BeanFactory介面中的bean的工廠的客戶端方法)。該BeanFactory介面不適應一般的應用程式碼中,應該使用BeanFactoryListableBeanFactory。該擴展介面僅僅用於內部框架的使用,並且是對bean工廠配置方法的特殊訪問。

  • ConfigurableListableBeanFactory 介面繼承自ListableBeanFactory, AutowireCapableBeanFactory, ConfigurableBeanFactory。大多數具有列出能力的bean工廠都應該實現此介面。此了這些介面的能力之外,該介面還提供了分析、修改bean的定義和單例的預先實例化的機制。這個介面不應該用於一般的客戶端程式碼中,應該僅僅提供給內部框架使用。

  • AbstractBeanFactory 繼承自FactoryBeanRegistrySupport,實現了ConfigurableBeanFactory介面。AbstractBeanFactoryBeanFactory的抽象基礎類實現,提供了完整的ConfigurableBeanFactory的能力。

    • 單例快取
    • 別名的管理
    • FactoryBean的處理
    • 用於子bean定義的bean的合併
    • bean的摧毀介面
    • 自定義的摧毀方法
    • BeanFactory的繼承管理
  • AbstractAutowireCapableBeanFactory繼承自AbstractBeanFactory,實現了AutowireCapableBeanFactory介面。該抽象了實現了默認的bean的創建。

    • 提供了bean的創建、屬性填充、裝配和初始化
    • 處理運行時bean的引用,解析管理的集合、調用初始化方法等
    • 支援構造器自動裝配,根據類型來對屬性進行裝配,根據名字來對屬性進行裝配
  • DefaultListableBeanFactory 繼承自AbstractAutowireCapableBeanFactory,實現了ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable介面。這個類是一個非常完全的BeanFactory,基於bean的定義元數據,通過後置處理器來提供可擴展性。

  • XmlBeanFactory 繼承自DefaultListableBeanFactory,用來從xml文檔中讀取bean的定義的一個非常方便的類。最底層是委派給XmlBeanDefinitionReader,實際上等價於帶有XmlBeanDefinitionReaderDefaultListableBeanFactory。 該類已經廢棄,推薦使用的是DefaultListableBeanFactory

ApplicationContext體系

接下來看看更高層次的容器實現ApplicationContext的體系。類結構圖如下,這裡只展示了常用的實現,並且去掉了大部分spring-web模組的實現類:

1651824404738

  • ConfigurableApplicationContext 從上面的類的繼承層次圖能看到,ConfigurableApplicationContext是比較上層的一個介面,該介面也是比較重要的一個介面,幾乎所有的應用上下文都實現了該介面。該介面在ApplicationContext的基礎上提供了配置應用上下文的能力,此外提供了生命周期的控制能力。

  • AbstractApplicationContextApplicationContext介面的抽象實現,這個抽象類僅僅是實現了公共的上下文特性。這個抽象類使用了模板方法設計模式,需要具體的實現類去實現這些抽象的方法。

  • GenericApplicationContext繼承自AbstractApplicationContext,是為通用目的設計的,它能載入各種配置文件,例如xml,properties等等。它的內部持有一個DefaultListableBeanFactory的實例,實現了BeanDefinitionRegistry介面,以便允許向其應用任何bean的定義的讀取器。為了能夠註冊bean的定義,refresh()只允許調用一次。

  • AnnotationConfigApplicationContext繼承自GenericApplicationContext,提供了註解配置(例如:Configuration、Component、inject等)和類路徑掃描(scan方法)的支援,可以使用register(Class... annotatedClasses)來註冊一個一個的進行註冊。實現了AnnotationConfigRegistry介面,來完成對註冊配置的支援,只有兩個方法:register()scan()。內部使用AnnotatedBeanDefinitionReader來完成註解配置的解析,使用ClassPathBeanDefinitionScanner來完成類路徑下的bean定義的掃描。

  • AbstractXmlApplicationContext繼承自AbstractRefreshableConfigApplicationContext,用於描繪包含能被XmlBeanDefinitionReader所理解的bean定義的XML文檔。子類只需要實現getConfigResourcesgetConfigLocations來提供配置文件資源。

  • ClassPathXmlApplicationContext繼承自AbstractXmlApplicationContext,和FileSystemXmlApplicationContext類似,只不過ClassPathXmlApplicationContext是用於處理類路徑下的xml配置文件。文件的路徑可以是具體的文件路徑,例如:xxx/application.xml,也可以是ant風格的配置,例如:xxx/*-context.xml

  • AnnotationConfigWebApplicationContext繼承自AbstractRefreshableWebApplicationContext,接受註解的類作為輸入(特殊的@Configuration註解類,一般的@Component註解類,與JSR-330兼容的javax.inject註解)。允許一個一個的注入,同樣也能使用類路徑掃描。對於web環境,基本上是和AnnotationConfigApplicationContext等價的。使用AnnotatedBeanDefinitionReader來對註解的bean進行處理,使用ClassPathBeanDefinitionScanner來對類路徑下的bean進行掃描。

小結

這篇主要做了一些基礎知識的準備,簡單介紹了一些Ioc的概念,這裡並沒有舉程式碼例子,只是通過生活中的容器去類比了一下Spring的容器。接下來對比分析了BeanFactoryApplicationContext區別與聯繫,然後介紹了Spring的資源載入,Spring的許多元數據載入通過統一資源載入的方式去獲取的,特別是classpath路徑下文件的獲取。最後我們簡單看了一下BeanFactoryApplicationContext的體系結構,展示常見的類圖,並且有簡單的描述,但是沒有涉及太多的程式碼分析,主要也是混個眼熟。

那麼有了這些準備,下一篇,我們就會通過一個xml配置文件去載入配置,通過Spring容器獲取我們需要的bean,那麼這就會用到這篇文章介紹過的資源載入,BeanFactory以及ApplicationContext體系里的類等等。

那麼下面的文章就會進行真正的源碼分析了,庖丁解牛。

如果有人看到這裡,那在這裡老話重提。與君共勉,路漫漫其修遠兮,吾將上下而求索。

Tags: