萬字解析XML配置對映為BeanDefinition的原始碼

華為雲開發者聯盟發表於2023-11-14

本文分享自華為雲社群《Spring高手之路16——解析XML配置對映為BeanDefinition的原始碼》,作者:磚業洋__。

1. BeanDefinition階段的分析

Spring框架中控制反轉(IOC)容器的BeanDefinition階段的具體步驟,主要涉及到Bean的定義、載入、解析,並在後面進行程式設計式注入和後置處理。這個階段是Spring框架中Bean生命週期的早期階段之一,對於理解整個Spring框架非常關鍵。

  • 載入配置檔案、配置類

在這一步,Spring容器透過配置檔案或配置類來了解需要管理哪些Bean。對於基於XML的配置,通常使用ClassPathXmlApplicationContext或者FileSystemXmlApplicationContext

  • 解析配置檔案、配置類並封裝為BeanDefinition

Spring框架透過使用BeanDefinitionReader例項(如XmlBeanDefinitionReader)來解析配置檔案。解析後,每個Bean配置會被封裝成一個BeanDefinition物件,這個物件包含了類名、作用域、生命週期回撥等資訊。

  • 程式設計式注入額外的BeanDefinition

除了配置檔案定義的Bean,也可以透過程式設計的方式動態新增BeanDefinitionIOC容器中,這增加了靈活性。

  • BeanDefinition的後置處理

BeanDefinition的後置處理是指容器允許使用BeanDefinitionRegistryPostProcessorBeanFactoryPostProcessor來對解析後的BeanDefinition做進一步處理,例如修改Bean的屬性等。

2. 載入xml配置檔案

2.1 XML配置檔案中載入bean的程式碼示例

先給出最簡單的程式碼示例,然後逐步分析

全部程式碼如下:

package com.example.demo.bean;

// HelloWorld.java
public class HelloWorld {
    private String message;

    public void setMessage(String message) {
        this.message = message;
    }

    public void sayHello() {
        System.out.println("Hello, " + message + "!");
    }
}

主程式:

package com.example.demo;

import com.example.demo.bean.HelloWorld;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class DemoApplication {

    public static void main(String[] args) {
        // 建立Spring上下文(容器)
        ClassPathXmlApplicationContext context =
                new ClassPathXmlApplicationContext("ApplicationContext.xml");

        // 從容器中獲取bean,假設我們有一個名為 'helloWorld' 的bean
        HelloWorld helloWorld = context.getBean("helloWorld", HelloWorld.class);

        // 使用bean
        helloWorld.sayHello();

        // 關閉上下文
        context.close();
    }
}

xml檔案

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- Bean定義 -->
    <bean id="helloWorld" class="com.example.demo.bean.HelloWorld">
        <!-- 設定屬性 -->
        <property name="message" value="World"/>
    </bean>

</beans>

執行結果:

萬字解析XML配置對映為BeanDefinition的原始碼

接著我們就從這段程式碼開始分析

2.2 setConfigLocations - 設定和儲存配置檔案路徑

我們還是以Spring 5.3.7的原始碼為例分析

// 建立Spring上下文(容器)
        ClassPathXmlApplicationContext context =
                new ClassPathXmlApplicationContext("ApplicationContext.xml");

這段程式碼,我們利用idea點選去分析,最後在ClassPathXmlApplicationContext的過載方法裡看到呼叫了setConfigLocations設定配置檔案的路徑。

萬字解析XML配置對映為BeanDefinition的原始碼

接著看看setConfigLocations方法

萬字解析XML配置對映為BeanDefinition的原始碼

setConfigLocations() 方法的主要作用是設定 Spring 容器載入 Bean 定義時所需要讀取的配置檔案路徑。這些路徑可以是類路徑下的資源、檔案系統中的資源或者其他任何透過URL定位的資源。該方法確保所有提供的配置路徑都被儲存並在稍後的容器重新整理操作中使用。

原始碼提出來分析:

public void setConfigLocations(@Nullable String... locations) {
    if (locations != null) {
        // 使用Spring的Assert類來校驗,確保傳入的配置位置陣列中沒有null元素。
        Assert.noNullElements(locations, "Config locations must not be null");
        
        // 根據傳入的配置位置數量,初始化內部儲存配置位置的陣列。
        this.configLocations = new String[locations.length];

        // 遍歷傳入的配置位置陣列。
        for(int i = 0; i < locations.length; ++i) {
            // 呼叫resolvePath方法處理每一個配置位置(可能進行必要的路徑解析,如解析佔位符)。
            // trim()用於移除字串首尾的空格,保證儲存的路徑是淨化的。
            this.configLocations[i] = this.resolvePath(locations[i]).trim();
        }
    } else {
        // 如果傳入的配置位置是null,清除掉所有已設定的配置位置。
        this.configLocations = null;
    }
}

在上下文被重新整理的時候,這些配置檔案位置會被讀取,並且Spring容器將解析其中定義的beans並將它們註冊到容器中。setConfigLocations() 方法只是設定了這些位置,而實際的載入和註冊過程是在上下文重新整理時完成的。

這個setConfigLocations方法通常不是由使用者直接呼叫的,而是在ApplicationContext初始化的過程中被框架呼叫,例如在基於XML的配置中,我們會在初始化ClassPathXmlApplicationContextFileSystemXmlApplicationContext時提供配置檔案的路徑。

debug的時候,可以看到把測試程式碼中設定的 xml 配置檔案的路徑儲存了。

萬字解析XML配置對映為BeanDefinition的原始碼

2.3 refresh - 觸發容器重新整理,配置檔案的載入與解析

我們上面看到ClassPathXmlApplicationContext方法裡面,執行完setConfigLocations後,緊接著有個refresh方法,我們來看看。

萬字解析XML配置對映為BeanDefinition的原始碼

萬字解析XML配置對映為BeanDefinition的原始碼

Spring框架中,refresh()方法是非常關鍵的,它是ApplicationContext介面的一部分。這個方法的主要功能是重新整理應用上下文,載入或者重新載入配置檔案中定義的Bean,初始化所有的單例,配置訊息資源,事件釋出器等。

程式碼提出來分析:

