死磕Spring之IoC篇 - BeanDefinition 的加載階段(XML 文件)


該系列文章是本人在學習 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 接口的類圖如下所示:

總覽:

  • 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 對象,這個過程會經過校驗、解析兩個步驟


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM