我該如何學習spring原始碼以及解析bean定義的註冊

寧願呢發表於2019-06-06

如何學習spring原始碼

前言

本文屬於spring原始碼解析的系列文章之一,文章主要是介紹如何學習spring的原始碼,希望能夠最大限度的幫助到有需要的人。文章總體難度不大,但比較繁重,學習時一定要耐住性子堅持下去。

獲取原始碼

原始碼的獲取有多種途徑

GitHub

spring-framework

spring-wiki

可以從GitHub上獲取原始碼,然後自行編譯

maven

使用過maven的都知道可以通過maven下載相關的原始碼和相關文件,簡單方便。

這裡推薦通過maven的方式構建一個web專案。通過對實際專案的執行過程中進行除錯來學習更好。

如何開始學習

前置條件

如果想要開始學習spring的原始碼,首先要求本身對spring框架的使用基本瞭解。明白spring中的一些特性如ioc等。瞭解spring中各個模組的作用。

確定目標

首先我們要知道spring框架本身經過多年的發展現在已經是一個龐大的家族。可能其中一個功能的實現依賴於多個模組多個類的相互配合,這樣會導致我們在閱讀程式碼時難度極大。多個類之間進行跳躍很容易讓我們暈頭轉向。

所以在閱讀spring的原始碼的時候不能像在JDK程式碼時一行一行的去理解程式碼,需要把有限的精力更多的分配給重要的地方。而且我們也沒有必要這樣去閱讀。

在閱讀spring某一功能的程式碼時應當從一個上帝視角來總覽全域性。只需要知道某一個功能的實現流程即可,而且幸運的是spring的程式碼規範較好,大多數方法基本都能見名知意,這樣也省去了我們很多的麻煩。

利用好工具

閱讀程式碼最好在idea或者eclipse中進行,這類IDE提供的很多功能很有幫助。

在閱讀時配合spring文件更好(如果是自行編譯原始碼直接看註釋更好)。

筆記和複習

這個過程及其重要,我以前也看過一些spring的原始碼,但是好幾次都是感覺比較吃力在看過一些後就放棄了。而由於沒有做筆記和沒有複習的原因很快就忘了。下次想看的時候還要重新看一遍,非常的浪費時間。

下面以IOC為例說明下我是怎麼看的,供參考。

IOC

入口:ApplicationContext

在研究原始碼時首先要找到一個入口,這個入口怎麼選擇可以自己定,當一定要和你需要看的模組有關聯。

比如在IOC中,首先我們想到建立容器是在什麼過程?

在程式啟動的時候就建立了,而且在啟動過程中大多數的bean例項就被注入了。

那問題來了,在啟動的時候是從那個類開始的呢?熟悉spring的應該都知道我們平時在做單元測試時如果要獲取bean例項,一個是通過註解,另外我們還可以通過構建一個ApplicationContext來獲取:

    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath*:application.xml");
    XxxService xxxService = applicationContext.getBean("xxxService");

在例項化ApplicationContext後既可以獲取bean,那麼例項化的這個過程就相當於啟動的過程了,所以我們可以將ApplicationContext當成我們的入口。

ApplicationContext是什麼

首先我們要明白的事我們平時一直說的IOC容器在Spring中實際上指的就是ApplicationContext

如果有看過我之前手寫Spring系列文章的同學肯定知道在當時文章中充當ioc容器的是BeanFactory,每當有bean需要注入時都是由BeanFactory儲存,取bean例項時也是從BeanFactory中獲取。

那為什麼現在要說ApplicationContext才是IOC容器呢?

因為在spring中BeanFactory實際上是被隱藏了的。ApplicationContext是對BeanFactory的一個封裝,也提供了獲取bean例項等功能。因為BeanFactory本身的能力實在太強,如果可以讓我們隨便使用可能會對spring功能的執行造成破壞。於是就封裝了一個提供查詢ioc容器內容的ApplicationContext供我們使用。

如果專案中需要用到ApplicationContext,可以直接使用spring提供的註解獲取:

    @Autowired
    private ApplicationContext applicationContext;

如何使用ApplicationContext