public void refresh() throws BeansException, IllegalStateException {
    // 同步塊,確保容器重新整理過程的執行緒安全
    synchronized(this.startupShutdownMonitor) {
        // 開始上下文重新整理的步驟記錄,用於監控和診斷
        StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
        
        // 準備重新整理過程,設定開始時間,狀態標誌等
        this.prepareRefresh();
        
        // 獲取新的BeanFactory,如果是第一次重新整理則建立一個BeanFactory
        ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
        
        // 配置BeanFactory,註冊忽略的依賴介面等
        this.prepareBeanFactory(beanFactory);

        try {
            // 允許BeanFactory的後置處理器對其進行修改
            this.postProcessBeanFactory(beanFactory);
            
            // 開始Bean工廠的後置處理步驟的監控
            StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
            
            // 呼叫BeanFactoryPostProcessors
            this.invokeBeanFactoryPostProcessors(beanFactory);
            
            // 註冊BeanPostProcessors到BeanFactory
            this.registerBeanPostProcessors(beanFactory);
            
            // Bean後置處理步驟結束
            beanPostProcess.end();
            
            // 初始化MessageSource元件,用於國際化等功能
            this.initMessageSource();
            
            // 初始化事件廣播器
            this.initApplicationEventMulticaster();
            
            // 留給子類覆蓋的定製方法
            this.onRefresh();
            
            // 註冊監聽器
            this.registerListeners();
            
            // 初始化剩餘的單例Bean
            this.finishBeanFactoryInitialization(beanFactory);
            
            // 完成重新整理過程,通知生命週期處理器lifecycleProcessor重新整理過程,釋出ContextRefreshedEvent事件
            this.finishRefresh();
        } catch (BeansException var10) {
            // 捕獲BeansException,記錄警告資訊,銷燬已建立的Bean
            if (this.logger.isWarnEnabled()) {
                this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var10);
            }

            // 銷燬已經初始化的單例Bean
            this.destroyBeans();
            
            // 取消重新整理,重置同步監視器上的標誌位
            this.cancelRefresh(var10);
            
            // 丟擲異常,結束重新整理過程
            throw var10;
        } finally {
            // 在重新整理的最後,重置Spring核心中的共享快取
            this.resetCommonCaches();
            
            // 結束上下文重新整理步驟的記錄
            contextRefresh.end();
        }
    }
}

這個方法精確執行一系列步驟來配置ApplicationContext,包括Bean的載入、註冊和初始化。重新整理過程包括了Bean定義的載入、註冊以及Bean的初始化等一系列複雜的步驟。

在現代Spring框架中,ApplicationContext一般在容器啟動時重新整理一次。一旦容器啟動並且上下文被重新整理,所有的Bean就被載入並且建立了。儘管技術上可能存在呼叫refresh()方法多次的可能性,但這在實際中並不常見,因為這意味著重置應用上下文的狀態並重新開始。這樣做將銷燬所有的單例Bean,並重新初始化它們,這在大多數應用中是不可取的,不僅代價昂貴而且可能導致狀態丟失、資料不一致等問題。

對於基於xmlApplicationContext(如ClassPathXmlApplicationContext),在呼叫refresh()方法時會重新讀取和解析配置檔案,然後重新建立BeanFactoryBean的定義。如果容器已經被重新整理過,則需要先銷燬所有的單例Bean,關閉BeanFactory,然後重新建立。通常,這個功能用於開發過程中或者測試中,不推薦在生產環境使用,因為它的開銷和風險都很大。

我們來看一下重點,載入配置檔案的操作在哪裡?這裡圖上我標註出來了,obtainFreshBeanFactory方法裡面有個refreshBeanFactory方法。

萬字解析XML配置對映為BeanDefinition的原始碼

refreshBeanFactory方法是個抽象方法,我們來看看實現類是怎麼實現的,根據繼承關係找到實現類的refreshBeanFactory方法。

萬字解析XML配置對映為BeanDefinition的原始碼

refreshBeanFactory()方法通常在refresh()方法中被呼叫。這個方法確保當前ApplicationContext含有一個清潔狀態的BeanFactory

程式碼提出來分析:

protected final void refreshBeanFactory() throws BeansException {
    // 檢查當前應用上下文是否已經包含了一個BeanFactory
    if (this.hasBeanFactory()) {
        // 如果已經存在BeanFactory,銷燬它管理的所有bean
        this.destroyBeans();
        // 關閉現有的BeanFactory,釋放其可能持有的任何資源
        this.closeBeanFactory();
    }

    try {
        // 建立一個DefaultListableBeanFactory的新例項,這是Spring中ConfigurableListableBeanFactory介面的預設實現
        DefaultListableBeanFactory beanFactory = this.createBeanFactory();
        // 為beanFactory設定一個序列化ID,這個ID後面可以用於反序列化
        beanFactory.setSerializationId(this.getId());
        // 允許子類定製新建立的beanFactory
        this.customizeBeanFactory(beanFactory);
        // 從底層資源(例如XML檔案)中載入bean定義到beanFactory
        this.loadBeanDefinitions(beanFactory);
        // 將新的beanFactory賦值給這個上下文的beanFactory屬性
        this.beanFactory = beanFactory;
    } catch (IOException var2) {
        // 如果在解析bean定義資源過程中發生I/O異常,將其包裝並重新丟擲為ApplicationContextException
        throw new ApplicationContextException("I/O錯誤解析用於" + this.getDisplayName() + "的bean定義源", var2);
    }
}

這個方法在AbstractApplicationContext的具體實現中被重寫。它提供了重新整理bean工廠的模板——如果已經存在一個,則將其銷燬並關閉;然後建立一個新的bean工廠,進行定製,並填充bean定義。在載入bean定義(例如,從XML檔案讀取)時,如果遇到I/O異常,會丟擲一個ApplicationContextException,提供有關錯誤性質的更多上下文資訊。

這段程式碼我們可以看到有loadBeanDefinitions方法,是從底層資源(例如XML檔案)中載入bean定義到beanFactory,邏輯很複雜,我們下面來進行單獨分析。

2.4 loadBeanDefinitions - 具體的BeanDefinition載入邏輯

this.loadBeanDefinitions 方法是在 AbstractApplicationContext 的子類中實現的,這種模式是一個典型的模板方法設計模式的例子。在模板方法設計模式中,一個演算法的框架(即一系列的步驟)被定義在父類的方法中,但是一些步驟的具體實現會延遲到子類中完成。

AbstractApplicationContext 提供了 refreshBeanFactory 方法的框架,這個方法定義了重新整理 BeanFactory 的步驟,但是它將 loadBeanDefinitions 的具體實現留給了子類。子類需要根據具體的儲存資源型別(比如 XML 檔案、Java 註解、Groovy 指令碼等)來實現這個方法。

萬字解析XML配置對映為BeanDefinition的原始碼

子類AbstractXmlApplicationContext實現的loadBeanDefinitions 方法如下:

loadBeanDefinitions()方法是Spring框架中用於載入、解析並註冊Bean定義的核心方法。其基本職責是從一個或多個源讀取配置資訊,然後將這些資訊轉換成Spring容器可以管理的Bean定義。這個方法通常在Spring上下文初始化過程中被呼叫,是Spring容器裝載Bean定義的關鍵步驟。

程式碼提出來分析:

// 使用DefaultListableBeanFactory作為Bean定義註冊的目標工廠,載入Bean定義
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
    // 建立一個讀取XML Bean定義的讀取器,並將工廠傳入用於註冊定義
    XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
    // 設定環境物件,可能包含屬性解析相關的環境配置
    beanDefinitionReader.setEnvironment(this.getEnvironment());
    // 設定資源載入器,允許讀取器載入XML資源
    beanDefinitionReader.setResourceLoader(this);
    // 設定實體解析器,用於解析XML中的實體如DTD
    beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
    // 初始化Bean定義讀取器,可能設定一些引數,如是否驗證XML
    this.initBeanDefinitionReader(beanDefinitionReader);
    // 呼叫過載的loadBeanDefinitions,根據配置的資源和位置載入Bean定義
    this.loadBeanDefinitions(beanDefinitionReader);
}

// 初始化Bean定義讀取器,主要設定是否進行XML驗證
protected void initBeanDefinitionReader(XmlBeanDefinitionReader reader) {
    // 設定XML驗證模式,通常取決於應用上下文的配置
    reader.setValidating(this.validating);
}

// 透過XmlBeanDefinitionReader載入Bean定義
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
    // 獲取所有配置資源的陣列(如XML配置檔案)
    Resource[] configResources = this.getConfigResources();
    // 如果配置資源非空,則載入這些資源
    if (configResources != null) {
        reader.loadBeanDefinitions(configResources);
    }

    // 獲取所有配置檔案位置的陣列
    String[] configLocations = this.getConfigLocations();
    // 如果配置檔案位置非空,則載入這些位置指定的配置檔案
    if (configLocations != null) {
        reader.loadBeanDefinitions(configLocations);
    }
}

loadBeanDefinitions(DefaultListableBeanFactory beanFactory)方法中,首先建立了一個XmlBeanDefinitionReader例項,這個讀取器是專門用來解析XML配置檔案並把Bean定義載入到DefaultListableBeanFactory中。beanDefinitionReader的相關屬性被設定了,包括環境變數、資源載入器和實體解析器。這些設定確保了beanDefinitionReader能正確地解析XML檔案並能解析檔案中的佔位符和外部資源。

接著,透過呼叫initBeanDefinitionReader方法,可以對XmlBeanDefinitionReader例項進行一些額外的配置,例如設定XML驗證。最後,呼叫loadBeanDefinitions(XmlBeanDefinitionReader reader)方法實際進行載入操作。這個方法會呼叫讀取器來實際地讀取和解析XML檔案,把Bean定義載入到Spring容器中。

loadBeanDefinitions(XmlBeanDefinitionReader reader)方法中,首先嚐試從getConfigResources方法獲取XML配置檔案資源,如果存在這樣的資源,則透過reader載入這些定義。其次,嘗試獲取配置檔案位置資訊,如果存在,則透過reader載入這些位置指定的配置檔案。這種設計允許從不同的來源載入配置,如直接從資原始檔或者從指定的檔案路徑。

debug可以看到reader和configLocations的詳細狀態

cke_135.png

這裡看到還有一個reader.loadBeanDefinitions(configLocations);這是在做什麼呢?下面接著來看!

2.5 loadBeanDefinitions - 由XmlBeanDefinitionReader實現

debug的時候可以看到這裡的reader是XmlBeanDefinitionReader,點選跟蹤reader.loadBeanDefinitions(configLocations);方法,呼叫的方法在AbstractBeanDefinitionReader,而XmlBeanDefinitionReader 繼承自 AbstractBeanDefinitionReader。

cke_136.png

這裡配置檔案迴圈載入,有一個count += this.loadBeanDefinitions(location); 繼續跟蹤!

cke_137.png

這段程式碼的邏輯動作大致為:

  1. 根據傳入的資源位置字串,透過資源載入器(ResourceLoader)獲取對應的資源。
  2. 如果資源載入器是資源模式解析器(ResourcePatternResolver),它會處理路徑中的模式(比如萬用字元),載入所有匹配的資源。
  3. 讀取資源,解析並註冊其中定義的所有bean定義。
  4. 如果提供了一個實際資源的集合(actualResources),解析出來的資源將被新增到這個集合中。
  5. 返回載入並註冊的bean定義的數量。

我們還是看重點,繼續跟蹤裡面的loadBeanDefinitions

cke_138.png

程式碼提出來分析:

public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {

// 將Resource包裝為EncodedResource,允許指定編碼,然後繼續載入Bean定義

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

}

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {

// 斷言傳入的EncodedResource不為空

Assert.notNull(encodedResource, "EncodedResource must not be null");

// 如果日誌級別為trace,則輸出跟蹤日誌

if (this.logger.isTraceEnabled()) {

this.logger.trace("Loading XML bean definitions from " + encodedResource);

}

// 獲取當前執行緒正在載入的資源集合

Set<EncodedResource> currentResources = (Set)this.resourcesCurrentlyBeingLoaded.get();

// 檢查資源是否已經在載入中,如果是,則丟擲BeanDefinitionStoreException異常,避免迴圈載入

if (!currentResources.add(encodedResource)) {

throw new BeanDefinitionStoreException("Detected cyclic loading of " + encodedResource + " - check your import definitions!");

} else {

int var6; // 這將用來儲存載入的Bean定義數量

try {

// 開啟資源的InputStream進行讀取

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

Throwable var4 = null;

try {

// 將InputStream封裝為InputSource,XML解析器可以接受這個型別

InputSource inputSource = new InputSource(inputStream);

// 如果資源編碼不為空,設定資源的編碼

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

inputSource.setEncoding(encodedResource.getEncoding());

}

// 實際載入Bean定義的方法,返回載入的Bean定義數量

var6 = this.doLoadBeanDefinitions(inputSource, encodedResource.getResource());

} catch (Throwable var24) {

// 捕獲Throwable以便在finally塊中處理資源釋放

var4 = var24;

throw var24;

} finally {

// 關閉InputStream資源

if (inputStream != null) {

if (var4 != null) {

try {

inputStream.close();

} catch (Throwable var23) {

// 新增被抑制的異常

var4.addSuppressed(var23);

}

} else {

inputStream.close();

}

}

}

} catch (IOException var26) {

// 丟擲IOException異常,如果解析XML文件失敗

throw new BeanDefinitionStoreException("IOException parsing XML document from " + encodedResource.getResource(), var26);

} finally {

// 從當前載入的資源集合中移除該資源

currentResources.remove(encodedResource);

// 如果當前載入的資源集合為空,則從ThreadLocal中移除

if (currentResources.isEmpty()) {

this.resourcesCurrentlyBeingLoaded.remove();

}

}

// 返回載入的Bean定義數量

return var6;

}

}
在這段程式碼中,loadBeanDefinitions 首先將Resource轉換為EncodedResource,這允許它保留關於資源編碼的資訊。然後,它嘗試將資源載入為InputStream並將其轉換為InputSource,這是XML解析所需要的。接著它呼叫doLoadBeanDefinitions方法,實際上負責解析XML並註冊Bean定義。

