本文分享自華為雲社群《Spring高手之路16——解析XML設定對映為BeanDefinition的原始碼》,作者:磚業洋__。
Spring
框架中控制反轉(IOC
)容器的BeanDefinition
階段的具體步驟,主要涉及到Bean
的定義、載入、解析,並在後面進行程式設計式注入和後置處理。這個階段是Spring
框架中Bean
生命週期的早期階段之一,對於理解整個Spring
框架非常關鍵。
在這一步,Spring
容器通過組態檔或設定類來了解需要管理哪些Bean
。對於基於XML
的設定,通常使用ClassPathXmlApplicationContext
或者FileSystemXmlApplicationContext
。
Spring
框架通過使用BeanDefinitionReader
範例(如XmlBeanDefinitionReader
)來解析組態檔。解析後,每個Bean
設定會被封裝成一個BeanDefinition
物件,這個物件包含了類名、作用域、生命週期回撥等資訊。
除了組態檔定義的Bean
,也可以通過程式設計的方式動態新增BeanDefinition
到IOC
容器中,這增加了靈活性。
BeanDefinition
的後置處理是指容器允許使用BeanDefinitionRegistryPostProcessor
或BeanFactoryPostProcessor
來對解析後的BeanDefinition
做進一步處理,例如修改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>
執行結果:
接著我們就從這段程式碼開始分析
我們還是以Spring 5.3.7
的原始碼為例分析
// 建立Spring上下文(容器) ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("ApplicationContext.xml");
這段程式碼,我們利用idea
點選去分析,最後在ClassPathXmlApplicationContext
的過載方法裡看到呼叫了setConfigLocations
設定組態檔的路徑。
接著看看setConfigLocations
方法
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
的設定中,我們會在初始化ClassPathXmlApplicationContext
或FileSystemXmlApplicationContext
時提供組態檔的路徑。
在debug
的時候,可以看到把測試程式碼中設定的 xml
組態檔的路徑儲存了。
我們上面看到ClassPathXmlApplicationContext
方法裡面,執行完setConfigLocations
後,緊接著有個refresh
方法,我們來看看。
在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
,並重新初始化它們,這在大多數應用中是不可取的,不僅代價昂貴而且可能導致狀態丟失、資料不一致等問題。
對於基於xml
的ApplicationContext
(如ClassPathXmlApplicationContext
),在呼叫refresh()
方法時會重新讀取和解析組態檔,然後重新建立BeanFactory
和Bean
的定義。如果容器已經被重新整理過,則需要先銷燬所有的單例Bean
,關閉BeanFactory
,然後重新建立。通常,這個功能用於開發過程中或者測試中,不推薦在生產環境使用,因為它的開銷和風險都很大。
我們來看一下重點,載入組態檔的操作在哪裡?這裡圖上我標註出來了,obtainFreshBeanFactory
方法裡面有個refreshBeanFactory
方法。
refreshBeanFactory
方法是個抽象方法,我們來看看實現類是怎麼實現的,根據繼承關係找到實現類的refreshBeanFactory
方法。
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
,邏輯很複雜,我們下面來進行單獨分析。
this.loadBeanDefinitions
方法是在 AbstractApplicationContext
的子類中實現的,這種模式是一個典型的模板方法設計模式的例子。在模板方法設計模式中,一個演演算法的框架(即一系列的步驟)被定義在父類別的方法中,但是一些步驟的具體實現會延遲到子類中完成。
AbstractApplicationContext
提供了 refreshBeanFactory
方法的框架,這個方法定義了重新整理 BeanFactory
的步驟,但是它將 loadBeanDefinitions
的具體實現留給了子類。子類需要根據具體的儲存資源型別(比如 XML
檔案、Java
註解、Groovy
指令碼等)來實現這個方法。
子類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的詳細狀態
這裡看到還有一個reader.loadBeanDefinitions(configLocations);這是在做什麼呢?下面接著來看!
debug的時候可以看到這裡的reader是XmlBeanDefinitionReader,點選跟蹤reader.loadBeanDefinitions(configLocations);方法,呼叫的方法在AbstractBeanDefinitionReader,而XmlBeanDefinitionReader 繼承自 AbstractBeanDefinitionReader。
這裡組態檔迴圈載入,有一個count += this.loadBeanDefinitions(location); 繼續跟蹤!
這段程式碼的邏輯動作大致為:
我們還是看重點,繼續跟蹤裡面的loadBeanDefinitions
程式碼提出來分析:
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方法!
doLoadBeanDefinitions方法做了什麼?
具體步驟如下:
這裡重點是registerBeanDefinitions方法,繼續跟蹤程式碼
繼續看重點,最終追到doRegisterBeanDefinitions方法
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方法
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方法,繼續來看看。
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框架預設名稱空間下的四種主要標籤:
這樣,Spring可以根據這些元素來構建應用上下文中的bean工廠。
偵錯可以發現,xml已經解析出初步的雛形了
在這裡似乎沒看到bean元素,這是怎麼解析的呢?讓我們一步一步來,在上面提到的parseDefaultElement方法中有呼叫processBeanDefinition方法,來看看這是幹嘛的。
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了
有人會好奇了,這是如何將 xml 元素封裝為 BeanDefinitionHolder呢
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 方法,我們來看下
方法 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。
從讀取XML組態檔到註冊BeanDefinition的完整流程:
1.載入組態檔:
2.初始化BeanFactory並進行重新整理:
3.讀取XML組態檔:
4.解析XML檔案:
5.Bean定義的解析和註冊:
6.事件釋出:
這個詳細流程顯示了從載入組態檔到解析並註冊BeanDefinition所涉及的複雜過程,它展示了Spring框架處理Bean宣告和依賴關係的內部機制。這是Spring依賴注入核心功能的基礎,確保了Bean能夠按照定義被範例化和管理。
1. XML組態檔解析:
Spring容器在解析組態檔時主要使用了 XmlBeanDefinitionReader 類。此外,還用到了 BeanDefinitionDocumentReader 來進行具體的檔案讀取。
BeanDefinitionReader 負責從XML檔案讀取bean定義並轉換為Spring內部的 BeanDefinition 物件。
parseBeanDefinitionElement 在XML元素被讀取時呼叫,它的輸出是 BeanDefinitionHolder 物件,其中包含了bean定義以及名稱和別名。
2. Bean定義解析:
BeanDefinition 物件是通過讀取XML中的 <bean> 元素並提取相關屬性來建立的。這些屬性包括bean的類名、作用域、生命週期回撥等。
parseBeanDefinitionAttributes 方法用於提取bean元素上的屬性,並設定到 AbstractBeanDefinition 物件中。
parseBeanDefinitionAttributes 方法處理的屬性包括 scope、lazy-init、autowire 等,這些屬性會決定bean的行為和它如何與其他bean互動。
3. Bean名稱與別名:
如果沒有提供id或name,Spring會自動生成一個唯一的bean名稱。它可能基於類名加上一定的序列號。提示:分析parseBeanDefinitionElement方法時有說過。
別名可以為bean提供額外的名稱,這在需要參照相同的bean但在不同上下文中使用不同名稱時很有用。在 parseBeanDefinitionElement 方法中,別名是通過解析 name 屬性並以逗號、分號或空格作為分隔符來處理的。
4. Bean作用域與生命週期屬性:
通過設定 <bean> 元素的 scope 屬性定義bean的作用域。singleton 表示全域性唯一範例,而 prototype 表示每次請求都建立一個新的範例。
lazy-init 屬性確定bean是否應該在啟動時延遲初始化,init-method 和 destroy-method 定義了bean的初始化和銷燬時呼叫的方法。
5. Bean註冊:
BeanDefinition 物件在解析後,通過 DefaultListableBeanFactory.registerBeanDefinition 方法註冊到Spring容器中。
如果發現名稱衝突,會丟擲 BeanDefinitionStoreException。如果是在不同的組態檔中定義相同名稱的bean,後者通常會覆蓋前者。
6. 例外處理:
Spring會通過丟擲 BeanDefinitionStoreException 來告知使用者設定錯誤。異常資訊會詳細說明錯誤的原因和位置。
Spring的錯誤處理機制包括異常的詳細資訊和精確的定位,這對於開發者快速識別設定錯誤非常有幫助。
1.範例化BeanFactory:
2.載入Bean定義:
3.BeanFactoryPostProcessor的執行:
4.BeanPostProcessor的註冊:
5.單例Bean的預範例化:
6.依賴注入:
7.Bean初始化:
8.Aware介面的呼叫:
9.BeanPostProcessor的後處理:
10.事件釋出:
11.使用Bean:
12.關閉容器:
在整個生命週期過程中,每個Bean的狀態被ApplicationContext和BeanFactory跟蹤和管理,從建立、依賴注入、初始化,到銷燬,確保Bean在正確的時機被建立和清理。
在Spring中的refresh方法:
1. 何時觸發:
2. 為什麼需要手動觸發:
在Spring Boot中的refresh方法:
Spring Boot大大簡化了Spring應用的設定和啟動過程。它自動設定了Spring的ApplicationContext並在合適的時候呼叫了refresh方法。
1. 自動觸發:
2. 可能的手動觸發場景:
一般情況下的建議:
在Spring Boot中,refresh方法的基本行為保持不變,因為Spring Boot建立在Spring之上,遵循相同的基本原則。不過,Spring Boot確實為應用上下文的管理和重新整理提供了更多的自動化和便利性:
1.自動設定:
2.外部化設定:
3.條件重新整理:
4.生命週期管理:
5.Actuator endpoints:
6.設定更改監聽:
7.錯誤處理:
綜上所述,Spring Boot提供了更為自動化的方式來處理應用上下文的變化,很多時候無需手動呼叫refresh方法。不過,如果需要在執行時動態改變Bean的設定,並希望這些改變立即生效,那麼可能還需要使用Spring提供的refresh方法或通過Spring Boot Actuator的相關端點來達成這一目的。