【超詳細的Spring源碼分析 —— 03 Spring對於Bean管理的核心組件源碼分析 - 解析前的準備工做】

在上一篇文章中,咱們對 IoC 容器整個執行流程的準備階段已經分析完畢,如今就差最核心、最複雜的邏輯了。對應到開篇中的代碼,也就是下面這段:java

beanDefinitionReader.loadBeanDefinitions(resource);
複製代碼

因爲這部分涉及的邏輯很複雜,所以我主要分爲 4 個步驟來分析:ide

  1. 解析前的準備工做
  2. 解析 Bean Definition
  3. 初始化 Bean Definition
  4. 註冊到工廠

這一篇主要就是解析前的準備工做相關的一些分析了。函數

1、解析前的準備工做

雖然以前 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();
        }
    }
}
複製代碼

2、總結

能夠發現,在真正執行讀取資源以前,Spring 還作了諸多的準備工做:日誌

  • 字符集處理
  • 建立/獲取線程私有的資源Set集合
  • 處理配置文件的循環依賴
  • 定義解析 xml 的方式(sax)

而後纔是真正的解析邏輯:doLoadBeanDefinitions(inputSource, encodedResource.getResource());

在真正處理完咱們傳入的 resource 後,也會在內存中移除相應的對象,避免內存泄漏的問題。

OK,在下一篇咱們真正進入到解析的邏輯。

相關文章
相關標籤/搜索