在這個過程中,程式碼確保了不會迴圈載入相同的資源,並且在載入資源時,如果發生異常,會適當地清理資源並報告錯誤。載入的Bean定義數量在完成後被返回。

我們來重點看下這段程式碼的重點步驟:doLoadBeanDefinitions方法!

2.6 doLoadBeanDefinitions - 讀取並解析XML配置檔案內容

cke_139.png

doLoadBeanDefinitions方法做了什麼?

具體步驟如下:

  1. 使用doLoadDocument方法將給定的InputSource解析為 DOM Document物件。這個Document物件代表了 XML 檔案的結構。
  2. 透過呼叫registerBeanDefinitions方法,將解析得到的Document中的 Bean 定義註冊到 Spring 的 Bean 工廠中。這個方法返回註冊的 Bean 定義的數量。
  3. 如果日誌級別設定為 DEBUG,則會記錄載入的 Bean 定義數量。

這裡重點是registerBeanDefinitions方法,繼續跟蹤程式碼

cke_140.png

繼續看重點,最終追到doRegisterBeanDefinitions方法

cke_141.png

doRegisterBeanDefinitions(Element root) 方法是 Spring 框架中用於解析 XML 配置檔案中的 Bean 定義並註冊它們到 Spring 容器的方法。這個方法通常在 XML 檔案讀取並轉換成 DOM(Document Object Model)樹之後呼叫,此時 XML 檔案的根元素透過引數 root 傳遞給這個方法。

程式碼提出來分析:

protected void doRegisterBeanDefinitions(Element root) {

// 儲存舊的解析代理(delegate),以便之後可以恢復

BeanDefinitionParserDelegate parent = this.delegate;

// 建立新的解析代理(delegate),用於處理當前XML根節點的解析

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

// 如果當前節點使用的是Spring預設的XML名稱空間

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

// 獲取根節點的"profile"屬性

String profileSpec = root.getAttribute("profile");

// 檢查"profile"屬性是否有文字內容

if (StringUtils.hasText(profileSpec)) {

// 按逗號、分號和空格分隔"profile"屬性值,得到指定的profiles陣列

String[] specifiedProfiles = StringUtils.tokenizeToStringArray(profileSpec, ",; ");

// 如果當前環境不接受任何指定的profiles,則不載入該Bean定義檔案

if (!this.getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {

// 如果日誌級別是DEBUG,則記錄跳過檔案的資訊

if (this.logger.isDebugEnabled()) {

this.logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec + "] not matching: " + this.getReaderContext().getResource());

}

// 退出方法,不進行後續處理

return;

}

}

}

// 在解析XML前進行預處理,可被重寫的方法

this.preProcessXml(root);

// 解析XML根節點下的Bean定義

this.parseBeanDefinitions(root, this.delegate);

// 在解析XML後進行後處理,可被重寫的方法

this.postProcessXml(root);

// 恢復舊的解析代理(delegate)

this.delegate = parent;

}

上述程式碼片段是Spring框架用於註冊Bean定義的內部方法。該方法在解析XML配置檔案並註冊Bean定義到Spring容器時被呼叫。它包含處理profile屬性以根據執行時環境決定是否載入特定Bean定義的邏輯,以及前後處理鉤子,允許在解析前後進行自定義操作。最後,它確保解析代理(delegate)被重置為之前的狀態,以維護正確的狀態。

接著,我們要看看是如何解析xml的,重點關注下parseBeanDefinitions方法

2.7 parseBeanDefinitions - 解析XML中的BeanDefinition元素

cke_142.png

parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) 方法的主要目的是遍歷 XML 配置檔案的根節點,解析並註冊其中定義的所有 Bean。該方法負責區分不同型別的元素,即預設名稱空間下的標準元素和自定義名稱空間下的自定義元素,並對它們進行相應的處理。

程式碼提出來分析:

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {

// 判斷根節點是否使用的是Spring的預設名稱空間

if (delegate.isDefaultNamespace(root)) {

// 獲取所有子節點

NodeList nl = root.getChildNodes();



// 遍歷所有子節點

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

Node node = nl.item(i);

// 只處理Element型別的節點(過濾掉文字節點等其他型別)

if (node instanceof Element) {

Element ele = (Element)node;

// 如果子元素節點也是預設名稱空間,則呼叫parseDefaultElement方法解析

if (delegate.isDefaultNamespace(ele)) {

this.parseDefaultElement(ele, delegate);

} else {

// 如果子元素節點不是預設名稱空間,則呼叫parseCustomElement方法解析

// 這通常表示節點定義了自定義的行為,可能是使用者自定義的標籤或者是Spring擴充套件的標籤

delegate.parseCustomElement(ele);

}

}

}

} else {

// 如果根節點不是預設名稱空間,那麼它可能是一個自定義標籤的頂級元素

// 在這種情況下,直接呼叫parseCustomElement進行解析

delegate.parseCustomElement(root);

}

}

這段程式碼的作用是解析XML檔案中定義的bean。它檢查每個XML元素(包括根元素和子元素),並根據這些元素是否屬於Spring的預設名稱空間(通常是"http://www.springframework.org/schema/beans"),呼叫不同的處理方法。如果元素屬於預設名稱空間,那麼它將呼叫parseDefaultElement來解析標準的Spring配置元素,例如<bean>。如果元素不屬於預設名稱空間,那麼將認為它是一個自定義元素,並呼叫parseCustomElement來解析。自定義元素通常是由開發人員定義或Spring擴充套件提供的,以增加框架的功能。

這裡可以看到是一個迴圈處理Element節點,解析的動作主要是parseDefaultElement方法,繼續來看看。

cke_143.png

parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) 方法是 Spring 框架解析 XML 配置檔案中預設名稱空間(也就是沒有字首的 Spring 名稱空間)元素的方法。這個方法專門處理 <import>, <alias>, <bean>, 和 <beans> 這幾種標籤。

“沒有字首的 Spring 名稱空間” 是指那些元素?它們屬於 Spring 的預設名稱空間,但在使用時不需要指定名稱空間字首。如 <bean>, <property> 或 <constructor-arg> ,這些元素都是沒有字首的,它們屬於 Spring 預設定義的 XML 模式名稱空間,預設名稱空間通常在 XML 檔案的頂部透過 xmlns 屬性宣告。

