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

月圓吖發表於2021-02-23

該系列文章是本人在學習 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 容器的使用過程大致如下:

  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 介面的類圖如下所示:

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

總覽:

  • 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 物件,目的是讓資源物件可設定編碼

  1. 獲取當前執行緒正在載入的 Resource 資源集合,新增當前 Resource,防止重複載入
  2. 從 Resource 資源獲取 InputStream 流物件(支援編碼)
  3. 【核心】呼叫 doLoadBeanDefinitions(InputSource inputSource, Resource resource) 方法,執行載入 Resource 資源過程,解析出 BeanDefinition 進行註冊
  4. 從當前執行緒移除當前載入的 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 各種異常
}
  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 例項,方法如下:

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. 通過 builderinputSource(Resource 資源)進行解析,返回一個 Document 物件

上述過程目的就是獲取到 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;
}
  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 物件,這個過程會經過校驗、解析兩個步驟

相關文章