《spring原始碼解讀》 - IoC 之解析 import 標籤

AnonyStar發表於2020-09-02

spring-framework.jpg

在上一文中我們分析了註冊 BeanDefinition 的過程,在其中我們瞭解到在解析跟節點和子節點時分兩種情況,對於預設名稱空間的標籤我們通過 DefaultBeanDefinitionDocumentReader#parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) 進行處理,而對於自定義標籤則通過 BeanDefinitionParserDelegate#parseCustomElement(Element ele) 方法進行處理。


這裡我們首先對預設名稱空間的解析進行開始解讀, #parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) 方法的程式碼如下:

	public static final String NESTED_BEANS_ELEMENT = "beans";

	public static final String ALIAS_ELEMENT = "alias";

	public static final String NAME_ATTRIBUTE = "name";

	public static final String ALIAS_ATTRIBUTE = "alias";

	public static final String IMPORT_ELEMENT = "import";
	/**
	 * 如果根節點或者子節點採用預設名稱空間的話 採用預設的解析方式
	 * @param ele
	 * @param delegate
	 */
	private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
		// import 標籤
		if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
			importBeanDefinitionResource(ele);
		}
		else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
			//alias 標籤
			processAliasRegistration(ele);
		}
		else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
			//處理 bean 標籤 這是spring中很核心的標籤處理
			processBeanDefinition(ele, delegate);
		}
		else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
			// 處理 beans 標籤
			doRegisterBeanDefinitions(ele);
		}
	}
  • 通過上述程式碼我們可以的看到預設標籤包含 importaliasbeanbeans , 本文將對 import 的解析進行解讀

1. Import 案例

經歷過 Spring 配置檔案的小夥伴都知道,如果工程比較大,配置檔案的維護會讓人覺得恐怖,檔案太多了,想象將所有的配置都放在一個 spring.xml 配置檔案中,哪種後怕感是不是很明顯?
所有針對這種情況 Spring 提供了一個分模組的思路,利用 import 標籤,例如我們可以構造一個這樣的 spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <import resource="spring-student.xml"/>

    <import resource="spring-student-dtd.xml"/>

</beans>

spring.xml 配置檔案中,使用 import 標籤的方式匯入其他模組的配置檔案。

  • 如果有配置需要修改直接修改相應配置檔案即可。
  • 若有新的模組需要引入直接增加 import 即可。

這樣大大簡化了配置後期維護的複雜度,同時也易於管理。

2. importBeanDefinitionResource

DefaultBeanDefinitionDocumentReader#parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) 方法使用者解析 import 標籤,方法程式碼如下:

	protected void importBeanDefinitionResource(Element ele) {
		// 1.獲取 節點 屬性resource的值
		String location = ele.getAttribute(RESOURCE_ATTRIBUTE);
		//2. 判斷是否為空,為空直接返回
		if (!StringUtils.hasText(location)) {
			getReaderContext().error("Resource location must not be empty", ele);
			return;
		}

		//3.解析 系統屬性  ${user.dir}
		// Resolve system properties: e.g. "${user.dir}"
		location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location);

		// 實際 Resource 集合, 即 import 的地址,
		Set<Resource> actualResources = new LinkedHashSet<>(4);

		// Discover whether the location is an absolute or relative URI
		// 檢查路徑 location 是絕對路徑還是相對路徑
		boolean absoluteLocation = false;
		try {
			absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute();
		}
		catch (URISyntaxException ex) {
			// cannot convert to an URI, considering the location relative
			// unless it is the well-known Spring prefix "classpath*:"
		}

		// Absolute or relative?
		// 絕對路徑
		if (absoluteLocation) {
			try {
				// 解析 location 得到 resource 並且新增到 actualResources中
				int importCount = getReaderContext().getReader().loadBeanDefinitions(location, actualResources);
				if (logger.isTraceEnabled()) {
					logger.trace("Imported " + importCount + " bean definitions from URL location [" + location + "]");
				}
			}
			catch (BeanDefinitionStoreException ex) {
				getReaderContext().error(
						"Failed to import bean definitions from URL location [" + location + "]", ele, ex);
			}
		}
		else {
			//相對路徑
			// No URL -> considering resource location as relative to the current file.
			try {
				int importCount;
				// 解析 location 路徑,得到相對路徑的 Resource relativeResource
				Resource relativeResource = getReaderContext().getResource().createRelative(location);
				//存在
				if (relativeResource.exists()) {
					// 載入 resource 中的 Definition
					importCount = getReaderContext().getReader().loadBeanDefinitions(relativeResource);
					// 新增 Resource 到 relativeResource
					actualResources.add(relativeResource);
				}
				else {
					// 獲取根路徑
					String baseLocation = getReaderContext().getResource().getURL().toString();
					// 通過 根路徑與相對路徑獲取到 Resource 並且新增到 actualResources,同時載入相應的 BeanDefinition
					importCount = getReaderContext().getReader().loadBeanDefinitions(
							StringUtils.applyRelativePath(baseLocation, location), actualResources);
				}
				if (logger.isTraceEnabled()) {
					logger.trace("Imported " + importCount + " bean definitions from relative location [" + location + "]");
				}
			}
			catch (IOException ex) {
				getReaderContext().error("Failed to resolve current resource location", ele, ex);
			}
			catch (BeanDefinitionStoreException ex) {
				getReaderContext().error(
						"Failed to import bean definitions from relative location [" + location + "]", ele, ex);
			}
		}
		// 解析成功後,進行監聽器啟用處理
		Resource[] actResArray = actualResources.toArray(new Resource[0]);
		getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele));
	}

解析 import 標籤的過程較為清晰,整個過程如下

  • <1> 處,獲取 Resource 屬性的值,該值表示資源的路徑。

  • <2> 處,解析路徑中的系統屬性,如 "${user.dir}"

  • <3> 處,判斷資源路徑 location 是絕對路徑還是相對路徑。詳細解析,見 「2.1 判斷路徑」 。

  • <4> 處,如果是絕對路徑,則調遞迴呼叫 Bean 的解析過程,進行另一次的解析。詳細解析,見 「2.2 處理絕對路徑」 。

  • <5> 處,如果是相對路徑,則先計算出絕對路徑得到 Resource ,然後進行解析。詳細解析,見 「2.3 處理相對路徑」 。

  • <6> 處,通知監聽器,完成解析。

  • 上述程式碼的執行過程 UML 如下:


2.1 判斷路徑

在上述程式碼中,通過判斷 location 是否是絕對路徑的程式碼如下:

absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute();
  • ResourcePatternUtils.isUrl(location) 如果是以 classpath*: 或者 classpath: 開頭則為絕對路徑,能夠通過該 location 構建 java.net.URL 為絕對路徑
  • 根據 location 構建 java.net.URI 判斷呼叫 #isAbsolute() 方法,判斷是否為絕對路徑

2.2 處理絕對路徑


如果  location  為絕對路徑,則呼叫  #loadBeanDefinitions(String location, Set<Resource> actualResources) , 方法。該方在  org.springframework.beans.factory.support.AbstractBeanDefinitionReader  中定義,程式碼如下:

	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;
		}
	}
  • 上述程式碼執行過程的 UML 圖如下





整個邏輯比較簡單 :

  • 首先,獲取 ResourceLoader 物件。
  • 然後,根據不同的 ResourceLoader 執行不同的邏輯,主要是可能存在多個 Resource 。
  • 最終,都會迴歸到 XmlBeanDefinitionReader#loadBeanDefinitions(Resource... resources) 方法,所以這是一個遞迴的過程。
  • 另外,獲得到的 Resource 的物件或陣列,都會新增到 actualResources 中。

2.3 處理相對路徑


如果 location 是相對路徑,則會根據相應的 Resource 計算出相應的相對路徑的 Resource 物件 ,然後:

  • 若該 Resource 存在,則呼叫 XmlBeanDefinitionReader#loadBeanDefinitions() 方法,進行 BeanDefinition 載入。

  • 否則,構造一個絕對 location ( 即 StringUtils.applyRelativePath(baseLocation, location) 處的程式碼),並呼叫 #loadBeanDefinitions(String location, Set<Resource> actualResources) 方法,與絕對路徑過程一樣

3. 小結


至此, import 標籤解析完畢,整個過程比較清晰明瞭:獲取 source 屬性值,得到正確的資源路徑,然後呼叫 XmlBeanDefinitionReader#loadBeanDefinitions(Resource... resources) 方法,進行遞迴的 BeanDefinition 載入。

本文由AnonyStar 釋出,可轉載但需宣告原文出處。
仰慕「優雅編碼的藝術」 堅信熟能生巧,努力改變人生
歡迎關注微信公賬號 :雲棲簡碼 獲取更多優質文章
更多文章關注筆者部落格 :雲棲簡碼

相關文章