程式碼提出來分析:

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {

// 判斷當前元素節點名稱是否是"import"

if (delegate.nodeNameEquals(ele, "import")) {

// 如果是"import",則匯入其他配置檔案

this.importBeanDefinitionResource(ele);

} else if (delegate.nodeNameEquals(ele, "alias")) {

// 如果節點是"alias",則處理別名定義,為一個bean定義一個或多個別名

this.processAliasRegistration(ele);

} else if (delegate.nodeNameEquals(ele, "bean")) {

// 如果節點是"bean",則處理bean定義,這是定義Spring bean的核心元素

this.processBeanDefinition(ele, delegate);

} else if (delegate.nodeNameEquals(ele, "beans")) {

// 如果節點是"beans",意味著有巢狀的beans定義,需要遞迴地註冊其中的bean定義

this.doRegisterBeanDefinitions(ele);

}

}

這段程式碼的功能是根據元素的名稱來決定對XML配置檔案中的不同標籤進行不同的處理操作。它處理Spring框架預設名稱空間下的四種主要標籤:

  1. <import>:匯入其他Spring XML配置檔案到當前的配置檔案中。
  2. <alias>:為一個已經定義的bean提供一個或多個別名。
  3. <bean>:定義一個Spring管理的bean,是最常用的元素,包含了bean的詳細配置。
  4. <beans>:定義一個beans的集合,通常是配置檔案中的頂層元素,但也可以是巢狀定義,表示一個新的作用域或者上下文。

這樣,Spring可以根據這些元素來構建應用上下文中的bean工廠。

除錯可以發現,xml已經解析出初步的雛形了

cke_144.png

在這裡似乎沒看到bean元素,這是怎麼解析的呢?讓我們一步一步來,在上面提到的parseDefaultElement方法中有呼叫processBeanDefinition方法,來看看這是幹嘛的。

2.8 processBeanDefinition - 對<bean>標籤進行具體解析和處理

cke_145.png

processBeanDefinition方法是 Spring 框架中用於處理 <bean> XML 配置元素的方法。其目的是將 <bean> 元素中描述的資訊轉換為 Spring 內部使用的BeanDefinition物件,並將其註冊到 Spring IoC 容器中。這是 Spring bean 生命週期中的一個關鍵步驟,因為在這裡定義的 bean 會在容器啟動時被例項化和管理

程式碼提出來分析:

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {

// 使用代理解析bean定義元素,這涉及將XML定義的<bean>元素轉換成Spring的BeanDefinitionHolder物件,

// 該物件包含了bean定義和名稱。

BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);

// 檢查解析是否返回了BeanDefinitionHolder物件。

if (bdHolder != null) {

// 如有需要,對bean定義進行裝飾。這可能涉及應用任何額外的屬性或巢狀元素,

// 這些都是bean定義的一部分,但不是標準<bean> XML配置的一部分。

bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);

try {

// 在註冊中心註冊bean定義。註冊中心通常是持有所有bean定義的Spring IoC容器。

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

} catch (BeanDefinitionStoreException var5) {

// 如果在bean註冊過程中出現異常,報告錯誤上下文並丟擲異常。

// 錯誤上下文包括bean的名稱和引起問題的XML元素。

this.getReaderContext().error("Failed to register bean definition with name '" + bdHolder.getBeanName() + "'", ele, var5);

}

// 在成功註冊後,通知任何監聽器一個新的bean定義已被註冊。

// 這是Spring事件機制的一部分,允許對容器內的特定動作作出響應。

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

}

// 注意:如果bdHolder為空,則意味著bean定義元素沒有被正確解析

// 或者它不是要被註冊的(例如,在抽象定義的情況下)。

// 因此,在這種情況下,該方法不執行任何操作。

}

該方法通常在Spring框架的bean定義解析過程中使用,它處理基於提供的XML元素建立和註冊bean定義的邏輯。BeanDefinitionParserDelegate 是一個幫助類,負責處理解析特定Spring XML結構的細節。

debug這個類的時候,發現已經解析出這個bean的class和id了

cke_146.png

有人會好奇了,這是如何將 xml 元素封裝為 BeanDefinitionHolder呢

cke_147.png

parseBeanDefinitionElement方法是用來解析 Spring 配置檔案中 <bean> 元素的定義,並生成對應的 BeanDefinitionHolder 物件。BeanDefinitionHolder 是一個包裝類,它封裝了 BeanDefinition 例項和該定義的名稱(即bean的id)以及別名(如果有的話)。

程式碼提出來分析:

@Nullable

public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) {

// 呼叫過載方法parseBeanDefinitionElement,並將BeanDefinition設定為null

return this.parseBeanDefinitionElement(ele, (BeanDefinition)null);

}

@Nullable

public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {

// 獲取元素的id屬性

String id = ele.getAttribute("id");

// 獲取元素的name屬性

String nameAttr = ele.getAttribute("name");

// 建立別名列表

List<String> aliases = new ArrayList();

if (StringUtils.hasLength(nameAttr)) {

// 如果name屬性非空,則使用分隔符分割name字串,並將結果新增到別名列表

String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, ",; ");

aliases.addAll(Arrays.asList(nameArr));

}

// 預設情況下bean的名稱使用id屬性的值

String beanName = id;

if (!StringUtils.hasText(id) && !aliases.isEmpty()) {

// 如果id為空且別名列表非空,則使用別名列表中的第一個作為bean名稱,並從列表中移除它

beanName = aliases.remove(0);

if (this.logger.isTraceEnabled()) {

this.logger.trace("No XML 'id' specified - using '" + beanName + "' as bean name and " + aliases + " as aliases");

}

}

if (containingBean == null) {

// 如果不是巢狀bean定義,則檢查bean名稱和別名的唯一性

this.checkNameUniqueness(beanName, aliases, ele);

}

// 解析bean定義元素,返回AbstractBeanDefinition物件

AbstractBeanDefinition beanDefinition = this.parseBeanDefinitionElement(ele, beanName, containingBean);

if (beanDefinition != null) {

if (!StringUtils.hasText(beanName)) {

// 如果bean名稱為空,則嘗試生成bean名稱

try {

if (containingBean != null) {

// 如果是內部bean,則使用特定的生成策略

beanName = BeanDefinitionReaderUtils.generateBeanName(beanDefinition, this.readerContext.getRegistry(), true);

} else {

// 否則使用預設策略

beanName = this.readerContext.generateBeanName(beanDefinition);

String beanClassName = beanDefinition.getBeanClassName();

if (beanClassName != null && beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() && !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {

// 如果bean類名不為空,且生成的bean名稱以類名開頭,且未被使用,則將類名新增到別名列表

aliases.add(beanClassName);

}

}

if (this.logger.isTraceEnabled()) {

this.logger.trace("Neither XML 'id' nor 'name' specified - using generated bean name [" + beanName + "]");

}

} catch (Exception var9) {

// 在名稱生成過程中捕獲異常,並記錄錯誤

this.error(var9.getMessage(), ele);

return null;

}

}

// 將別名列表轉換為陣列

String[] aliasesArray = StringUtils.toStringArray(aliases);

// 建立並返回BeanDefinitionHolder物件

return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);

} else {

// 如果bean定義為空,則返回null

return null;

}

}