如果我們要使用ApplicationContext可以通過new該類的一個例項即可,定義好相應的xml檔案。然後通過下面的程式碼即可:

    @Test
    public void testClassPathXmlApplicationContext() {
        //1.準備配置檔案,從當前類載入路徑中獲取配置檔案
        //2.初始化容器
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath*:application.xml");
        //2、從容器中獲取Bean
        HelloApi helloApi = applicationContext.getBean("hello", HelloApi.class);
        //3、執行業務邏輯
        helloApi.sayHello();
    }

ApplicationContext的體系

瞭解一個類,首先可以來看看它的繼承關係來了解其先天的提供哪些功能。然後在看其本身又實現了哪些功能。

ApplicationContext繼承體系

上圖中繼承關係從左至右簡要介紹其功能。

  • ApplicationEventPublisher:提供釋出監聽事件的功能,接收一個監聽事件實體作為引數。需要了解的可以通過這篇文章:事件監聽
  • ResourcePatternResolver:用於解析一些傳入的檔案路徑(比如ant風格的路徑),然後將檔案載入為resource。
  • HierarchicalBeanFactory:提供父子容器關係,保證子容器能訪問父容器,父容器無法訪問子容器。
  • ListableBeanFactory:繼承自BeanFactory,提供訪問IOC容器的方法。
  • EnvironmentCapable:獲取環境變數相關的內容。
  • MessageSource:提供國際化的message的解析

配置檔案的載入

Spring中每一個功能都是很大的一個工程,所以在閱讀時也要分為多個模組來理解。要理解IOC容器,我們首先需要了解spring是如何載入配置檔案的。

縱覽大局

idea或者eclipse提供了一個很好的功能就是能在除錯模式下看到整個流程的呼叫鏈。利用這個功能我們可以直接觀察到某一功能實現的整體流程,也方便在閱讀程式碼時在不同類切換。

以載入配置檔案為例,這裡給出整個呼叫鏈。

配置檔案載入流程

上圖中下面的紅框是我們寫的程式碼,即就是我們應該開始的地方。下面的紅框就是載入配置檔案結束的地方。中間既是整體流程的實現過程。在閱讀配置檔案載入的原始碼時我們只需要關心這一部分的內容即可。

需要知道的是這裡展示出來的僅僅只是跟這個過程密切相關的一些方法。實際上在這個過程中還有需要的方法被執行,只不過執行完畢後方法棧彈出所以不顯示在這裡。不過大多數方法都是在為這個流程做準備,所以基本上我們也不用太在意這部分內容

refresh()

前面的關於ClassPathXmlApplicationContext的建構函式部分沒有啥好說的,在建構函式中呼叫了一個方法AbstractApplicationContext#refresh。該方法非常重要,在建立IOC容器的過程中該方法基本上是全程參與。主要功能為用於載入配置或這用於重新整理已經載入完成的容器配置。通過該方法可以在執行過程中動態的加入配置檔案等:

    ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext();
    ctx.setConfigLocation("application-temp.xml");
    ctx.refresh();

AbstractApplicationContext#refresh

    public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            prepareRefresh();

            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
            // more statement ...
        }
    }

這裡將於當前功能不相關的部分刪除掉了,可以看到進入方法後就會進入一個同步程式碼塊。這是為了防止在同一時間有多個執行緒開始建立IOC容器造成重複例項化。

prepareRefresh();方法主要用於設定一些日誌相關的資訊,比如容器啟動時間用於計算啟動容器整體用時,以及設定一些變數用來標識當前容器已經被啟用,後續不會再進行建立。


#### refreshBeanFactory
refreshBeanFactory方法有obtainFreshBeanFactory方法呼叫

protected final void refreshBeanFactory() throws BeansException {
    if (hasBeanFactory()) {
        destroyBeans();
        closeBeanFactory();
    }
    try {
        DefaultListableBeanFactory beanFactory = createBeanFactory();
        beanFactory.setSerializationId(getId());
        customizeBeanFactory(beanFactory);
        loadBeanDefinitions(beanFactory);
        synchronized (this.beanFactoryMonitor) {
            this.beanFactory = beanFactory;
        }
    }catch (IOException ex) {
        throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
    }
}

```

該方法首先判斷是否已經例項化好BeanFactory,如果已經例項化完成則將已經例項化好的BeanFactory銷燬。

然後通過new關鍵字建立一個BeanFactory的實現類例項,設定好相關資訊。customizeBeanFactory(beanFactory)方法用於設定是否執行當beanName重複是修改bean的名稱(allowBeanDefinitionOverriding)和是否執行迴圈引用(allowCircularReferences)。


#### loadBeanDefinitions

protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
    XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);

    beanDefinitionReader.setEnvironment(this.getEnvironment());
    beanDefinitionReader.setResourceLoader(this);
    beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

    initBeanDefinitionReader(beanDefinitionReader);
    loadBeanDefinitions(beanDefinitionReader);
}

```


在spring中如果是完成一些類似操作的類的命名都是有跡可循的,比如這裡讀取xml檔案就是以reader結尾,類似的讀取註解中bean定義也有如```AnnotatedBeanDefinitionReader```。如果需要向類中注入一些Spring中的bean,一般是以Aware結尾如```BeanFactoryAware```等。所以在閱讀spring原始碼時如果遇到這樣的類很多時候我們可以直接根據其命名瞭解其大概的實現方式。
public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException {
    ResourceLoader resourceLoader = getResourceLoader();
    if (resourceLoader == null) {
        throw new BeanDefinitionStoreException(
                "Cannot import bean definitions from location [" + location + "]: no ResourceLoader available");
    }

    if (resourceLoader instanceof ResourcePatternResolver) {
        try {
            Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
            int loadCount = loadBeanDefinitions(resources);
            if (actualResources != null) {
                for (Resource resource : resources) {
                    actualResources.add(resource);
                }
            }
            //logging
            return loadCount;
        }catch (IOException ex) {
            throw new BeanDefinitionStoreException(
                    "Could not resolve bean definition resource pattern [" + location + "]", ex);
        }
    }else {
        Resource resource = resourceLoader.getResource(location);
        int loadCount = loadBeanDefinitions(resource);
        if (actualResources != null) {
            actualResources.add(resource);
        }
        //logging
        return loadCount;
    }
}
上面程式碼是```loadBeanDefinitions```的一個實現類,該方法的主要注意點在於三個地方。

一個是方法中丟擲的兩個異常,前一個異常時因為ResourceLoader定義的問題,一般來說不需要我們關注。後一個就是配置檔案出錯了,可能是因為檔案本身xml格式出錯或者是由於迴圈引用等原因,具體的原因也會通過日誌列印。我們需要對這些異常資訊有印象,也不用刻意去記,遇到了能快速定位問題即可。

另一個就是程式碼中的一個```if(){}else{}```語句塊,判斷語句快中都是用於解析配置檔案,不同之處在於if中支援解析匹配風格的location,比如```classpath*:spring.xml```這種,該功能的實現由```ResourcePatternResolver```提供,```ResourcePatternResolver```對```ResourceLoader```的功能進行了增強,支援解析ant風格等模式的location。而else中僅僅只能解析指定的某一檔案如```spring.xml```這種。實際上在```ApplicationContext```中實現了```ResourcePatternResolver```,如果也按照```spring.xml```配置,也是按照```ResourceLoader```提供的解析方式解析。


最後一處就是```Resource```類,```Resource```是spring為了便於載入檔案而特意設計的介面。其提供了大量對傳入的location操作方法,支援對不同風格的location(比如檔案系統或者ClassPath)。其本身還有許多不同的實現類,本質上是對```File,URL,ClassPath```等不同方式獲取location的一個整合,功能十分強大。即使我們的專案不依賴spring,如果涉及到Resource方面的操作也可以使用Spring中的Resource。
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
    //log and assert
    Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
    if (currentResources == null) {
        currentResources = new HashSet<EncodedResource>(4);
        this.resourcesCurrentlyBeingLoaded.set(currentResources);
    }
    if (!currentResources.add(encodedResource)) {
        throw new BeanDefinitionStoreException(
                "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
    }
    try {
        InputStream inputStream = encodedResource.getResource().getInputStream();
        try {
            InputSource inputSource = new InputSource(inputStream);
            if (encodedResource.getEncoding() != null) {
                inputSource.setEncoding(encodedResource.getEncoding());
            }
            return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
        }
        finally {
            inputStream.close();
        }
    }catch (IOException ex) {
        throw new BeanDefinitionStoreException(
                "IOException parsing XML document from " + encodedResource.getResource(), ex);
    }finally {
        currentResources.remove(encodedResource);
        if (currentResources.isEmpty()) {
            this.resourcesCurrentlyBeingLoaded.remove();
        }
    }
}
該方法依舊是loadBeanDefinitions的過載方法。

