在上一篇文章中,咱們對 IoC 容器整個執行流程的準備階段已經分析完畢,如今就差最核心、最複雜的邏輯了。對應到開篇中的代碼,也就是下面這段:java
beanDefinitionReader.loadBeanDefinitions(resource);
複製代碼
因爲這部分涉及的邏輯很複雜,所以我主要分爲 4 個步驟來分析:ide
這一篇主要就是解析前的準備工做相關的一些分析了。函數
雖然以前 Spring 已經作了足夠多的準備工做了,可是在解析以前,還須要完成一些準備事項,下面咱們一步一步來看這部分的源碼。學習
首先咱們進入到 loadBeanDefinitions() 這個函數:this
/** * 該方法須要傳入須要解析的資源對象 * * 它會從指定的資源中, 讀取bean定義 * 這裏的資源, 就是咱們以前建立的Resource對象 * * 這個方法的返回值,就是解析的Bean個數 */
@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
// 這裏會先對resource封裝一些編碼、字符集的處理,而後調用一個重載方法
return loadBeanDefinitions(new EncodedResource(resource));
}
複製代碼
咱們接着深刻EncodedResource這個構造方法:編碼
/** * 根據給定的resource建立一個新的EncodedResource * 這個構造方法沒有指定編碼格式或字符集 */
public EncodedResource(Resource resource) {
// 這裏會調用下面貼出的構造方法
this(resource, null, null);
}
// 注意這個構造方法是私有的
// 由於編碼和字符集咱們只須要指定其一便可, 不須要兩者都指定
// Spring 也分別提供了參數爲(resource + encoding) 和 (resource + charset)的構造方法
// 這種設計方式也是十分值得學習借鑑的
private EncodedResource(Resource resource, @Nullable String encoding, @Nullable Charset charset) {
// 首先調用了父類構造
super();
// 斷言資源不能爲空
Assert.notNull(resource, "Resource must not be null");
// 而後賦值參數到全局變量
this.resource = resource;
this.encoding = encoding;
this.charset = charset;
}
// 針對encoding + resource的構造
public EncodedResource(Resource resource, @Nullable String encoding) {
this(resource, encoding, null);
}
// 針對encoding + charset的構造
public EncodedResource(Resource resource, @Nullable Charset charset) {
this(resource, null, charset);
}
複製代碼
其實在 Spring 在給 resource 封裝編碼屬性時,所採起的設計方式是很是優雅的。spa
由於編碼字符處理與讀取資源並無任何邏輯關聯,所以 Spring 並無在 loadBeanDefinitions()
方法中增長兩個 encoding、charset 參數,而是將 resource、encoding、charset 封裝到一個 EncodedResource 類中,而後經過重載來實現讀取資源的方法。簡直是將面向對象的封裝特性發揮地淋漓盡致。線程
回到正題,當編碼字符集封裝完畢後,會進入到 loadBeanDefinitions()
的另外一個重載方法,這個方法須要傳入一個封裝了字符處理的資源對象。設計
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
// 斷言:encodedResource 不爲空
Assert.notNull(encodedResource, "EncodedResource must not be null");
// 記錄一下日誌
if (logger.isTraceEnabled()) {
logger.trace("Loading XML bean definitions from " + encodedResource);
}
// this.resourcesCurrentlyBeingLoaded是一個ThreadLocal<Set<EncodedResource>>類型的對象
// 它用於存儲當前線程私有的EncodedResource集合
// 這裏會獲取到當前線程私有的資源集合
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
// 若是獲取到的資源集合Set爲null
if (currentResources == null) {
// 那麼會初始化一個set, 而後將其set到threadLocal中
currentResources = new HashSet<>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
// 嘗試將resource資源添加到set, 若是添加失敗了則拋出異常
// 這裏提一下:
// Spring提供了 <import> 標籤, 用於在xml配置文件中導入其餘多個配置文件
// 若是 A.xml import B.xml, 且 B.xml import A.xml
// 那麼在加載 A.xml 的時候也會加載 B.xml
// 但 B.xml 是依賴於 A.xml 的, 這就會致使A、B之間的循環導入
// 爲了不這種狀況, 這裏經過set來進行一個去重
// 簡單來講, 就是解決資源的循環依賴問題
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException("Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
// 到這裏, 就是真正讀取配置文件的核心邏輯了
// 首先是一個 try-with-resource 語句, 獲取到了resource的輸入流
try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
// 而後會將inputStream封裝爲InputSource
// 這裏提一下:
// 解析xml的方式有兩種
// 1. DOM: 將整個xml讀取到內存進行解析
// 2. SAX: 以文件流的形式逐行加載到內存並解析
// 這裏採用的是後者
InputSource inputSource = new InputSource(inputStream);
// 若是encodedResource被設置了編碼格式, 那麼在解析的時候採用指定編碼格式
// 默認狀況下, 是沒有設置編碼格式、字符集的, 這裏以前源碼中有分析到
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
// 而後就是真正的解析邏輯了
// 這裏會調用 doLoadBeanDefinitions, 傳入一個InputSource、以及 resource
// 還有一個小細節須要注意:Spring中的大部份內部方法都是以 」do「 開頭的
// 這些方法是 Spring 不但願咱們調用的
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
} catch (IOException ex) {
throw new BeanDefinitionStoreException("IOException parsing XML document from " + encodedResource.getResource(), ex);
} finally {
// 爲了不內存泄漏
// 在資源被加載完畢後, 會在Set集合中刪除掉resource
currentResources.remove(encodedResource);
// 而且當資源集合爲空時, 刪除掉threadLocal中的元素
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
複製代碼
能夠發現,在真正執行讀取資源以前,Spring 還作了諸多的準備工做:日誌
而後纔是真正的解析邏輯:doLoadBeanDefinitions(inputSource, encodedResource.getResource());
在真正處理完咱們傳入的 resource 後,也會在內存中移除相應的對象,避免內存泄漏的問題。
OK,在下一篇咱們真正進入到解析的邏輯。