這段程式碼負責解析XML中的<bean>元素,提取id和name屬性,並處理可能的別名。然後它建立一個AbstractBeanDefinition,這是Spring中bean定義的抽象表現形式。如果沒有指定bean的名稱,它會嘗試生成一個唯一的名稱,並在必要時新增別名。最終,它返回一個包含所有這些資訊的BeanDefinitionHolder。如果在解析過程中遇到任何問題,會記錄錯誤並返回null。

在這段程式碼中,會呼叫另一個過載方法,this.parseBeanDefinitionElement(ele, beanName, containingBean);這段程式碼裡有封裝 <bean> 其它屬性的 parseBeanDefinitionAttributes 方法,我們來看下

cke_148.png

cke_149.png

方法 parseBeanDefinitionAttributes 用於解析 Spring 配置檔案中 <bean> 元素的屬性,並將這些屬性應用到傳入的 AbstractBeanDefinition 物件上。這個過程是為了設定bean的作用域、是否延遲初始化、自動裝配模式、依賴關係、是否作為自動裝配的候選、是否是優先考慮的bean(primary)、初始化方法、銷燬方法、工廠方法和工廠bean名稱等屬性。方法處理了屬性的預設值以及處理了一些屬性的遺留格式(如 singleton)。

直接提出程式碼分析:

public AbstractBeanDefinition parseBeanDefinitionAttributes(Element ele, String beanName, @Nullable BeanDefinition containingBean, AbstractBeanDefinition bd) {

// 檢查是否使用了已廢棄的singleton屬性,如果存在,則報錯提示應該升級到scope屬性

if (ele.hasAttribute("singleton")) {

this.error("Old 1.x 'singleton' attribute in use - upgrade to 'scope' declaration", ele);

// 如果存在scope屬性,則設定bean的作用域

} else if (ele.hasAttribute("scope")) {

bd.setScope(ele.getAttribute("scope"));

// 如果沒有設定scope屬性但是有包含bean,則設定為包含bean的作用域

} else if (containingBean != null) {

bd.setScope(containingBean.getScope());

}

// 如果設定了abstract屬性,根據該屬性的值設定bean定義是否為抽象

if (ele.hasAttribute("abstract")) {

bd.setAbstract("true".equals(ele.getAttribute("abstract")));

}

// 解析lazy-init屬性,預設使用配置的預設值,如果設定了則覆蓋

String lazyInit = ele.getAttribute("lazy-init");

if (this.isDefaultValue(lazyInit)) {

lazyInit = this.defaults.getLazyInit();

}

bd.setLazyInit("true".equals(lazyInit));

// 解析autowire屬性,將字串值轉換為相應的自動裝配模式

String autowire = ele.getAttribute("autowire");

bd.setAutowireMode(this.getAutowireMode(autowire));

// 解析depends-on屬性,將字串值轉換為陣列,並設定為bean定義的依賴

if (ele.hasAttribute("depends-on")) {

String dependsOn = ele.getAttribute("depends-on");

bd.setDependsOn(StringUtils.tokenizeToStringArray(dependsOn, ",; "));

}

// 解析autowire-candidate屬性,設定bean是否可作為自動裝配的候選者

String autowireCandidate = ele.getAttribute("autowire-candidate");

if (this.isDefaultValue(autowireCandidate)) {

String defaultValue = this.defaults.getAutowireCandidates();

if (defaultValue != null) {

String[] patterns = StringUtils.commaDelimitedListToStringArray(defaultValue);

bd.setAutowireCandidate(PatternMatchUtils.simpleMatch(patterns, beanName));

}

} else {

bd.setAutowireCandidate("true".equals(autowireCandidate));

}

// 解析primary屬性,設定bean是否為primary

if (ele.hasAttribute("primary")) {

bd.setPrimary("true".equals(ele.getAttribute("primary")));

}

// 解析init-method屬性,設定bean的初始化方法

String initMethodName = ele.getAttribute("init-method");

if (ele.hasAttribute("init-method")) {

bd.setInitMethodName(initMethodName);

// 如果沒有設定但是有預設值,則使用預設值

} else if (this.defaults.getInitMethod() != null) {

bd.setInitMethodName(this.defaults.getInitMethod());

bd.setEnforceInitMethod(false);

}

// 解析destroy-method屬性,設定bean的銷燬方法

String destroyMethodName = ele.getAttribute("destroy-method");

if (ele.hasAttribute("destroy-method")) {

bd.setDestroyMethodName(destroyMethodName);

// 如果沒有設定但是有預設值,則使用預設值

} else if (this.defaults.getDestroyMethod() != null) {

bd.setDestroyMethodName(this.defaults.getDestroyMethod());

bd.setEnforceDestroyMethod(false);

}

// 解析factory-method屬性,設定bean的工廠方法

if (ele.hasAttribute("factory-method")) {

bd.setFactoryMethodName(ele.getAttribute("factory-method"));

}

// 解析factory-bean屬性,設定bean的工廠bean名

if (ele.hasAttribute("factory-bean")) {

bd.setFactoryBeanName(ele.getAttribute("factory-bean"));

}

// 返回配置好的bean定義

return bd;

}

這段程式碼的核心功能是將XML配置檔案中的屬性轉換為BeanDefinition物件的屬性。對於每個屬性,它首先檢查該屬性是否存在,如果存在,則讀取其值並設定到BeanDefinition物件中。如果存在預設值,並且XML中沒有提供特定值,則使用預設值。透過這種方式,Spring容器能夠根據配置檔案建立和管理bean。

2.9 總結

從讀取XML配置檔案到註冊BeanDefinition的完整流程:

cke_150.png

1.載入配置檔案:

  • 圖中"建立上下文"步驟對應於例項化ClassPathXmlApplicationContext,這時會傳入XML檔案路徑。
  • ClassPathXmlApplicationContext接受一個或多個XML檔案路徑作為構造引數。

2.初始化BeanFactory並進行重新整理:

  • 在圖中"執行refresh"步驟表示refresh()方法被呼叫,這個方法會啟動容器的初始化和重新整理過程。
  • 在refresh()方法中初始化BeanFactory,並準備對配置檔案進行解析。

