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
,這些都會在後續解答。
那麼問題來了,為什麼需要一個容器,我隨手new
個對象不香嗎?要討論這個問題,可以對比有容器和沒有容器的區別,我個人認為有以下比較顯著的優點:
- 方便管理。容器提供了一個集中化的管理,方便進行其他的操作,例如Aop相關的功能實現。無容器的無法集中管理
bean
,所有bean
散落到項目的各個角落,如果要進行一些額外的調整需要改動的點非常多。 - 性能節省。容器只需初始化一次
bean
,後續使用只需要直接獲取。而無容器需要每次new
對象,開銷相比較而言肯定會更大。 - 程式碼美觀。容器屏蔽了複雜對象的構造過程,對於使用而言只需要直接去獲取,無容器需要每次構造複雜對象,程式碼重複率非常高,想想你的項目里充滿了各種
new
對象的程式碼,是不是就已經讓你很頭疼。
那麼一個東西不可能只有優點而沒有缺點,任何事物都需要辯證地去看待,那麼提供容器後的缺點是什麼?個人認為有如下比較顯著的缺點:
- 並發安全。提供了一個集中式的容器管理,不可避免得在多執行緒情況下出現並發訪問的情況,那麼在保證執行緒安全的時候需要付出額外的性能開銷。
- 啟動緩慢。同理,提供了一個集中式的容器管理,那麼就需要在啟動之初就把需要的各種
bean
初始化好,放入容器中,儘管這些bean
不一定會被用到。如果沒有指定初始化時機,那麼這部分沒有使用的bean
也會在啟動之初就進行初始化,這相比使用時再創建當然會消耗了額外的性能。 - 記憶體隱患。由於對象都放在容器里,那麼在有許多大對象或者對象的生命周期都非常的長的時候,需要考慮對象太多造成的記憶體開銷。
這裡簡單分析了一下優缺點,當然這只是一家之言,有錯漏補充歡迎指出。目前來看,Spring的優點遠遠大於其缺點,這也是Spring經久不衰的原因。
經過上面的介紹,我相信你已經對Ioc有個初步的整體認識。即這是一個容器,裡面放好了可以使用的bean
。請牢記這個結論。那麼接下來會介紹Ioc的一些知識體系,留下個整體輪廓就行,不涉及太多了源碼分析。
BeanFactory 還是 ApplicationContext
本節說明 BeanFactory
和 ApplicationContext
容器級別之間的差異以及對使用Ioc的影響。 相信嘗試看過Ioc源碼的人都會被這兩個迷惑過,BeanFactory
和 ApplicationContext
提供的功能看起來似乎是類似的,那麼這兩個玩意有啥聯繫和區別呢?
我們通常推薦使用ApplicationContext
,除非有充分的理由不這樣做,否則應該使用 ApplicationContext
,通常將 GenericApplicationContext
及其子類 AnnotationConfigApplicationContext
作為自定義引導的常見實現。這些是 Spring 核心容器的主要入口點,用於所有常見目的:載入配置文件、觸發類路徑掃描、以編程方式註冊 bean 定義和帶注釋的類,以及(從 5.0 開始)註冊功能 bean 定義。
因為 ApplicationContext
包含 BeanFactory
的所有功能,所以通常建議使用 ApplicationContext
,除非需要完全控制 bean
處理的場景。在 ApplicationContext
(例如 GenericApplicationContext
實現)中,按照約定(即按 bean
名稱或按 bean
類型 —特別是後處理器)檢測幾種 bean
,而普通的 DefaultListableBeanFactory
不知道任何特殊的 bean
。
對於許多擴展容器特性,例如註解處理和 AOP 代理,BeanPostProcessor
擴展點是必不可少的。如果你僅使用普通的 DefaultListableBeanFactory
,則默認情況下不會檢測和激活此類後處理器。這種情況可能會令人困惑,因為您的 bean
配置實際上沒有任何問題。相反,在這種情況下,需要通過額外的設置來完全引導容器。
下表列出了 BeanFactory
和 ApplicationContext
介面和實現提供的功能。
特性 | 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
的原因,尤其是在典型企業設置中依賴 BeanFactoryPostProcessor
和 BeanPostProcessor
實例來擴展容器功能時。
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();
}
子類結構如下:
從上圖可以看到,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 實例,根據實現不同而不同。
- URL位置資源,如
-
該方法的主要實現是在其子類
DefaultResourceLoader
中實現,具體過程我們在分析DefaultResourceLoader
時做詳細說明。 -
#getClassLoader()
方法,返回ClassLoader
實例,對於想要獲取ResourceLoader
使用的ClassLoader
用戶來說,可以直接調用該方法來獲取。在分析Resource
時,提到了一個類ClassPathResource
,這個類是可以根據指定的ClassLoader
來載入資源的。
子類結構如下:
-
DefaultResourceLoader
與AbstractResource
相似,org.springframework.core.io.DefaultResourceLoader
是ResourceLoader
的默認實現。 -
FileSystemResourceLoader
繼承DefaultResourceLoader
,且覆寫了#getResourceByPath(String)
方法,使之從文件系統載入資源並以FileSystemResource
類型返回,這樣我們就可以得到想要的資源類型。 -
ClassRelativeResourceLoader
是DefaultResourceLoader
的另一個子類的實現。和FileSystemResourceLoader
類似,在實現程式碼的結構上類似,也是覆寫#getResourceByPath(String path)
方法,並返回其對應的ClassRelativeContextResource
的資源類型。 -
PathMatchingResourcePatternResolver
為ResourcePatternResolver
最常用的子類,它除了支援ResourceLoader
和ResourcePatternResolver
新增的classpath*:
前綴外,還支援 Ant 風格的路徑匹配模式(類似於"**/*.xml"
)。
至此 Spring 整個資源記載過程已經分析完畢。下面簡要總結下:
- Spring 提供了
Resource
和ResourceLoader
來統一抽象整個資源及其定位。使得資源與資源的定位有了一個更加清晰的界限,並且提供了合適的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
部分的實現去掉了。
可以看到裡面的類還是比較多的,但是各司其職,每個類都有自己對應的職責,下面來介紹幾個比較重點的類。
-
AutowireCapableBeanFactory
介面提供了對現有bean
進行自動裝配的能力,設計目的不是為了用於一般的應用程式碼中,對於一般的應用程式碼應該使用BeanFactory
和ListableBeanFactory
。其他框架的程式碼集成可以利用這個介面去裝配和填充現有的bean的實例,但是Spring不會控制這些現有bean的生命周期。 -
ConfigurableBeanFactory
提供了bean工廠的配置機制(除了BeanFactory介面中的bean的工廠的客戶端方法)。該BeanFactory
介面不適應一般的應用程式碼中,應該使用BeanFactory
和ListableBeanFactory
。該擴展介面僅僅用於內部框架的使用,並且是對bean
工廠配置方法的特殊訪問。 -
ConfigurableListableBeanFactory
介面繼承自ListableBeanFactory
,AutowireCapableBeanFactory
,ConfigurableBeanFactory
。大多數具有列出能力的bean工廠都應該實現此介面。此了這些介面的能力之外,該介面還提供了分析、修改bean的定義和單例的預先實例化的機制。這個介面不應該用於一般的客戶端程式碼中,應該僅僅提供給內部框架使用。 -
AbstractBeanFactory
繼承自FactoryBeanRegistrySupport
,實現了ConfigurableBeanFactory
介面。AbstractBeanFactory
是BeanFactory
的抽象基礎類實現,提供了完整的ConfigurableBeanFactory
的能力。- 單例快取
- 別名的管理
FactoryBean
的處理- 用於子
bean
定義的bean
的合併 bean
的摧毀介面- 自定義的摧毀方法
BeanFactory
的繼承管理
-
AbstractAutowireCapableBeanFactory
繼承自AbstractBeanFactory
,實現了AutowireCapableBeanFactory
介面。該抽象了實現了默認的bean的創建。- 提供了
bean
的創建、屬性填充、裝配和初始化 - 處理運行時
bean
的引用,解析管理的集合、調用初始化方法等 - 支援構造器自動裝配,根據類型來對屬性進行裝配,根據名字來對屬性進行裝配
- 提供了
-
DefaultListableBeanFactory
繼承自AbstractAutowireCapableBeanFactory
,實現了ConfigurableListableBeanFactory
,BeanDefinitionRegistry
,Serializable
介面。這個類是一個非常完全的BeanFactory
,基於bean
的定義元數據,通過後置處理器來提供可擴展性。 -
XmlBeanFactory
繼承自DefaultListableBeanFactory
,用來從xml
文檔中讀取bean的定義的一個非常方便的類。最底層是委派給XmlBeanDefinitionReader
,實際上等價於帶有XmlBeanDefinitionReader
的DefaultListableBeanFactory
。 該類已經廢棄,推薦使用的是DefaultListableBeanFactory
。
ApplicationContext體系
接下來看看更高層次的容器實現ApplicationContext
的體系。類結構圖如下,這裡只展示了常用的實現,並且去掉了大部分spring-web
模組的實現類:
-
ConfigurableApplicationContext
從上面的類的繼承層次圖能看到,ConfigurableApplicationContext
是比較上層的一個介面,該介面也是比較重要的一個介面,幾乎所有的應用上下文都實現了該介面。該介面在ApplicationContext
的基礎上提供了配置應用上下文的能力,此外提供了生命周期的控制能力。 -
AbstractApplicationContext
是ApplicationContext
介面的抽象實現,這個抽象類僅僅是實現了公共的上下文特性。這個抽象類使用了模板方法設計模式,需要具體的實現類去實現這些抽象的方法。 -
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文檔。子類只需要實現getConfigResources
和getConfigLocations
來提供配置文件資源。 -
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的容器。接下來對比分析了BeanFactory
和ApplicationContext
區別與聯繫,然後介紹了Spring的資源載入,Spring的許多元數據載入通過統一資源載入的方式去獲取的,特別是classpath
路徑下文件的獲取。最後我們簡單看了一下BeanFactory
和ApplicationContext
的體系結構,展示常見的類圖,並且有簡單的描述,但是沒有涉及太多的程式碼分析,主要也是混個眼熟。
那麼有了這些準備,下一篇,我們就會通過一個xml
配置文件去載入配置,通過Spring容器獲取我們需要的bean
,那麼這就會用到這篇文章介紹過的資源載入,BeanFactory
以及ApplicationContext
體系里的類等等。
那麼下面的文章就會進行真正的源碼分析了,庖丁解牛。
如果有人看到這裡,那在這裡老話重提。與君共勉,路漫漫其修遠兮,吾將上下而求索。