方法傳入一個EncodedResource,該類可以通過制定的字符集對Resource進行編碼,利於統一字元編碼格式。

然後try語句塊上面的程式碼也是比較重要的,主要功能便是判斷是否有配置檔案存在迴圈引用的問題。

迴圈應用問題出現在比如我載入一個配置檔案```application.xml```,但是在該檔案內部又通過```import```標籤引用了自身。在解析到```import```時會載入```import```指定的檔案。這樣就造成了一個死迴圈,如果不解決程式就會永遠啟動不起來。

解決的方法也很簡單,通過一個```ThreadLocal```記錄下當前正在載入的配置檔名稱(包括路徑),每一次在載入新的配置檔案時從```ThreadLocal```中取出放入到set集合中,通過set自動去重的特性判斷是否迴圈載入了。當一個檔案載入完成後,就從```ThreadLocal```中去掉(finally)。這裡是判斷xml檔案時否重複載入,而在spring中判斷bean是否迴圈引用是雖然實現上有點差別,但基本思想也是這樣的。

#### doLoadBeanDefinitions(InputSource, Resource)

到了這一步基本上才算是真正開始解析了。該方法雖然程式碼行數較多,但是大多都是異常處理,異常程式碼已經省略。我們需要關注的就是try中的兩句程式碼。
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
        throws BeanDefinitionStoreException {
    try {
        Document doc = doLoadDocument(inputSource, resource);
        return registerBeanDefinitions(doc, resource);
    }catch (Exception ex) {
        //多個catch語句塊
    }
}