3.讀取XML配置檔案:

  • 圖中"載入Bean定義"步驟代表XmlBeanDefinitionReader的作用,它負責讀取和載入XML配置檔案。
  • XmlBeanDefinitionReader 負責讀取傳入的XML配置檔案。

4.解析XML檔案:

  • 圖中的"解析XML"步驟表示DefaultBeanDefinitionDocumentReader處理XML檔案,這包括解析頂層<beans>標籤。
  • DefaultBeanDefinitionDocumentReader 開始處理XML檔案,解析<beans>這樣的頂層標籤。
  • 對於<bean>元素的解析,首先檢查元素是否在預設名稱空間。如果是,進行預設元素的解析;如果不是,預設名稱空間之外的元素被認為是自定義元素,並交由delegate.parseCustomElement(ele)處理。

5.Bean定義的解析和註冊:

  • 圖中的"註冊Bean定義"、“處理別名”、“處理Bean”和“處理匯入”步驟對應於BeanDefinitionParserDelegate的各種解析活動,它涉及解析bean的id、name、別名、屬性、子元素等,以及將解析結果註冊到BeanDefinitionRegistry。
  • 使用BeanDefinitionParserDelegate來解析<bean>元素的細節,包括bean的id、name、別名等。
  • 解析<bean>元素的屬性,如scope、lazy-init等,並將這些值設定到BeanDefinition例項中。
  • 如果<bean>元素包含子元素(如<property>或<constructor-arg>),它們也將被解析並以相應的後設資料形式加入到BeanDefinition中。
  • 生成的BeanDefinition將會註冊到BeanDefinitionRegistry中,使用BeanDefinitionReaderUtils.registerBeanDefinition方法。
  • 如果解析過程中發生任何錯誤,會透過error方法記錄錯誤資訊。

6.事件釋出:

  • 在註冊BeanDefinition後,ApplicationContext會釋出一個元件註冊事件,以通知相關的監聽器。這個過程允許實現了ApplicationListener介面或使用@EventListener註解的元件接收到這個事件,並根據需要進行響應。例如,可以使用這個事件來觸發某些自定義的邏輯,如額外的配置檢查、啟動某些後處理操作等。

這個詳細流程顯示了從載入配置檔案到解析並註冊BeanDefinition所涉及的複雜過程,它展示了Spring框架處理Bean宣告和依賴關係的內部機制。這是Spring依賴注入核心功能的基礎,確保了Bean能夠按照定義被例項化和管理。

3. 原始碼閱讀練習題

1. XML配置檔案解析:

  • 解析Spring配置檔案時,Spring容器使用了哪些元件?

  Spring容器在解析配置檔案時主要使用了 XmlBeanDefinitionReader 類。此外,還用到了 BeanDefinitionDocumentReader 來進行具體的文件讀取。

  • BeanDefinitionReader 在配置檔案解析中扮演什麼角色?

  BeanDefinitionReader 負責從XML檔案讀取bean定義並轉換為Spring內部的 BeanDefinition 物件。

  • parseBeanDefinitionElement 方法是在什麼時候被呼叫的?它的輸出是什麼?

  parseBeanDefinitionElement 在XML元素被讀取時呼叫,它的輸出是 BeanDefinitionHolder 物件,其中包含了bean定義以及名稱和別名。

2. Bean定義解析:

  • 描述一個bean定義從讀取XML元素開始,到生成 BeanDefinition 物件的過程。

  BeanDefinition 物件是透過讀取XML中的 <bean> 元素並提取相關屬性來建立的。這些屬性包括bean的類名、作用域、生命週期回撥等。

  • parseBeanDefinitionAttributes 方法在整個解析過程中的作用是什麼?

  parseBeanDefinitionAttributes 方法用於提取bean元素上的屬性,並設定到 AbstractBeanDefinition 物件中。

  • 哪些XML屬性會被 parseBeanDefinitionAttributes 方法處理,並如何影響生成的 BeanDefinition 物件?

  parseBeanDefinitionAttributes 方法處理的屬性包括 scope、lazy-init、autowire 等,這些屬性會決定bean的行為和它如何與其他bean互動。

3. Bean名稱與別名:

  • 如果XML元素中沒有提供bean的id或name,Spring是如何處理的?

  如果沒有提供id或name,Spring會自動生成一個唯一的bean名稱。它可能基於類名加上一定的序列號。提示:分析parseBeanDefinitionElement方法時有說過。

  • 別名(alias)在Spring中有何用途?在 parseBeanDefinitionElement 方法中,別名是如何被處理的?

  別名可以為bean提供額外的名稱,這在需要引用相同的bean但在不同上下文中使用不同名稱時很有用。在 parseBeanDefinitionElement 方法中,別名是透過解析 name 屬性並以逗號、分號或空格作為分隔符來處理的。

4. Bean作用域與生命週期屬性:

  • 如何定義一個bean的作用域(scope)?singleton 和 prototype 有什麼不同?

  透過設定 <bean> 元素的 scope 屬性定義bean的作用域。singleton 表示全域性唯一例項,而 prototype 表示每次請求都建立一個新的例項。

  • lazy-init、init-method 和 destroy-method 這些屬性對bean的生命週期有什麼影響?

  lazy-init 屬性確定bean是否應該在啟動時延遲初始化,init-method 和 destroy-method 定義了bean的初始化和銷燬時呼叫的方法。

5. Bean註冊:

  • 一旦 BeanDefinition 物件被建立,Spring是如何將其註冊到容器中的?

  BeanDefinition 物件在解析後,透過 DefaultListableBeanFactory.registerBeanDefinition 方法註冊到Spring容器中。

  • 註冊過程中,如果發現bean名稱衝突,Spring會如何處理?

  如果發現名稱衝突,會丟擲 BeanDefinitionStoreException。如果是在不同的配置檔案中定義相同名稱的bean,後者通常會覆蓋前者。

6. 異常處理:

  • 當XML配置不正確或使用了不合法的屬性時,Spring是如何反饋給使用者的?

  Spring會透過丟擲 BeanDefinitionStoreException 來告知使用者配置錯誤。異常資訊會詳細說明錯誤的原因和位置。

  • 分析Spring中的錯誤處理機制,它對於開發者除錯配置有何幫助?

  Spring的錯誤處理機制包括異常的詳細資訊和精確的定位,這對於開發者快速識別配置錯誤非常有幫助。

4. 常見疑問

4.1 在refresh過程中,Bean的生命週期是怎樣的?每個Bean的狀態是如何被管理的?

1.例項化BeanFactory:

  • 在refresh方法開始時,Spring會例項化一個新的BeanFactory,通常是DefaultListableBeanFactory,作為容器用於建立Bean例項。

