該系列文章是本人在學習 Spring 的過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 Spring 原始碼分析 GitHub 地址 進行閱讀
Spring 版本:5.1.14.RELEASE
開始閱讀這一系列文章之前,建議先檢視《深入瞭解 Spring IoC(面試題)》這一篇文章
該系列其他文章請檢視:《死磕 Spring 之 IoC 篇 - 文章導讀》
BeanDefinition 的載入階段(XML 檔案)
上一篇文章 《Bean 的“前身”》 對 BeanDefinition 進行了介紹,Bean 是根據 BeanDefinition 配置元資訊物件生成的。我們在 Spring 中通常以這兩種方式定義一個 Bean:面向資源(XML、Properties)、面向註解,那麼 Spring 是如何將這兩種方式定義的資訊轉換成 BeanDefinition 物件的,接下來會先分析面向資源(XML、Properties)這種方式 Spring 是如何處理的
下來熟悉一段程式碼:
dependency-lookup-context.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- <context:component-scan base-package="org.geekbang.thinking.in.spring.ioc.overview" /> -->
<bean id="user" class="org.geekbang.thinking.in.spring.ioc.overview.domain.User">
<property name="id" value="1"/>
<property name="name" value="小馬哥"/>
</bean>
</beans>
// 建立 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 容器的使用過程大致如下:
- 建立 BeanFactory 物件(底層 IoC 容器)
- 建立 BeanDefinitionReader 物件(資源解析器),關聯第
1
步建立的 BeanFactory - 通過 BeanDefinitionReader 載入 XML 配置檔案資源,解析出所有的 BeanDefinition 物件
- 進行依賴查詢
上面的第 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 讀取器,定義了載入資源的方法,程式碼如下:
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 介面,程式碼如下:
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<Resource> 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 配置元資訊物件並註冊
建構函式
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<Set<EncodedResource>> 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 資源的入口,方法如下:
@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<EncodedResource> 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 物件,目的是讓資源物件可設定編碼
- 獲取當前執行緒正在載入的 Resource 資源集合,新增當前 Resource,防止重複載入
- 從 Resource 資源獲取 InputStream 流物件(支援編碼)
- 【核心】呼叫
doLoadBeanDefinitions(InputSource inputSource, Resource resource)
方法,執行載入 Resource 資源過程,解析出 BeanDefinition 進行註冊 - 從當前執行緒移除當前載入的 Resource 物件
doLoadBeanDefinitions 方法
doLoadBeanDefinitions(InputSource inputSource, Resource resource)
方法,執行載入 Resource 資源過程,解析出 BeanDefinition 進行註冊,方法如下:
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 各種異常
}
- 呼叫
doLoadDocument(InputSource inputSource, Resource resource)
方法,獲取 XML Document 例項 - 呼叫
registerBeanDefinitions(Document doc, Resource resource)
方法,根據 Document 例項,解析出 BeanDefinition 們並註冊,返回註冊數量
doLoadDocument 方法
doLoadDocument(InputSource inputSource, Resource resource)
方法,獲取 Resource 資源對應的 XML Document 例項,方法如下:
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 檔案的正確性
}
- 獲取
org.xml.sax.EntityResolver
實體解析器,ResourceEntityResolver,根據 publicId 和 systemId 獲取對應的 DTD 或 XSD 檔案,用於對 XML 檔案進行驗證,這個類比較關鍵,在後續文章會講到 - 獲取 XML 檔案驗證模式,保證 XML 檔案的正確性,通常情況下都是 XSD 模式
- 獲取指定的驗證模式,如果手動指定,則直接返回,通常情況下不會
- 從 Resource 資源中獲取驗證模式,根據 XML 檔案的內容進行獲取,如果包含
DOCTYPE
內容則為 DTD 模式,否則為 XSD 模式 - 如果還沒有獲取到驗證模式,則預設為 XSD 模式
- 通過 DefaultDocumentLoader 根據 Resource 獲取一個 Document 物件
- 建立 DocumentBuilderFactory 物件
factory
,開啟校驗 - 根據
factory
建立 DocumentBuilder 物件builder
,設定 EntityResolver(第1
步建立的)、ErrorHandler 屬性 - 通過
builder
對inputSource
(Resource 資源)進行解析,返回一個 Document 物件
- 建立 DocumentBuilderFactory 物件
上述過程目的就是獲取到 Resource 資源對應的 Document 物件,需要經過校驗和解析兩個過程
registerBeanDefinitions 方法
registerBeanDefinitions(Document doc, Resource resource)
方法,根據 Document 例項,解析出 BeanDefinition 們並註冊,返回註冊數量,方法如下:
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;
}
- 建立 DefaultBeanDefinitionDocumentReader 物件
documentReader
- 獲取已註冊的 BeanDefinition 數量
- 建立 XmlReaderContext 物件(讀取 Resource 資源的上下文物件),注意這裡會初始化一個 DefaultNamespaceHandlerResolver 物件,用於處理自定義標籤(XML 檔案),比較關鍵,在後續文章會講到
- 根據 Document、XmlReaderContext 解析出所有的 BeanDefinition 並註冊,呼叫
DefaultBeanDefinitionDocumentReader#registerBeanDefinitions(Document doc, XmlReaderContext readerContext)
方法 - 計算新註冊的 BeanDefinition 數量並返回
擴充:DTD 與 XSD 的區別?
DTD(Document Type Definition),即文件型別定義,為 XML 檔案的驗證機制,屬於 XML 檔案中組成的一部分。DTD 是一種保證 XML 文件格式正確的有效驗證方式,它定義了相關 XML 文件的元素、屬性、排列方式、元素的內容型別以及元素的層次結構。其實 DTD 就相當於 XML 中的 “詞彙”和“語法”,我們可以通過比較 XML 檔案和 DTD 檔案 來看文件是否符合規範,元素和標籤使用是否正確。
DTD 在一定的階段推動了 XML 的發展,但是它本身存在著一些缺陷:
- 它沒有使用 XML 格式,而是自己定義了一套格式,相對解析器的重用性較差;而且 DTD 的構建和訪問沒有標準的程式設計介面,導致解析器很難簡單的解析 DTD 文件
- DTD 對元素的型別限制較少;同時其他的約束力也比較弱
- DTD 擴充套件能力較差
- 基於正規表示式的 DTD 文件的描述能力有限
XSD(XML Schemas Definition),即 XML Schema 語言,針對 DTD 的缺陷由 W3C 在 2001 年推出。XML Schema 本身就是一個 XML 文件,使用的是 XML 語法,因此可以很方便的解析 XSD 文件。相對於 DTD,XSD 具有如下優勢:
- XML Schema 基於 XML,沒有專門的語法
- XML Schema 可以像其他 XML 檔案一樣解析和處理
- XML Schema 比 DTD 提供了更豐富的資料型別
- XML Schema 提供可擴充的資料模型
- XML Schema 支援綜合名稱空間
- XML Schema 支援屬性組
總結
我們在 Spring 中通常以這兩種方式定義一個 Bean:面向資源(XML、Properties)、面向註解,對於第一種方式如果定義的是一個 XML 檔案,Spring 會通過 XmlBeanDefinitionReader 載入該 XML 檔案,獲取該 Resource 資源的 org.w3c.dom.Document
物件,這個過程會經過校驗、解析兩個步驟