```

Document doc = doLoadDocument(inputSource, resource)就是讀取配置檔案並將其內容解析為一個Document的過程。解析xml一般來說並不需要我們特別的去掌握,稍微有個瞭解即可,spring這裡使用的解析方式為Sax解析,有興趣的可以直接搜尋相關文章,這裡不進行介紹。下面的registerBeanDefinitions才是我們需要關注的地方。

registerBeanDefinitions(Document, Resource)

    public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
        BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
        documentReader.setEnvironment(getEnvironment());
        int countBefore = getRegistry().getBeanDefinitionCount();
        documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
        return getRegistry().getBeanDefinitionCount() - countBefore;
    }

在進入該方法後首先建立了一個BeanDefinitionDocumentReader的例項,這和之前的用於讀取xml的reader類一樣,只不過該類是用於從xml檔案中讀取BeanDefinition

Environment

在上面的程式碼中給Reader設定了Environment,這裡談一下關於Environment。

Environment是對spring程式中涉及到環境有關的一個描述集合,主要分為profile和properties。

profile是一組bean定義的集合,通過profile可以指定不同的配置檔案用以在不同的環境中,如測試環境,生產環境的配置分開。在部署時只需要配置好當前所處環境值即可按不同分類載入不同的配置。

profile支援xml配置和註解的方式。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    //more >

    <!-- 定義開發環境的profile -->
    <beans profile="development">
        <!-- 只掃描開發環境下使用的類 -->
        <context:component-scan base-package="com.demo.service" />
        <!-- 載入開發使用的配置檔案 -->
        <util:properties id="config" location="classpath:dev/config.properties"/>
    </beans>

    <!-- 定義生產環境的profile -->
    <beans profile="produce">
        <!-- 只掃描生產環境下使用的類 -->
        <context:component-scan
            base-package="com.demo.service" />
        <!-- 載入生產使用的配置檔案 -->    
        <util:properties id="config" location="classpath:produce/config.properties"/>
    </beans>
</beans>

也可以通過註解配置:

@Service
@Profile("dev")
public class ProductRpcImpl implements ProductRpc {
    public String productBaseInfo(int id) {
        return "success";
    }
}

然後在啟動時根據傳入的環境值載入相應的配置。

properties是一個很寬泛的定義,其來源很多如properties檔案,JVM系統變數,系統環境變數,JNDI,servlet上下文引數,Map等。spring會讀取這些配置並在environment介面中提供了方便對其進行操作的方法。

總之就是設計到跟環境有關的直接來找Environment即可。

handler

程式碼接著往下走,documentReader.registerBeanDefinitions(doc, createReaderContext(resource))這一步很明顯就是從解析好的document物件中讀取BeanDefinition的過程,但是在此之前我們先要關注一下createReaderContext(resource)方法。

先來看一個XML檔案。

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc" >
</beans>

上面是xml中根元素定義部分,可能平時並沒有太多人注意。其屬性中的xmlns是XML NameSpace的縮寫。namespace的作用主要是為了防止在xml定義的節點存在衝突的問題。比如上面宣告瞭mvc的namespace:xmlns:mvc="http://www.springframework.org/schema/mvc"。在xml檔案中我們就可以使用mvc了:

    <mvc:annotation-driven />
    <mvc:default-servlet-handler/>

而實際上在spring中還根據上面定義的namespace來準備了各自的處理類。這裡因為解析過程就是將xml定義的每一個節點取出根據配置好的屬性和值來初始化或註冊bean,為了保證程式碼可讀性和明確的分工,每一個namespace通過一個專有的handler來處理。

跟蹤createReaderContext(resource)方法,最終來到DefaultNamespaceHandlerResolver類的構造方法中。

handler匹配

    public DefaultNamespaceHandlerResolver(ClassLoader classLoader) {
        //DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers"
        this(classLoader, DEFAULT_HANDLER_MAPPINGS_LOCATION);
    }

可以看到預設的handler是通過一個本地檔案來進行對映的。該檔案存在於被依賴jar包下的META-INF資料夾下的spring.handlers檔案中。

handler mapping

http\://www.springframework.org/schema/c=org.springframework.beans.factory.xml.SimpleConstructorNamespaceHandler
http\://www.springframework.org/schema/p=org.springframework.beans.factory.xml.SimplePropertyNamespaceHandler
http\://www.springframework.org/schema/util=org.springframework.beans.factory.xml.UtilNamespaceHandler

這裡只是展示了beans包下的對映檔案,其他如aop包,context包下都有相應的對映檔案。通過讀取這些配置檔案來對映相應的處理類。在解析xml時會根據使用的namespace字首使用對應的handler類解析。這種實現機制其實就是所謂的SPI(Service Provider Interface),目前很多的應用都在實現過程中使用了spi,如dubbo,mysql的jdbc實現等,有興趣的可以取了解一下。

doRegisterBeanDefinitions(Element)

到這一步中間省略了一個方法,很簡單沒有分析的必要。

    protected void doRegisterBeanDefinitions(Element root) {
        BeanDefinitionParserDelegate parent = this.delegate;
        this.delegate = createDelegate(getReaderContext(), root, parent);
        if (this.delegate.isDefaultNamespace(root)) {
            String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
            if (StringUtils.hasText(profileSpec)) {
                String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
                        profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
                if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
                    return;
                }
            }
        }
        preProcessXml(root);
        parseBeanDefinitions(root, this.delegate);
        postProcessXml(root);

        this.delegate = parent;
    }
delegate

這裡的delegate是對BeanDefinitionParse功能的代理,提供了一些支援解析過程的方法。我們可以看到上面有一個重新建立delegate同時又將之前的delegate儲存的程式碼。註釋上說是為了防止巢狀的beans標籤遞迴操作導致出錯,但是註釋後面又說這並不需要這樣處理,這個操作真的看不懂了,實際上我認為即使遞迴應該也是沒有影響的。還是我理解錯了?

建立好delegate後下面的if語句塊就是用來判斷當前載入的配置檔案是否是當前使用的profile指定的配置檔案。上面在介紹Environment的時候已經介紹過來,如果這裡載入的配置檔案和profile指定的不符則直接結束。


#### parseBeanDefinitions(Element, BeanDefinitionParserDelegate)

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    if (delegate.isDefaultNamespace(root)) {
        NodeList nl = root.getChildNodes();
        for (int i = 0; i < nl.getLength(); i++) {
            Node node = nl.item(i);
            if (node instanceof Element) {
                Element ele = (Element) node;
                if (delegate.isDefaultNamespace(ele)) {
                    parseDefaultElement(ele, delegate);
                }
                else {
                    delegate.parseCustomElement(ele);
                }
            }
        }
    }
    else {
        delegate.parseCustomElement(root);
    }
}
該方法的理解起來並不難,首先讀取根節點(beans)下的所有子節點,然後對這些節點進行解析。這裡需要注意的即使是對節點的解析也有一個判斷語句。

主要來看一下```delegate.isDefaultNamespace(ele)```,

public boolean isDefaultNamespace(String namespaceUri) {
//BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans"
return (!StringUtils.hasLength(namespaceUri) || BEANS_NAMESPACE_URI.equals(namespaceUri));
}

也就是說beans名稱空間下的標籤一個解析方法,而另外的標籤一個解析方法。

#### parseDefaultElement(Element, BeanDefinitionParserDelegate)
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
    if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {//import
        importBeanDefinitionResource(ele);
    }
    else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {//alias
        processAliasRegistration(ele);
    }
    else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {//bean
        processBeanDefinition(ele, delegate);
    }
    else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
        // recurse
        doRegisterBeanDefinitions(ele);
    }
}
可以看到對於每一個標籤,都提供了一個方法來進行解析,最後一個方法用於對巢狀標籤進行解析,這裡以bean標籤的解析為例。

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
    BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
    if (bdHolder != null) {
        //用於將一些屬性值塞進BeanDefinition中如lazy-init
        //以及子節點中的值 如bean節點下的property
        bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
        try {
            BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
        }
        catch (BeanDefinitionStoreException ex) {
            getReaderContext().error("Failed to register bean definition with name '" +
                    bdHolder.getBeanName() + "'", ele, ex);
        }
        getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
    }
}

Holder物件也是spring中的一個系列性的物件,主要就是對某一些例項進行包裝,比如```BeanDefinitionHolder```就是對```BeanDefinition```進行包裝,主要就是持有BeanDefinition以及它的名稱和別名等(BeanDefinition為介面,無法提供名稱等屬性)。

public static void registerBeanDefinition(
        BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
        throws BeanDefinitionStoreException {

    String beanName = definitionHolder.getBeanName();
    registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

    String[] aliases = definitionHolder.getAliases();
    if (aliases != null) {
        for (String alias : aliases) {
            registry.registerAlias(beanName, alias);
        }
    }
}

```

