死磕Spring之IoC篇 - BeanDefinition 的載入階段(XML 檔案)
阿新 • • 發佈:2021-02-23
> 該系列文章是本人在學習 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 extends BeanDefinitionDocumentReader> 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` 物件,這個過程會經過校驗、解析兩