歡迎光臨
每天分享高質量文章

【死磕 Spring】—– IOC 之 IOC 初始化總結

點選上方“Java技術驛站”,選擇“置頂公眾號”。

有內涵、有價值的文章第一時間送達!

精品專欄

 

前面 13 篇博文從原始碼層次分析了 IOC 整個初始化過程,這篇就這些內容做一個總結將其連貫起來。

在前文提過,IOC 容器的初始化過程分為三步驟:Resource 定位、BeanDefinition 的載入和解析,BeanDefinition 註冊。

  • Resource 定位。我們一般用外部資源來描述 Bean 物件,所以在初始化 IOC 容器的第一步就是需要定位這個外部資源。

  • BeanDefinition 的載入和解析。裝載就是 BeanDefinition 的載入。BeanDefinitionReader 讀取、解析 Resource 資源,也就是將使用者定義的 Bean 表示成 IOC 容器的內部資料結構:BeanDefinition。在 IOC 容器內部維護著一個 BeanDefinition Map 的資料結構,在配置檔案中每一個都對應著一個BeanDefinition物件。

  • BeanDefinition 註冊。向IOC容器註冊在第二步解析好的 BeanDefinition,這個過程是透過 BeanDefinitionRegistery 介面來實現的。在 IOC 容器內部其實是將第二個過程解析得到的 BeanDefinition 註入到一個 HashMap 容器中,IOC 容器就是透過這個 HashMap 來維護這些 BeanDefinition 的。在這裡需要註意的一點是這個過程並沒有完成依賴註入,依賴註冊是發生在應用第一次呼叫 getBean() 向容器索要 Bean 時。當然我們可以透過設定預處理,即對某個 Bean 設定 lazyinit 屬性,那麼這個 Bean 的依賴註入就會在容器初始化的時候完成。

還記得在部落格【死磕 Spring】—– IOC 之 載入 Bean 中提供的一段程式碼嗎?這裡我們同樣也以這段程式碼作為我們研究 IOC 初始化過程的開端,如下:

  1. ClassPathResource resource = new ClassPathResource("bean.xml");

  2. DefaultListableBeanFactory factory = new DefaultListableBeanFactory();

  3. XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);

  4. reader.loadBeanDefinitions(resource);

剛剛開始的時候可能對上面這幾行程式碼不知道什麼意思,現在應該就一目瞭然了。

  • ClassPathResourceresource=newClassPathResource("bean.xml");: 根據 Xml 配置檔案建立 Resource 資源物件。ClassPathResource 是 Resource 介面的子類,bean.xml 檔案中的內容是我們定義的 Bean 資訊。

  • DefaultListableBeanFactoryfactory=newDefaultListableBeanFactory(); 建立一個 BeanFactory。DefaultListableBeanFactory 是 BeanFactory 的一個子類,BeanFactory 作為一個介面,其實它本身是不具有獨立使用的功能的,而 DefaultListableBeanFactory 則是真正可以獨立使用的 IOC 容器,它是整個 Spring IOC 的始祖,在後續會有專門的文章來分析它。

  • XmlBeanDefinitionReaderreader=newXmlBeanDefinitionReader(factory);:建立 XmlBeanDefinitionReader 讀取器,用於載入 BeanDefinition 。

  • reader.loadBeanDefinitions(resource);:開啟 Bean 的載入和註冊行程,完成後的 Bean 放置在 IOC 容器中。

Resource 定位

Spring 為瞭解決資源定位的問題,提供了兩個介面:Resource、ResourceLoader,其中 Resource 介面是 Spring 統一資源的抽象介面,ResourceLoader 則是 Spring 資源載入的統一抽象。關於Resource、ResourceLoader 的更多知識請關註【死磕 Spring】—– IOC 之 Spring 統一資源載入策略

Resource 資源的定位需要 Resource 和 ResourceLoader 兩個介面互相配合,在上面那段程式碼中 newClassPathResource("bean.xml") 為我們定義了資源,那麼 ResourceLoader 則是在什麼時候初始化的呢?看 XmlBeanDefinitionReader 構造方法:

  1.    public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {

  2.        super(registry);

  3.    }