2.載入Bean定義:

  • 然後,refresh呼叫loadBeanDefinitions來載入和註冊Bean的定義。這些定義可以來源於XML配置檔案、Java配置類或者掃描的註解。

3.BeanFactoryPostProcessor的執行:

  • 在所有Bean定義載入完成之後,但在Bean例項化之前,Spring會呼叫BeanFactoryPostProcessor。這些處理器可以對Bean定義(配置後設資料)進行修改。

4.BeanPostProcessor的註冊:

  • 接下來,Spring註冊BeanPostProcessor例項。這些處理器可以對Bean的例項(建立和初始化後的物件)進行修改。

5.單例Bean的預例項化:

  • 隨後,Spring會預例項化單例Bean。對於單例作用域的Bean,Spring會建立並配置這些Bean,然後將它們放入快取中。

6.依賴注入:

  • 在Bean例項化後,Spring會進行依賴注入。此時,Bean的屬性將被設定,相關的依賴將被注入。

7.Bean初始化:

  • 之後,Bean將被初始化。如果Bean實現了InitializingBean介面,afterPropertiesSet方法會被呼叫;或者如果定義了init-method,指定的方法也會被呼叫。

8.Aware介面的呼叫:

  • 如果Bean實現了任何Aware介面,如ApplicationContextAware或BeanNameAware,它們將在初始化之前被呼叫。

9.BeanPostProcessor的後處理:

  • BeanPostProcessor的前置處理(postProcessBeforeInitialization)和後置處理(postProcessAfterInitialization)方法在Bean初始化之前和之後被呼叫,它們可以進一步定製Bean。

10.事件釋出:

  • 一旦所有單例Bean都被初始化,Spring會發布ContextRefreshedEvent,表明ApplicationContext已被重新整理。

11.使用Bean:

  • 此時,所有的Bean都準備就緒,並可以用於應用程式的其他部分。

12.關閉容器:

  • 當應用上下文被關閉時,如果Bean實現了DisposableBean介面,destroy方法會被呼叫;或者定義了destroy-method方法,它也會被執行來清理資源。

在整個生命週期過程中,每個Bean的狀態被ApplicationContext和BeanFactory跟蹤和管理,從建立、依賴注入、初始化,到銷燬,確保Bean在正確的時機被建立和清理。

4.2 refresh方法是自動觸發的嗎?如果不是,那麼是什麼條件下需要手動觸發?

在Spring中的refresh方法:

1. 何時觸發:

  • 自動觸發: 在初始化ApplicationContext的時候,比如在應用程式中使用new ClassPathXmlApplicationContext("config.xml"),Spring容器啟動過程中會自動呼叫refresh方法。
  • 手動觸發: 如果在應用程式執行時需要重新載入配置(可能是修改了配置檔案),可以手動呼叫refresh方法來實現。但這通常在開發或測試階段用於特殊場景,因為它會導致整個應用上下文重建,包括所有的Bean物件。

2. 為什麼需要手動觸發:

  • 通常情況下,Spring容器在啟動時只需要載入一次配置,初始化一次每個Bean。除非有特殊需求,例如動態調整日誌級別,重新載入配置檔案中的特定Bean,否則不需要手動觸發。

在Spring Boot中的refresh方法:

Spring Boot大大簡化了Spring應用的配置和啟動過程。它自動配置了Spring的ApplicationContext並在合適的時候呼叫了refresh方法。

1. 自動觸發:

  • 當使用Spring Boot的SpringApplication.run()方法啟動應用時,Spring Boot會自動建立ApplicationContext,並在內部呼叫refresh方法。這個過程是自動的,開發者通常不需要關心。

2. 可能的手動觸發場景:

  • Spring Boot提供了actuator模組,其中/refresh端點可以用來重新載入配置(通常是與Spring Cloud Config結合使用)。這不是傳統意義上的呼叫ApplicationContext的refresh方法,而是一種觸發重新載入部分配置的機制,特別是標註了@RefreshScope的Bean,它們可以在不重新啟動整個應用的情況下更新。

一般情況下的建議:

  • 對於開發者來說,不應該在生產環境中隨意手動呼叫refresh方法。因為這會導致整個應用的重新載入,影響效能並可能導致服務中斷。
  • 如果需要動態更新配置,應當使用Spring Cloud Config和Spring Boot Actuator的/refresh端點,這是一種更加安全和控制的方式來更新配置。

4.3 在Spring Boot中,refresh方法的行為是否有所不同?Spring Boot是否提供了更優的方法來處理應用上下文的變化?

在Spring Boot中,refresh方法的基本行為保持不變,因為Spring Boot建立在Spring之上,遵循相同的基本原則。不過,Spring Boot確實為應用上下文的管理和重新整理提供了更多的自動化和便利性:

1.自動配置:

  • Spring Boot特有的自動配置特性減少了需要手動重新整理的場景。在啟動時,它會自動裝配Bean,通常不需要顯式呼叫refresh。

2.外部化配置:

  • Spring Boot支援強大的外部化配置機制,允許透過配置檔案、環境變數等方式來注入配置。這使得改變配置而不需要重新重新整理上下文成為可能。

3.條件重新整理:

  • Spring Boot使用條件註解(如@ConditionalOnClass、@ConditionalOnBean等),這允許上下文根據環境或者特定條件動態調整其配置,減少了需要手動觸發refresh的場景。

4.生命週期管理:

  • 透過SpringApplication類,Spring Boot為應用生命週期提供了額外的管理能力。它處理了許多在傳統Spring應用中需要手動完成的任務,如初始化和重新整理應用上下文。

5.Actuator endpoints:

  • 對於執行中的應用,Spring Boot Actuator提供了一系列管理和監控的端點,其中一些可以用來重新整理配置(如/refresh端點)或者重啟上下文(如/restart端點),這在某些情況下可以替代完整的應用重啟。

6.配置更改監聽:

  • 使用Spring Cloud Config的應用可以在配置變化時自動重新整理上下文。在配置伺服器上的變化可以被監聽,並且可以觸發客戶端上下文的自動重新整理,而不需要手動干預。

7.錯誤處理:

  • Spring Boot有一套預設的錯誤處理機制,特別是在Web應用程式中,它會提供預設的錯誤頁面和/error端點。此外,開發者可以定製錯誤處理,以適應具體需求。

綜上所述,Spring Boot提供了更為自動化的方式來處理應用上下文的變化,很多時候無需手動呼叫refresh方法。不過,如果需要在執行時動態改變Bean的配置,並希望這些改變立即生效,那麼可能還需要使用Spring提供的refresh方法或透過Spring Boot Actuator的相關端點來達成這一目的。

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章