接下來的BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry())可以算是我們本次目標最重要的一步了,前面所有的流程都是給這一步鋪墊。通過該方法將解析出來的BeanDefinition註冊到容器中,方便例項化。

方法接收兩個引數holder和registry,如果有看過我手寫系列的文章(IOC篇)應該知道,當時為了將bean定義和容器關聯起來以及為了將beanfactory的功能簡化,所以我們定義了一個BeanDefinitionRegistry介面用於將BeanDefinition註冊到容器中和從容器中取BeanDefinition,這裡的registry功能也是一樣的。(BeanDefinitionRegistryDefaultListableBeanFactory實現,而DefaultListableBeanFactory實際就是容器)

而且可以看到實際上這裡也是通過beanName來區分BeanDefinition是否重複(實際上肯定是我仿的spring的(笑)),只不過為了執行名稱相同的BeanDefinition註冊提供了alias,之前在實現ioc時沒有實現這一步。

processBeanDefinition方法的最後一步實際上是註冊了一個listener,在一個BeanDefinition被註冊後觸發,只不過上spring中實際觸發方法是一個空方法,如果我們需要在BeanDefinition註冊完成後做一些什麼工作可以直接繼承EmptyReaderEventListener後實現componentRegistered(componentDefinition)方法即可。

到這裡基本上關於BeanDefinition的載入就完成了,後面就是重複上面的流程載入多個配置檔案。

小結

本節主要介紹了我關於學習spring原始碼的一些方法,以及以spring的BeanDefinition的載入為例分析了其整體的流程,希望對大家能有所幫助。還要提的是spring原始碼很複雜,如果只是開斷點一路除錯下去肯定是不夠的,看的過程中需要多做筆記。由於該文章內容較多以及本人水平問題,文章中可能會存在錯誤,如果有可以指出來方便修改。

相關文章