直接呼叫父類 AbstractBeanDefinitionReader :

  1.    protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) {

  2.        Assert.notNull(registry, "BeanDefinitionRegistry must not be null");

  3.        this.registry = registry;

  4.        // Determine ResourceLoader to use.

  5.        if (this.registry instanceof ResourceLoader) {

  6.            this.resourceLoader = (ResourceLoader) this.registry;

  7.        }

  8.        else {

  9.            this.resourceLoader = new PathMatchingResourcePatternResolver();

  10.        }

  11.        // Inherit Environment if possible

  12.        if (this.registry instanceof EnvironmentCapable) {

  13.            this.environment = ((EnvironmentCapable) this.registry).getEnvironment();

  14.        }

  15.        else {

  16.            this.environment = new StandardEnvironment();

  17.        }

  18.    }

核心在於設定 resourceLoader 這段,如果設定了 ResourceLoader 則用設定的,否則使用 PathMatchingResourcePatternResolver ,該類是一個集大成者的 ResourceLoader。

BeanDefinition 的載入和解析

reader.loadBeanDefinitions(resource); 開啟 BeanDefinition 的解析過程。如下:

  1.    public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {

  2.        return loadBeanDefinitions(new EncodedResource(resource));

  3.    }

在這個方法會將資源 resource 包裝成一個 EncodedResource 實體物件,然後呼叫 loadBeanDefinitions() 方法,而將 Resource 封裝成 EncodedResource 主要是為了對 Resource 進行編碼,保證內容讀取的正確性。

  1.   public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {

  2.        // 省略一些程式碼

  3.        try {

  4.            // 將資源檔案轉為 InputStream 的 IO 流

  5.            InputStream inputStream = encodedResource.getResource().getInputStream();

  6.            try {

  7.                // 從 InputStream 中得到 XML 的解析源

  8.                InputSource inputSource = new InputSource(inputStream);

  9.                if (encodedResource.getEncoding() != null) {

  10.                    inputSource.setEncoding(encodedResource.getEncoding());

  11.                }

  12.                // 具體的讀取過程

  13.                return doLoadBeanDefinitions(inputSource, encodedResource.getResource());

  14.            }

  15.            finally {

  16.                inputStream.close();

  17.            }

  18.        }

  19.        // 省略一些程式碼

  20.    }

從 encodedResource 源中獲取 xml 的解析源,呼叫 doLoadBeanDefinitions() 執行具體的解析過程。

  1.    protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)

  2.            throws BeanDefinitionStoreException {

  3.        try {

  4.            Document doc = doLoadDocument(inputSource, resource);

  5.            return registerBeanDefinitions(doc, resource);

  6.        }

  7.        // 省略很多catch程式碼

在該方法中主要做兩件事:1、根據 xml 解析源獲取相應的 Document 物件,2、呼叫 registerBeanDefinitions() 開啟 BeanDefinition 的解析註冊過程。

轉換為 Document 物件

呼叫 doLoadDocument() 會將 Bean 定義的資源轉換為 Document 物件。

  1.    protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {

  2.        return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,

  3.                getValidationModeForResource(resource), isNamespaceAware());

  4.    }

loadDocument() 方法接受五個引數:

  • inputSource:載入 Document 的 Resource 源

  • entityResolver:解析檔案的解析器

  • errorHandler:處理載入 Document 物件的過程的錯誤

  • validationMode:驗證樣式

  • namespaceAware:名稱空間支援。如果要提供對 XML 名稱空間的支援,則為true

對於這五個引數,有兩個引數需要重點關註下:entityResolver、validationMode。這兩個引數分別在 【死磕Spring】----- IOC 之 獲取 Document 物件【死磕Spring】----- IOC 之 獲取驗證模型 中有詳細的講述。

loadDocument() 在類 DefaultDocumentLoader 中提供了實現,如下:

  1.    public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,

  2.            ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {

  3.      // 建立檔案解析工廠

  4.        DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);

  5.        if (logger.isDebugEnabled()) {

  6.            logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");

  7.        }

  8.        // 建立檔案解析器

  9.        DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);

  10.        // 解析 Spring 的 Bean 定義資源

  11.        return builder.parse(inputSource);

  12.    }

這到這裡,就已經將定義的 Bean 資源檔案,載入並轉換為 Document 物件了,那麼下一步就是如何將其解析為 Spring IOC 管理的 Bean 物件並將其註冊到容器中。這個過程有方法 registerBeanDefinitions() 實現。如下:

  1.    public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {

  2.       // 建立 BeanDefinitionDocumentReader 來對 xml 格式的BeanDefinition 解析

  3.        BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();

  4.        // 獲得容器中註冊的Bean數量

  5.        int countBefore = getRegistry().getBeanDefinitionCount();

  6.        // 解析過程入口,這裡使用了委派樣式,BeanDefinitionDocumentReader只是個介面,

  7.        // 具體的解析實現過程有實現類DefaultBeanDefinitionDocumentReader完成

  8.        documentReader.registerBeanDefinitions(doc, createReaderContext(resource));

  9.        return getRegistry().getBeanDefinitionCount() - countBefore;

  10.    }

