1. 程式人生 > >死磕Spring之IoC篇 - BeanDefinition 的載入階段(XML 檔案)

死磕Spring之IoC篇 - BeanDefinition 的載入階段(XML 檔案)

> 該系列文章是本人在學習 Spring 的過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 [Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-framework) 進行閱讀 > > Spring 版本:5.1.14.RELEASE > > 開始閱讀這一系列文章之前,建議先檢視[**《深入瞭解 Spring IoC(面試題)》**](https://www.cnblogs.com/lifullmoon/p/14422101.html)這一篇文章 > > 該系列其他文章請檢視:[**《死磕 Spring 之 IoC 篇 - 文章導讀》**](https://www.cnblogs.com/lifullmoon/p/14436372.html) ## BeanDefinition 的載入階段(XML 檔案) 上一篇文章 [**《Bean 的“前身”》**](https://www.cnblogs.com/lifullmoon/p/14434009.html) 對 BeanDefinition 進行了介紹,Bean 是根據 BeanDefinition 配置元資訊物件生成的。我們在 Spring 中通常以這兩種方式定義一個 Bean:**面向資源(XML、Properties)**、**面向註解**,那麼 Spring 是如何將這兩種方式定義的資訊轉換成 BeanDefinition 物件的,接下來會先分析**面向資源(XML、Properties)**這種方式 Spring 是如何處理的 下來熟悉一段程式碼: `dependency-lookup-context.xml`: ```xml
``` ```java // 建立 BeanFactory 容器 DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); // XML 配置檔案 ClassPath 路徑 String location = "classpath:/META-INF/dependency-lookup-context.xml"; // 載入配置 int beanDefinitionsCount = reader.loadBeanDefinitions(location); System.out.println("Bean 定義載入的數量:" + beanDefinitionsCount); // 依賴查詢 System.out.println(beanFactory.getBean("user"));; ``` 這段程式碼是 Spring 中程式設計式使用 IoC 容器,我們可以看到 IoC 容器的使用過程大致如下: 1. 建立 BeanFactory 物件(底層 IoC 容器) 2. 建立 BeanDefinitionReader 物件(資源解析器),關聯第 `1` 步建立的 BeanFactory 3. 通過 BeanDefinitionReader 載入 XML 配置檔案資源,解析出所有的 BeanDefinition 物件 4. 進行依賴查詢 上面的第 `3` 步會解析 Resource 資源,將 XML 檔案中定義的 Bean 解析成 BeanDefinition 配置元資訊物件,並往 BeanDefinitionRegistry 註冊中心註冊,此時並沒有生成對應的 Bean 物件,需要通過依賴查詢獲取到 Bean。當然,我們在實際場景中一般不會這樣使用 Spring,這些工作都會有 Spring 來完成。接下來我們一起來看看 Sping 是如何載入 XML 檔案的 ### BeanDefinitionReader 體系結構 `org.springframework.beans.factory.support.BeanDefinitionReader` 介面的類圖如下所示:
總覽: - `org.springframework.beans.factory.support.BeanDefinitionReader` 介面,BeanDefinition 讀取器 - `org.springframework.beans.factory.support.AbstractBeanDefinitionReader` 抽象類,提供通用的實現,具體的資源載入邏輯在由子類實現 - `org.springframework.beans.factory.xml.XmlBeanDefinitionReader`,XML 檔案資源解析器,解析出 BeanDefinition 配置元資訊物件並註冊 - `org.springframework.beans.factory.support.PropertiesBeanDefinitionReader`,Properties 檔案資源解析器 ### BeanDefinitionReader 介面 `org.springframework.beans.factory.support.BeanDefinitionReader` 介面,BeanDefinition 讀取器,定義了載入資源的方法,程式碼如下: ```java public interface BeanDefinitionReader { /** 返回 BeanDefinition 註冊中心 */ BeanDefinitionRegistry getRegistry(); /** 返回 Resource 資源載入器,預設為 PathMatchingResourcePatternResolver */ @Nullable ResourceLoader getResourceLoader(); /** 返回類載入器 */ @Nullable ClassLoader getBeanClassLoader(); /** 返回 Bean 的名稱生成器,預設為 DefaultBeanNameGenerator */ BeanNameGenerator getBeanNameGenerator(); /** 從 Resource 資源中載入 BeanDefinition 並返回數量 */ int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException; int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException; int loadBeanDefinitions(String location) throws BeanDefinitionStoreException; int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException; } ``` ### AbstractBeanDefinitionReader 抽象類 `org.springframework.beans.factory.support.AbstractBeanDefinitionReader` 抽象類,實現了 BeanDefinitionReader 和 EnvironmentCapable 介面,程式碼如下: ```java public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader, EnvironmentCapable { private final BeanDefinitionRegistry registry; @Nullable private ResourceLoader resourceLoader; @Nullable private ClassLoader beanClassLoader; private Environment environment; private BeanNameGenerator beanNameGenerator = new DefaultBeanNameGenerator(); protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) { Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); this.registry = registry; // Determine ResourceLoader to use. if (this.registry instanceof ResourceLoader) { this.resourceLoader = (ResourceLoader) this.registry; } else { this.resourceLoader = new PathMatchingResourcePatternResolver(); } // Inherit Environment if possible if (this.registry instanceof EnvironmentCapable) { this.environment = ((EnvironmentCapable) this.registry).getEnvironment(); } else { this.environment = new StandardEnvironment(); } } @Override public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException { Assert.notNull(resources, "Resource array must not be null"); int count = 0; for (Resource resource : resources) { count += loadBeanDefinitions(resource); } return count; } @Override public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException { return loadBeanDefinitions(location, null); } public int loadBeanDefinitions(String location, @Nullable Set actualResources) throws BeanDefinitionStoreException { // 獲得 ResourceLoader 物件 ResourceLoader resourceLoader = getResourceLoader(); if (resourceLoader == null) { throw new BeanDefinitionStoreException( "Cannot load bean definitions from location [" + location + "]: no ResourceLoader available"); } if (resourceLoader instanceof ResourcePatternResolver) { // Resource pattern matching available. try { // 獲得 Resource 陣列,因為 Pattern 模式匹配下,可能有多個 Resource 。例如說,Ant 風格的 location Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location); // 載入 BeanDefinition 們 int count = loadBeanDefinitions(resources); if (actualResources != null) { // 新增到 actualResources 中 Collections.addAll(actualResources, resources); } if (logger.isTraceEnabled()) { logger.trace("Loaded " + count + " bean definitions from location pattern [" + location + "]"); } return count; } catch (IOException ex) { throw new BeanDefinitionStoreException( "Could not resolve bean definition resource pattern [" + location + "]", ex); } } else { // Can only load single resources by absolute URL. // 獲得 Resource 物件 Resource resource = resourceLoader.getResource(location); // 載入 BeanDefinition 們 int count = loadBeanDefinitions(resource); if (actualResources != null) { // 新增到 actualResources 中 actualResources.add(resource); } if (logger.isTraceEnabled()) { logger.trace("Loaded " + count + " bean definitions from location [" + location + "]"); } return count; } } @Override public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException { Assert.notNull(locations, "Location array must not be null"); int count = 0; for (String location : locations) { count += loadBeanDefinitions(location); } return count; } // ... 省略相關程式碼 } ``` 在實現的方法中,最終都會呼叫 `int loadBeanDefinitions(Resource resource)` 這個方法,該方法在子類中實現 ### XmlBeanDefinitionReader `org.springframework.beans.factory.xml.XmlBeanDefinitionReader`,XML 檔案資源解析器,解析出 BeanDefinition 配置元資訊物件並註冊 #### 建構函式 ```java public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader { /** * 禁用驗證模式 */ public static final int VALIDATION_NONE = XmlValidationModeDetector.VALIDATION_NONE; /** * 自動獲取驗證模式 */ public static final int VALIDATION_AUTO = XmlValidationModeDetector.VALIDATION_AUTO; /** * DTD 驗證模式 */ public static final int VALIDATION_DTD = XmlValidationModeDetector.VALIDATION_DTD; /** * XSD 驗證模式 */ public static final int VALIDATION_XSD = XmlValidationModeDetector.VALIDATION_XSD; /** Constants instance for this class. */ private static final Constants constants = new Constants(XmlBeanDefinitionReader.class); /** * 驗證模式,預設為自動模式。 */ private int validationMode = VALIDATION_AUTO; private boolean namespaceAware = false; private Class documentReaderClass = DefaultBeanDefinitionDocumentReader.class; /** * 解析過程中異常處理器 */ private ProblemReporter problemReporter = new FailFastProblemReporter(); private ReaderEventListener eventListener = new EmptyReaderEventListener(); private SourceExtractor sourceExtractor = new NullSourceExtractor(); @Nullable private NamespaceHandlerResolver namespaceHandlerResolver; private DocumentLoader documentLoader = new DefaultDocumentLoader(); @Nullable private EntityResolver entityResolver; private ErrorHandler errorHandler = new SimpleSaxErrorHandler(logger); /** * XML 驗證模式探測器 */ private final XmlValidationModeDetector validationModeDetector = new XmlValidationModeDetector(); /** * 當前執行緒,正在載入的 EncodedResource 集合。 */ private final ThreadLocal> resourcesCurrentlyBeingLoaded = new NamedThreadLocal<>( "XML bean definition resources currently being loaded"); /** * Create new XmlBeanDefinitionReader for the given bean factory. * @param registry the BeanFactory to load bean definitions into, * in the form of a BeanDefinitionRegistry */ public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) { super(registry); } } ``` #### loadBeanDefinitions 方法 `loadBeanDefinitions(Resource resource)` 方法,解析 Resource 資源的入口,方法如下: ```java @Override public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException { return loadBeanDefinitions(new EncodedResource(resource)); } public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { Assert.notNull(encodedResource, "EncodedResource must not be null"); if (logger.isTraceEnabled()) { logger.trace("Loading XML bean definitions from " + encodedResource); } // <1> 獲取當前執行緒正在載入的 Resource 資源集合,添加當前 Resource,防止重複載入 Set currentResources = this.resourcesCurrentlyBeingLoaded.get(); if (currentResources == null) { currentResources = new HashSet<>(4); this.resourcesCurrentlyBeingLoaded.set(currentResources); } if (!currentResources.add(encodedResource)) { // 將當前資源加入記錄中。如果已存在,丟擲異常,防止迴圈載入同一資源出現死迴圈 throw new BeanDefinitionStoreException( "Detected cyclic loading of " + encodedResource + " - check your import definitions!"); } try { // <2> 從 Resource 資源獲取 InputStream 流物件(支援編碼) InputStream inputStream = encodedResource.getResource().getInputStream(); try { InputSource inputSource = new InputSource(inputStream); if (encodedResource.getEncoding() != null) { inputSource.setEncoding(encodedResource.getEncoding()); } // <3> 【核心】執行載入 Resource 資源過程,解析出 BeanDefinition 進行註冊 return doLoadBeanDefinitions(inputSource, encodedResource.getResource()); } finally { // 關閉流 inputStream.close(); } } catch (IOException ex) { throw new BeanDefinitionStoreException( "IOException parsing XML document from " + encodedResource.getResource(), ex); } finally { // <4> 從當前執行緒移除當前載入的 Resource 物件 currentResources.remove(encodedResource); if (currentResources.isEmpty()) { this.resourcesCurrentlyBeingLoaded.remove(); } } } ``` 將 Resource 封裝成 EncodedResource 物件,目的是讓資源物件可設定編碼 1. 獲取當前執行緒正在載入的 Resource 資源集合,添加當前 Resource,防止重複載入 2. 從 Resource 資源獲取 InputStream 流物件(支援編碼) 3. 【核心】呼叫 `doLoadBeanDefinitions(InputSource inputSource, Resource resource)` 方法,執行載入 Resource 資源過程,解析出 BeanDefinition 進行註冊 4. 從當前執行緒移除當前載入的 Resource 物件 #### doLoadBeanDefinitions 方法 `doLoadBeanDefinitions(InputSource inputSource, Resource resource)` 方法,執行載入 Resource 資源過程,解析出 BeanDefinition 進行註冊,方法如下: ```java protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException { try { // <1> 獲取 XML Document 例項 Document doc = doLoadDocument(inputSource, resource); // <2> 根據 Document 例項,解析出 BeanDefinition 們並註冊,返回註冊數量 int count = registerBeanDefinitions(doc, resource); if (logger.isDebugEnabled()) { logger.debug("Loaded " + count + " bean definitions from " + resource); } return count; } // 省略 catch 各種異常 } ``` 1. 呼叫 `doLoadDocument(InputSource inputSource, Resource resource)` 方法,獲取 XML Document 例項 2. 呼叫 `registerBeanDefinitions(Document doc, Resource resource)` 方法,根據 Document 例項,解析出 BeanDefinition 們並註冊,返回註冊數量 #### doLoadDocument 方法 `doLoadDocument(InputSource inputSource, Resource resource)` 方法,獲取 Resource 資源對應的 XML Document 例項,方法如下: ```java protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception { // <3> 通過 DefaultDocumentLoader 根據 Resource 獲取一個 Document 物件 return this.documentLoader.loadDocument(inputSource, getEntityResolver(), // <1> 獲取 `org.xml.sax.EntityResolver` 實體解析器,ResourceEntityResolver this.errorHandler, getValidationModeForResource(resource), isNamespaceAware()); // <2> 獲取 XML 檔案驗證模式,保證 XML 檔案的正確性 } ``` 1. 獲取 `org.xml.sax.EntityResolver` 實體解析器,**ResourceEntityResolver**,根據 publicId 和 systemId 獲取對應的 DTD 或 XSD 檔案,用於對 XML 檔案進行驗證,這個類比較**關鍵**,在後續文章會講到 2. 獲取 XML 檔案驗證模式,保證 XML 檔案的正確性,通常情況下都是 **XSD 模式** 1. 獲取指定的驗證模式,如果手動指定,則直接返回,通常情況下不會 2. 從 Resource 資源中獲取驗證模式,根據 XML 檔案的內容進行獲取,如果包含 `DOCTYPE` 內容則為 DTD 模式,否則為 XSD 模式 3. 如果還沒有獲取到驗證模式,則預設為 XSD 模式 3. 通過 DefaultDocumentLoader 根據 Resource 獲取一個 Document 物件 1. 建立 DocumentBuilderFactory 物件 `factory`,開啟校驗 2. 根據 `factory` 建立 DocumentBuilder 物件 `builder`,設定 EntityResolver(第 `1` 步建立的)、ErrorHandler 屬性 3. 通過 `builder` 對 `inputSource`(Resource 資源)進行解析,返回一個 Document 物件 上述過程目的就是獲取到 Resource 資源對應的 Document 物件,需要經過校驗和解析兩個過程 #### registerBeanDefinitions 方法 `registerBeanDefinitions(Document doc, Resource resource)` 方法,根據 Document 例項,解析出 BeanDefinition 們並註冊,返回註冊數量,方法如下: ```java public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { // <1> 建立 BeanDefinitionDocumentReader 物件 BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); // <2> 獲取已註冊的 BeanDefinition 數量 int countBefore = getRegistry().getBeanDefinitionCount(); // <3> 建立 XmlReaderContext 物件(讀取 Resource 資源的上下文物件) // <4> 根據 Document、XmlReaderContext 解析出所有的 BeanDefinition 並註冊 documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); // <5> 計算新註冊的 BeanDefinition 數量 return getRegistry().getBeanDefinitionCount() - countBefore; } ``` 1. 建立 DefaultBeanDefinitionDocumentReader 物件 `documentReader` 2. 獲取已註冊的 BeanDefinition 數量 3. 建立 XmlReaderContext 物件(讀取 Resource 資源的上下文物件),注意這裡會初始化一個 **DefaultNamespaceHandlerResolver** 物件,用於處理自定義標籤(XML 檔案),比較**關鍵**,在**後續文章**會講到 4. 根據 Document、XmlReaderContext 解析出所有的 BeanDefinition 並註冊,呼叫 `DefaultBeanDefinitionDocumentReader#registerBeanDefinitions(Document doc, XmlReaderContext readerContext)` 方法 5. 計算新註冊的 BeanDefinition 數量並返回 ### 拓展:DTD 與 XSD 的區別? **DTD(Document Type Definition)**,即文件型別定義,為 XML 檔案的驗證機制,屬於 XML 檔案中組成的一部分。DTD 是一種保證 XML 文件格式正確的有效驗證方式,它定義了相關 XML 文件的元素、屬性、排列方式、元素的內容型別以及元素的層次結構。其實 DTD 就相當於 XML 中的 “詞彙”和“語法”,我們可以通過比較 XML 檔案和 DTD 檔案 來看文件是否符合規範,元素和標籤使用是否正確。 DTD 在一定的階段推動了 XML 的發展,但是它本身存在著一些**缺陷**: 1. 它沒有使用 XML 格式,而是自己定義了一套格式,相對解析器的重用性較差;而且 DTD 的構建和訪問沒有標準的程式設計介面,導致解析器很難簡單的解析 DTD 文件 2. DTD 對元素的型別限制較少;同時其他的約束力也比較弱 3. DTD 擴充套件能力較差 4. 基於正則表示式的 DTD 文件的描述能力有限 **XSD(XML Schemas Definition)**,即 XML Schema 語言,針對 DTD 的缺陷由 W3C 在 2001 年推出。XML Schema 本身就是一個 XML 文件,使用的是 XML 語法,因此可以很方便的解析 XSD 文件。相對於 DTD,XSD 具有如下**優勢**: 1. XML Schema 基於 XML,沒有專門的語法 2. XML Schema 可以像其他 XML 檔案一樣解析和處理 3. XML Schema 比 DTD 提供了更豐富的資料型別 4. XML Schema 提供可擴充的資料模型 5. XML Schema 支援綜合名稱空間 6. XML Schema 支援屬性組 ### 總結 我們在 Spring 中通常以這兩種方式定義一個 Bean:**面向資源(XML、Properties)**、**面向註解**,對於第一種方式如果定義的是一個 XML 檔案,Spring 會通過 XmlBeanDefinitionReader 載入該 XML 檔案,獲取該 Resource 資源的 `org.w3c.dom.Document` 物件,這個過程會經過校驗、解析兩