首先建立 BeanDefinition 的解析器 BeanDefinitionDocumentReader,然後呼叫 documentReader.registerBeanDefinitions() 開啟解析過程,這裡使用的是委派樣式,具體的實現由子類 DefaultBeanDefinitionDocumentReader 完成。

  1.    public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {

  2.       // 獲得XML描述符

  3.        this.readerContext = readerContext;

  4.        logger.debug("Loading bean definitions");

  5.        // 獲得Document的根元素

  6.        Element root = doc.getDocumentElement();

  7.        // 解析根元素

  8.        doRegisterBeanDefinitions(root);

  9.    }

對 Document 物件的解析

從 Document 物件中獲取根元素 root,然後呼叫 doRegisterBeanDefinitions() 開啟真正的解析過程。

  1.    protected void doRegisterBeanDefinitions(Element root) {

  2.        BeanDefinitionParserDelegate parent = this.delegate;

  3.        this.delegate = createDelegate(getReaderContext(), root, parent);

  4.       // 省略部分程式碼

  5.        preProcessXml(root);

  6.        parseBeanDefinitions(root, this.delegate);

  7.        postProcessXml(root);

  8.        this.delegate = parent;

  9.    }

preProcessXml()、 postProcessXml() 為前置、後置增強處理,目前 Spring 中都是空實現, parseBeanDefinitions() 是對根元素 root 的解析註冊過程。

  1.    protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {

  2.       // Bean定義的Document物件使用了Spring預設的XML名稱空間

  3.        if (delegate.isDefaultNamespace(root)) {

  4.          // 獲取Bean定義的Document物件根元素的所有子節點

  5.            NodeList nl = root.getChildNodes();

  6.            for (int i = 0; i < nl.getLength(); i++) {

  7.                Node node = nl.item(i);

  8.                // 獲得Document節點是XML元素節點

  9.                if (node instanceof Element) {

  10.                    Element ele = (Element) node;

  11.                    // Bean定義的Document的元素節點使用的是Spring預設的XML名稱空間

  12.                    if (delegate.isDefaultNamespace(ele)) {

  13.                       // 使用Spring的Bean規則解析元素節點(預設解析規則)

  14.                        parseDefaultElement(ele, delegate);

  15.                    }

  16.                    else {

  17.                       // 沒有使用Spring預設的XML名稱空間,則使用使用者自定義的解析規則解析元素節點

  18.                        delegate.parseCustomElement(ele);

  19.                    }

  20.                }

  21.            }

  22.        }

  23.        else {

  24.          // Document 的根節點沒有使用Spring預設的名稱空間,則使用使用者自定義的解析規則解析

  25.            delegate.parseCustomElement(root);

  26.        }

  27.    }

迭代 root 元素的所有子節點,對其進行判斷,若節點為預設名稱空間,則ID呼叫 parseDefaultElement() 開啟預設標簽的解析註冊過程,否則呼叫 parseCustomElement() 開啟自定義標簽的解析註冊過程。

標簽解析

若定義的元素節點使用的是 Spring 預設名稱空間,則呼叫 parseDefaultElement() 進行預設標簽解析,如下:

  1.    private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {

  2.       // 如果元素節點是匯入元素,進行匯入解析

  3.        if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {

  4.            importBeanDefinitionResource(ele);

  5.        }

  6.        // 如果元素節點是別名元素,進行別名解析

  7.        else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {

  8.            processAliasRegistration(ele);

  9.        }

  10.        // 如果元素節點元素,則進行Bean解析註冊

  11.        else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {

  12.            processBeanDefinition(ele, delegate);

  13.        }

  14.        // // 如果元素節點元素,則進行Beans解析

  15.        else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {

  16.            // recurse

  17.            doRegisterBeanDefinitions(ele);

  18.        }

  19.    }

對四大標簽:import、alias、bean、beans 進行解析,其中 bean 標簽的解析為核心工作。關於各個標簽的解析過程見如下文章:

對於預設標簽則由 parseCustomElement() 負責解析。

  1.    public BeanDefinition parseCustomElement(Element ele) {

  2.        return parseCustomElement(ele, null);

  3.    }

  4.    public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {

  5.        String namespaceUri = getNamespaceURI(ele);

  6.        if (namespaceUri == null) {

  7.            return null;

  8.        }

  9.        NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);

  10.        if (handler == null) {

  11.            error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);

  12.            return null;

  13.        }

  14.        return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));

  15.    }

獲取節點的 namespaceUri,然後根據該 namespaceuri 獲取相對應的 Handler,呼叫 Handler 的 parse() 方法即完成自定義標簽的解析和註入。想瞭解更多參考:【死磕Spring】----- IOC 之解析自定義標簽

註冊 BeanDefinition

經過上面的解析,則將 Document 物件裡面的 Bean 標簽解析成了一個個的 BeanDefinition ,下一步則是將這些 BeanDefinition 註冊到 IOC 容器中。動作的觸發是在解析 Bean 標簽完成後,如下:

  1.    protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {

  2.        BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);

  3.        if (bdHolder != null) {

  4.            bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);

  5.            try {

  6.                // Register the final decorated instance.

  7.                BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());

  8.            }

  9.            catch (BeanDefinitionStoreException ex) {

  10.                getReaderContext().error("Failed to register bean definition with name '" +

  11.                        bdHolder.getBeanName() + "'", ele, ex);

  12.            }

  13.            // Send registration event.

  14.            getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));

  15.        }

  16.    }

呼叫 BeanDefinitionReaderUtils.registerBeanDefinition() 註冊,其實這裡面也是呼叫 BeanDefinitionRegistry 的 registerBeanDefinition()來註冊 BeanDefinition ,不過最終的實現是在 DefaultListableBeanFactory 中實現,如下:

  1.    @Override

  2.    public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)

  3.            throws BeanDefinitionStoreException {

  4.      // 省略一堆校驗

  5.        BeanDefinition oldBeanDefinition;

  6.        oldBeanDefinition = this.beanDefinitionMap.get(beanName);

  7.          // 省略一堆 if

  8.            this.beanDefinitionMap.put(beanName, beanDefinition);

  9.        }

  10.        else {

  11.            if (hasBeanCreationStarted()) {

  12.                // Cannot modify startup-time collection elements anymore (for stable iteration)

  13.                synchronized (this.beanDefinitionMap) {

  14.                    this.beanDefinitionMap.put(beanName, beanDefinition);

  15.                    List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);

  16.                    updatedDefinitions.addAll(this.beanDefinitionNames);

  17.                    updatedDefinitions.add(beanName);

  18.                    this.beanDefinitionNames = updatedDefinitions;

  19.                    if (this.manualSingletonNames.contains(beanName)) {

  20.                        Set<String> updatedSingletons = new LinkedHashSet<>(this.manualSingletonNames);

  21.                        updatedSingletons.remove(beanName);

  22.                        this.manualSingletonNames = updatedSingletons;

  23.                    }

  24.                }

  25.            }

  26.            else {

  27.                // Still in startup registration phase

  28.                this.beanDefinitionMap.put(beanName, beanDefinition);

  29.                this.beanDefinitionNames.add(beanName);

  30.                this.manualSingletonNames.remove(beanName);

  31.            }

  32.            this.frozenBeanDefinitionNames = null;

  33.        }

  34.        if (oldBeanDefinition != null || containsSingleton(beanName)) {

  35.            resetBeanDefinition(beanName);

  36.        }

  37.    }

這段程式碼最核心的部分是這句 this.beanDefinitionMap.put(beanName,beanDefinition) ,所以註冊過程也不是那麼的高大上,就是利用一個 Map 的集合物件來存放,key 是 beanName,value 是 BeanDefinition。

至此,整個 IOC 的初始化過程就已經完成了,從 Bean 資源的定位,轉換為 Document 物件,接著對其進行解析,最後註冊到 IOC 容器中,都已經完美地完成了。現在 IOC 容器中已經建立了整個 Bean 的配置資訊,這些 Bean 可以被檢索、使用、維護,他們是控制反轉的基礎,是後面註入 Bean 的依賴。最後用一張流程圖來結束這篇總結之文。

更多閱讀:

END

我是 Java 技術驛站,感謝有你

>>>>>> 加群交流技術 <<<<<<

贊(0)

分享創造快樂

© 2024 知識星球   網站地圖