1. 程式人生 > >死磕Spring之IoC篇 - 解析自定義標籤(XML 檔案)

死磕Spring之IoC篇 - 解析自定義標籤(XML 檔案)

> 該系列文章是本人在學習 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) ## 解析自定義標籤(XML 檔案) 上一篇**《BeanDefinition 的解析階段(XML 檔案)》**文章分析了 Spring 處理 `org.w3c.dom.Document` 物件(XML Document)的過程,會解析裡面的元素。預設名稱空間(為空或者 `http://www.springframework.org/schema/beans`)的元素,例如 `
` 標籤會被解析成 GenericBeanDefinition 物件並註冊。本文會分析 Spring 是如何處理非預設名稱空間的元素,通過 Spring 的實現方式我們如何自定義元素 先來了解一下 XML 檔案中的名稱空間: ```java ``` 上述 XML 檔案 `` 的預設名稱空間為 `http://www.springframework.org/schema/beans`,內部的 `` 標籤沒有定義名稱空間,則使用預設名稱空間 `` 還定義了 **context** 名稱空間為 `http://www.springframework.org/schema/context`,那麼內部的 `
` 標籤就不是預設名稱空間,處理方式也不同。其實 Spring 內部自定義了很多的名稱空間,用於處理不同的場景,原理都一樣,接下來會進行分析。 ### 自定義標籤的實現步驟 擴充套件 Spring XML 元素的步驟如下: 1. 編寫 XML Schema 檔案(XSD 檔案):定義 XML 結構 2. 自定義 NamespaceHandler 實現:定義名稱空間的處理器,實現 **NamespaceHandler** 介面,我們通常繼承 **NamespaceHandlerSupport** 抽象類,Spring 提供了通用實現,只需要實現其 init() 方法即可 3. 自定義 BeanDefinitionParser 實現:繫結名稱空間下不同的 XML 元素與其對應的解析器,因為一個名稱空間下可以有很多個標籤,對於不同的標籤需要不同的 **BeanDefinitionParser** 解析器,在上面的 init() 方法中進行繫結 4. 註冊 XML 擴充套件(`META-INF/spring.handlers` 檔案):名稱空間與名稱空間處理器的對映 5. 編寫 Spring Schema 資源對映檔案(`META-INF/spring.schemas` 檔案):XML Schema 檔案通常定義為網路的形式,在無網的情況下無法訪問,所以一般在本地的也有一個 XSD 檔案,可通過編寫 `spring.schemas` 檔案,將網路形式的 XSD 檔案與本地的 XSD 檔案進行對映,這樣會**優先**從本地獲取對應的 XSD 檔案 ### Spring 內部自定義標籤預覽 在 `spring-context` 模組的 ClassPath 下可以看到有 `META-INF/spring.handlers`、`META-INF/spring.schemas` 以及對應的 XSD 檔案,如下: - [META-INF/spring.handlers](https://github.com/liu844869663/spring-framework/blob/5.1.x/spring-context/src/main/resources/META-INF/spring.handlers) ```properties http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler http\://www.springframework.org/schema/task=org.springframework.scheduling.config.TaskNamespaceHandler http\://www.springframework.org/schema/cache=org.springframework.cache.config.CacheNamespaceHandler ``` - [META-INF/spring.schemas](https://github.com/liu844869663/spring-framework/blob/5.1.x/spring-context/src/main/resources/META-INF/spring.schemas) ```properties http\://www.springframework.org/schema/context/spring-context.xsd=org/springframework/context/config/spring-context.xsd http\://www.springframework.org/schema/jee/spring-jee.xsd=org/springframework/ejb/config/spring-jee.xsd http\://www.springframework.org/schema/lang/spring-lang.xsd=org/springframework/scripting/config/spring-lang.xsd http\://www.springframework.org/schema/task/spring-task.xsd=org/springframework/scheduling/config/spring-task.xsd http\://www.springframework.org/schema/cache/spring-cache.xsd=org/springframework/cache/config/spring-cache.xsd https\://www.springframework.org/schema/context/spring-context.xsd=org/springframework/context/config/spring-context.xsd https\://www.springframework.org/schema/jee/spring-jee.xsd=org/springframework/ejb/config/spring-jee.xsd https\://www.springframework.org/schema/lang/spring-lang.xsd=org/springframework/scripting/config/spring-lang.xsd https\://www.springframework.org/schema/task/spring-task.xsd=org/springframework/scheduling/config/spring-task.xsd https\://www.springframework.org/schema/cache/spring-cache.xsd=org/springframework/cache/config/spring-cache.xsd ### ... 省略 ``` 其他模組也有這兩種檔案,這裡不一一展示,從上面的 `spring.handlers` 這裡可以看到 **context** 名稱空間對應的是 ContextNamespaceHandler 處理器,先來看一下: ```java public class ContextNamespaceHandler extends NamespaceHandlerSupport { @Override public void init() { registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser()); registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser()); registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser()); registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser()); registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser()); registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser()); registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser()); registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser()); } } ``` 可以看到註冊了不同的標籤所對應的解析器,其中 **component-scan** 對應 **ComponentScanBeanDefinitionParser** 解析器,這裡先看一下,後面再具體分析 ### Spring 如何處理非預設名稱空間的元素 回顧到 [**《BeanDefinition 的載入階段(XML 檔案)》**](https://www.cnblogs.com/lifullmoon/p/14437305.html) 文章中的 XmlBeanDefinitionReader#registerBeanDefinitions 方法,解析 Document 前會先建立 XmlReaderContext 物件(讀取 Resource 資源的上下文物件),建立方法如下: ```java // XmlBeanDefinitionReader.java public XmlReaderContext createReaderContext(Resource resource) { return new XmlReaderContext(resource, this.problemReporter, this.eventListener, this.sourceExtractor, this, getNamespaceHandlerResolver()); } public NamespaceHandlerResolver getNamespaceHandlerResolver() { if (this.namespaceHandlerResolver == null) { this.namespaceHandlerResolver = createDefaultNamespaceHandlerResolver(); } return this.namespaceHandlerResolver; } protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver() { ClassLoader cl = (getResourceLoader() != null ? getResourceLoader().getClassLoader() : getBeanClassLoader()); return new DefaultNamespaceHandlerResolver(cl); } ``` 在 XmlReaderContext 物件中會有一個 **DefaultNamespaceHandlerResolver** 物件 回顧到 [**《BeanDefinition 的解析階段(XML 檔案)》**](https://www.cnblogs.com/lifullmoon/p/14439274.html) 文章中的 DefaultBeanDefinitionDocumentReader#parseBeanDefinitions 方法,如果不是預設的名稱空間,則執行自定義解析,呼叫 `BeanDefinitionParserDelegate#parseCustomElement(Element ele)` 方法,方法如下 ```java // BeanDefinitionParserDelegate.java @Nullable public BeanDefinition parseCustomElement(Element ele) { return parseCustomElement(ele, null); } @Nullable public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) { // <1> 獲取 `namespaceUri` String namespaceUri = getNamespaceURI(ele); if (namespaceUri == null) { return null; } // <2> 通過 DefaultNamespaceHandlerResolver 根據 `namespaceUri` 獲取相應的 NamespaceHandler 處理器 NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); if (handler == null) { error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele); return null; } // <3> 根據 NamespaceHandler 名稱空間處理器處理該標籤 return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd)); } ``` 過程如下: 1. 獲取該節點對應的 `namespaceUri` 名稱空間 2. 通過 DefaultNamespaceHandlerResolver 根據 `namespaceUri` 獲取相應的 NamespaceHandler 處理器 3. 根據 NamespaceHandler 名稱空間處理器處理該標籤 關鍵就在與 DefaultNamespaceHandlerResolver 是如何找到該名稱空間對應的 NamespaceHandler 處理器,我們只是在 `spring.handlers` 檔案中進行關聯,它是怎麼找到的呢,我們進入 **DefaultNamespaceHandlerResolver** 看看 ### DefaultNamespaceHandlerResolver `org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver`,名稱空間的預設處理器 #### 建構函式 ```java public class DefaultNamespaceHandlerResolver implements NamespaceHandlerResolver { /** * The location to look for the mapping files. Can be present in multiple JAR files. */ public static final String DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers"; /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); /** ClassLoader to use for NamespaceHandler classes. */ @Nullable private final ClassLoader classLoader; /** Resource location to search for. */ private final String handlerMappingsLocation; /** Stores the mappings from namespace URI to NamespaceHandler class name / instance. */ @Nullable private volatile Map handlerMappings; public DefaultNamespaceHandlerResolver() { this(null, DEFAULT_HANDLER_MAPPINGS_LOCATION); } public DefaultNamespaceHandlerResolver(@Nullable ClassLoader classLoader) { this(classLoader, DEFAULT_HANDLER_MAPPINGS_LOCATION); } public DefaultNamespaceHandlerResolver(@Nullable ClassLoader classLoader, String handlerMappingsLocation) { Assert.notNull(handlerMappingsLocation, "Handler mappings location must not be null"); this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); this.handlerMappingsLocation = handlerMappingsLocation; } } ``` 注意有一個 `DEFAULT_HANDLER_MAPPINGS_LOCATION` 屬性為 `META-INF/spring.handlers`,我們定義的 `spring.handlers` 在這裡出現了,說明名稱空間和對應的處理器在這裡大概率會有體現 還有一個 `handlerMappingsLocation` 屬性預設為 `META-INF/spring.handlers` #### resolve 方法 `resolve(String namespaceUri)` 方法,根據名稱空間找到對應的 NamespaceHandler 處理器,方法如下: ```java @Override @Nullable public NamespaceHandler resolve(String namespaceUri) { // <1> 獲取所有已經配置的名稱空間與 NamespaceHandler 處理器的對映 Map handlerMappings = getHandlerMappings(); // <2> 根據 `namespaceUri` 名稱空間獲取 NamespaceHandler 處理器 Object handlerOrClassName = handlerMappings.get(namespaceUri); // <3> 接下來對 NamespaceHandler 進行初始化,因為定義在 `spring.handler` 檔案中,可能還沒有轉換成 Class 類物件 // <3.1> 不存在 if (handlerOrClassName == null) { return null; } // <3.2> 已經初始化 else if (handlerOrClassName instanceof NamespaceHandler) { return (NamespaceHandler) handlerOrClassName; } // <3.3> 需要進行初始化 else { String className = (String) handlerOrClassName; try { // 獲得類,並建立 NamespaceHandler 物件 Class handlerClass = ClassUtils.forName(className, this.classLoader); if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) { throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri + "] does not implement the [" + NamespaceHandler.class.getName() + "] interface"); } NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass); // 初始化 NamespaceHandler 物件 namespaceHandler.init(); // 新增到快取 handlerMappings.put(namespaceUri, namespaceHandler); return namespaceHandler; } catch (ClassNotFoundException ex) { throw new FatalBeanException("Could not find NamespaceHandler class [" + className + "] for namespace [" + namespaceUri + "]", ex); } catch (LinkageError err) { throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" + className + "] for namespace [" + namespaceUri + "]", err); } } } ``` 過程如下: 1. 獲取所有已經配置的名稱空間與 NamespaceHandler 處理器的對映,呼叫 `getHandlerMappings()` 方法 2. 根據 `namespaceUri` 名稱空間獲取 NamespaceHandler 處理器 3. 接下來對 NamespaceHandler 進行初始化,因為定義在 `spring.handler` 檔案中,可能還沒有轉換成 Class 類物件 1. 不存在則返回空物件 2. 否則,已經初始化則直接返回 3. 否則,根據 className 建立一個 Class 物件,然後進行例項化,還呼叫其 `init()` 方法 該方法可以找到名稱空間對應的 NamespaceHandler 處理器,關鍵在於第 `1` 步如何將 `spring.handlers` 檔案中的內容返回的 #### getHandlerMappings 方法 `getHandlerMappings()` 方法,從所有的 `META-INF/spring.handlers` 檔案中獲取名稱空間與處理器之間的對映,方法如下: ```java private Map getHandlerMappings() { // 雙重檢查鎖,延遲載入 Map handlerMappings = this.handlerMappings; if (handlerMappings == null) { synchronized (this) { handlerMappings = this.handlerMappings; if (handlerMappings == null) { if (logger.isTraceEnabled()) { logger.trace("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]"); } try { // 讀取 `handlerMappingsLocation`,也就是當前 JVM 環境下所有的 `META-INF/spring.handlers` 檔案的內容都會讀取到 Properties mappings = PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader); if (logger.isTraceEnabled()) { logger.trace("Loaded NamespaceHandler mappings: " + mappings); } // 初始化到 `handlerMappings` 中 handlerMappings = new ConcurrentHashMap<>(mappings.size()); CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings); this.handlerMappings = handlerMappings; } catch (IOException ex) { throw new IllegalStateException( "Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex); } } } } return handlerMappings; } ``` 邏輯不復雜,會讀取當前 JVM 環境下所有的 `META-INF/spring.handlers` 檔案,將裡面的內容以 key-value 的形式儲存在 Map 中返回 到這裡,對於 Spring XML 檔案中的自定義標籤的處理邏輯你是不是清晰了,接下來我們來看看 `
` 標籤的具體實現 ### ContextNamespaceHandler `org.springframework.context.config.ContextNamespaceHandler`,繼承 NamespaceHandlerSupport 抽象類,context 名稱空間(`http://www.springframework.org/schema/context`)的處理器,程式碼如下: ```java public class ContextNamespaceHandler extends NamespaceHandlerSupport { @Override public void init() { registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser()); registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser()); registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser()); registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser()); registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser()); registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser()); registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser()); registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser()); } } ``` init() 方法在 DefaultNamespaceHandlerResolver#resolve 方法中可以看到,初始化該物件的時候會被呼叫,註冊該名稱空間下各種標籤的解析器 #### registerBeanDefinitionParser 方法 `registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser)`,註冊標籤的解析器,方法如下: ```java // NamespaceHandlerSupport.java private final Map parsers = new HashMap<>(); protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) { this.parsers.put(elementName, parser); } ``` 將標籤名稱和對應的解析器儲存在 Map 中 #### parse 方法 `parse(Element element, ParserContext parserContext)` 方法,解析標籤節點,方法如下: ```java @Override @Nullable public BeanDefinition parse(Element element, ParserContext parserContext) { // <1> 獲得元素對應的 BeanDefinitionParser 物件 BeanDefinitionParser parser = findParserForElement(element, parserContext); // <2> 執行解析 return (parser != null ? parser.parse(element, parserContext) : null); } @Nullable private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) { // 獲得元素名 String localName = parserContext.getDelegate().getLocalName(element); // 獲得 BeanDefinitionParser 物件 BeanDefinitionParser parser = this.parsers.get(localName); if (parser == null) { parserContext.getReaderContext().fatal( "Cannot locate BeanDefinitionParser for element [" + localName + "]", element); } return parser; } ``` 邏輯很簡單,從 `Map parsers` 找到標籤物件的 BeanDefinitionParser 解析器,然後進行解析 ### ComponentScanBeanDefinitionParser `org.springframework.context.annotation.ComponentScanBeanDefinitionParser`,實現了 BeanDefinitionParser 介面,`` 標籤的解析器 #### parse 方法 `parse(Element element, ParserContext parserContext)` 方法,`` 標籤的解析過程,方法如下: ```java @Override @Nullable public BeanDefinition parse(Element element, ParserContext parserContext) { // <1> 獲取 `base-package` 屬性 String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE); // 處理佔位符 basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage); // 根據分隔符進行分割 String[] basePackages = StringUtils.tokenizeToStringArray(basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); // Actually scan for bean definitions and register them. // <2> 建立 ClassPathBeanDefinitionScanner 掃描器,用於掃描指定路徑下符合條件的 BeanDefinition 們 ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element); // <3> 通過掃描器掃描 `basePackages` 指定包路徑下的 BeanDefinition(帶有 @Component 註解或其派生註解的 Class 類),並註冊 Set beanDefinitions = scanner.doScan(basePackages); // <4> 將已註冊的 `beanDefinitions` 在當前 XMLReaderContext 上下文標記為已註冊,避免重複註冊 registerComponents(parserContext.getReaderContext(), beanDefinitions, element); return null; } ``` 過程如下: 1. 獲取 `base-package` 屬性,處理佔位符,根據分隔符進行分割 2. 建立 ClassPathBeanDefinitionScanner 掃描器,用於掃描指定路徑下符合條件的 BeanDefinition 們,呼叫 `configureScanner(ParserContext parserContext, Element element)` 方法 3. 通過掃描器掃描 `basePackages` 指定包路徑下的 BeanDefinition(帶有 @Component 註解或其派生註解的 Class 類),並**註冊** 4. 將已註冊的 `beanDefinitions` 在當前 XMLReaderContext 上下文標記為已註冊,避免重複註冊 上面的第 `3` 步的解析過程和本文的主題有點不符,過程也比較複雜,下一篇文章再進行分析 #### configureScanner 方法 `configureScanner(ParserContext parserContext, Element element)` 方法,建立 ClassPathBeanDefinitionScanner 掃描器,方法如下: ```java protected ClassPathBeanDefinitionScanner configureScanner(ParserContext parserContext, Element element) { // <1> 預設使用過濾器(過濾出 @Component 註解或其派生註解的 Class 類) boolean useDefaultFilters = true; if (element.hasAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)) { useDefaultFilters = Boolean.valueOf(element.getAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)); } // Delegate bean definition registration to scanner class. // <2> 建立 ClassPathBeanDefinitionScanner 掃描器 `scanner`,用於掃描指定路徑下符合條件的 BeanDefinition 們 ClassPathBeanDefinitionScanner scanner = createScanner(parserContext.getReaderContext(), useDefaultFilters); // <3> 設定生成的 BeanDefinition 物件的相關預設屬性 scanner.setBeanDefinitionDefaults(parserContext.getDelegate().getBeanDefinitionDefaults()); scanner.setAutowireCandidatePatterns(parserContext.getDelegate().getAutowireCandidatePatterns()); // <4> 根據標籤的屬性進行相關配置 // <4.1> `resource-pattern` 屬性的處理,設定資原始檔表示式,預設為 `**/*.class`,即 `classpath*:包路徑/**/*.class` if (element.hasAttribute(RESOURCE_PATTERN_ATTRIBUTE)) { scanner.setResourcePattern(element.getAttribute(RESOURCE_PATTERN_ATTRIBUTE)); } try { // <4.2> `name-generator` 屬性的處理,設定 Bean 的名稱生成器,預設為 AnnotationBeanNameGenerator parseBeanNameGenerator(element, scanner); } catch (Exception ex) { parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause()); } try { // <4.3> `scope-resolver`、`scoped-proxy` 屬性的處理,設定 Scope 的模式和元資訊處理器 parseScope(element, scanner); } catch (Exception ex) { parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause()); } // <4.4> `exclude-filter`、`include-filter` 屬性的處理,設定 `.class` 檔案的過濾器 parseTypeFilters(element, scanner, parserContext); // <5> 返回 `scanner` 掃描器 return scanner; } ``` 過程如下: 1. 預設使用過濾器(過濾出 @Component 註解或其派生註解的 Class 類) 2. 建立 ClassPathBeanDefinitionScanner 掃描器 `scanner`,用於掃描指定路徑下符合條件的 BeanDefinition 們 3. 設定生成的 BeanDefinition 物件的相關預設屬性 4. 根據標籤的屬性進行相關配置 1. `resource-pattern` 屬性的處理,設定資原始檔表示式,預設為 `**/*.class`,即 `classpath*:包路徑/**/*.class` 2. `name-generator` 屬性的處理,設定 Bean 的名稱生成器,預設為 AnnotationBeanNameGenerator 3. `scope-resolver`、`scoped-proxy` 屬性的處理,設定 Scope 的模式和元資訊處理器 4. `exclude-filter`、`include-filter` 屬性的處理,設定 `.class` 檔案的過濾器 5. 返回 `scanner` 掃描器 至此,對於 `` 標籤的解析過程已經分析完 ### spring.schemas 的原理 `META-INF/spring.handlers` 檔案的原理在 DefaultNamespaceHandlerResolver 中已經分析過,那麼 Sping 是如何處理 `META-INF/spring.schemas` 檔案的? 先回到 [**《BeanDefinition 的載入階段(XML 檔案)》**](https://www.cnblogs.com/lifullmoon/p/14437305.html) 中的 XmlBeanDefinitionReader#doLoadDocument 方法,如下: ```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 檔案的正確性 } protected EntityResolver getEntityResolver() { if (this.entityResolver == null) { // Determine default EntityResolver to use. ResourceLoader resourceLoader = getResourceLoader(); if (resourceLoader != null) { this.entityResolver = new ResourceEntityResolver(resourceLoader); } else { this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader()); } } return this.entityResolver; } ``` 第 `1` 步先獲取 `org.xml.sax.EntityResolver` 實體解析器,預設為 ResourceEntityResolver 資源解析器,根據 publicId 和 systemId 獲取對應的 DTD 或 XSD 檔案,用於對 XML 檔案進行驗證 #### ResourceEntityResolver `org.springframework.beans.factory.xml.ResourceEntityResolver`,XML 資源例項解析器,獲取對應的 DTD 或 XSD 檔案 ##### 建構函式 ```java public class ResourceEntityResolver extends DelegatingEntityResolver { /** 資源載入器 */ private final ResourceLoader resourceLoader; public ResourceEntityResolver(ResourceLoader resourceLoader) { super(resourceLoader.getClassLoader()); this.resourceLoader = resourceLoader; } } public class DelegatingEntityResolver implements EntityResolver { /** Suffix for DTD files. */ public static final String DTD_SUFFIX = ".dtd"; /** Suffix for schema definition files. */ public static final String XSD_SUFFIX = ".xsd"; private final EntityResolver dtdResolver; private final EntityResolver schemaResolver; public DelegatingEntityResolver(@Nullable ClassLoader classLoader) { this.dtdResolver = new BeansDtdResolver(); this.schemaResolver = new PluggableSchemaResolver(classLoader); } } ``` 注意 `schemaResolver` 為 XSD 的解析器,預設為 **PluggableSchemaResolver** 物件 ##### resolveEntity 方法 `resolveEntity(@Nullable String publicId, @Nullable String systemId)` 方法,獲取名稱空間對應的 DTD 或 XSD 檔案,方法如下: ```java // DelegatingEntityResolver.java @Override @Nullable public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws SAXException, IOException { if (systemId != null) { // DTD 模式 if (systemId.endsWith(DTD_SUFFIX)) { return this.dtdResolver.resolveEntity(publicId, systemId); } // XSD 模式 else if (systemId.endsWith(XSD_SUFFIX)) { return this.schemaResolver.resolveEntity(publicId, systemId); } } // Fall back to the parser's default behavior. return null; } // ResourceEntityResolver.java @Override @Nullable public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws SAXException, IOException { // <1> 呼叫父類的方法,進行解析,獲取本地 XSD 檔案資源 InputSource source = super.resolveEntity(publicId, systemId); // <2> 如果沒有獲取到本地 XSD 檔案資源,則嘗試通直接通過 systemId 獲取(網路形式) if (source == null && systemId != null) { // <2.1> 將 systemId 解析成一個 URL 地址 String resourcePath = null; try { String decodedSystemId = URLDecoder.decode(systemId, "UTF-8"); String givenUrl = new URL(decodedSystemId).toString(); // 解析檔案資源的相對路徑(相對於系統根路徑) String systemRootUrl = new File("").toURI().toURL().toString(); // Try relative to resource base if currently in system root. if (givenUrl.startsWith(systemRootUrl)) { resourcePath = givenUrl.substring(systemRootUrl.length()); } } catch (Exception ex) { // Typically a MalformedURLException or AccessControlException. if (logger.isDebugEnabled()) { logger.debug("Could not resolve XML entity [" + systemId + "] against system root URL", ex); } // No URL (or no resolvable URL) -> try relative to resource base. resourcePath = systemId; } // <2.2> 如果 URL 地址解析成功,則根據該地址獲取對應的 Resource 檔案資源 if (resourcePath != null) { if (logger.isTraceEnabled()) { logger.trace("Trying to locate XML entity [" + systemId + "] as resource [" + resourcePath + "]"); } // 獲得 Resource 資源 Resource resource = this.resourceLoader.getResource(resourcePath); // 建立 InputSource 物件 source = new InputSource(resource.getInputStream()); // 設定 publicId 和 systemId 屬性 source.setPublicId(publicId); source.setSystemId(systemId); if (logger.isDebugEnabled()) { logger.debug("Found XML entity [" + systemId + "]: " + resource); } } // <2.3> 否則,再次嘗試直接根據 systemId(如果是 "http" 則會替換成 "https")獲取 XSD 檔案(網路形式) else if (systemId.endsWith(DTD_SUFFIX) || systemId.endsWith(XSD_SUFFIX)) { // External dtd/xsd lookup via https even for canonical http declaration String url = systemId; if (url.startsWith("http:")) { url = "https:" + url.substring(5); } try { source = new InputSource(new URL(url).openStream()); source.setPublicId(publicId); source.setSystemId(systemId); } catch (IOException ex) { if (logger.isDebugEnabled()) { logger.debug("Could not resolve XML entity [" + systemId + "] through URL [" + url + "]", ex); } // Fall back to the parser's default behavior. source = null; } } } return source; } ``` 過程如下: 1. 呼叫父類的方法,進行解析,獲取**本地** XSD 檔案資源,如果是 XSD 模式,則先通過 **PluggableSchemaResolver** 解析 2. 如果沒有獲取到本地 XSD 檔案資源,則嘗試通直接通過 systemId 獲取(**網路**形式) 1. 將 systemId 解析成一個 URL 地址 2. 如果 URL 地址解析成功,則根據該地址獲取對應的 Resource 檔案資源 3. 否則,再次嘗試直接根據 systemId(如果是 "http" 則會替換成 "https")獲取 XSD 檔案(網路形式) 先嚐試獲取**本地**的 XSD 檔案,獲取不到再獲取**遠端**的 XSD 檔案 #### PluggableSchemaResolver `org.springframework.beans.factory.xml.PluggableSchemaResolver`,獲取 XSD 檔案(網路形式)對應的本地的檔案資源 ##### 建構函式 ```java public class PluggableSchemaResolver implements EntityResolver { public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas"; private static final Log logger = LogFactory.getLog(PluggableSchemaResolver.class); @Nullable private final ClassLoader classLoader; /** Schema 檔案地址 */ private final String schemaMappingsLocation; /** Stores the mapping of schema URL -> local schema path. */ @Nullable private volatile Map schemaMappings; public PluggableSchemaResolver(@Nullable ClassLoader classLoader) { this.classLoader = classLoader; this.schemaMappingsLocation = DEFAULT_SCHEMA_MAPPINGS_LOCATION; } } ``` 注意這裡的 `DEFAULT_SCHEMA_MAPPINGS_LOCATION` 為 `META-INF/spring.schemas`,看到這個可以確定實現原理就在這裡了 `schemaMappingsLocation` 屬性預設為 `META-INF/spring.schemas` ##### resolveEntity 方法 `resolveEntity(@Nullable String publicId, @Nullable String systemId)` 方法,獲取名稱空間對應的 DTD 或 XSD 檔案(本地),方法如下: ```java @Override @Nullable public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException { if (logger.isTraceEnabled()) { logger.trace("Trying to resolve XML entity with public id [" + publicId + "] and system id [" + systemId + "]"); } if (systemId != null) { // <1> 獲得對應的 XSD 檔案位置,從所有 `META-INF/spring.schemas` 檔案中獲取對應的本地 XSD 檔案位置 String resourceLocation = getSchemaMappings().get(systemId); if (resourceLocation == null && systemId.startsWith("https:")) { // Retrieve canonical http schema mapping even for https declaration resourceLocation = getSchemaMappings().get("http:" + systemId.substring(6)); } if (resourceLocation != null) { // 本地 XSD 檔案位置 // <2> 建立 ClassPathResource 物件 Resource resource = new ClassPathResource(resourceLocation, this.classLoader); try { // <3> 建立 InputSource 物件,設定 publicId、systemId 屬性,返回 InputSource source = new InputSource(resource.getInputStream()); source.setPublicId(publicId); source.setSystemId(systemId); if (logger.isTraceEnabled()) { logger.trace("Found XML schema [" + systemId + "] in classpath: " + resourceLocation); } return source; } catch (FileNotFoundException ex) { if (logger.isDebugEnabled()) { logger.debug("Could not find XML schema [" + systemId + "]: " + resource, ex); } } } } // Fall back to the parser's default behavior. return null; } ``` 過程如下: 1. 獲得對應的 XSD 檔案位置 `resourceLocation`,從所有 `META-INF/spring.schemas` 檔案中獲取對應的本地 XSD 檔案位置,會先呼叫 `getSchemaMappings()` 解析出本地所有的 XSD 檔案的位置資訊 2. 根據 `resourceLocation` 建立 ClassPathResource 物件 3. 建立 InputSource 物件,設定 publicId、systemId 屬性,返回 ##### getSchemaMappings 方法 `getSchemaMappings()`方法, 解析當前 JVM 環境下所有的 `META-INF/spring.handlers` 檔案的內容,方法如下: ```java private Map getSchemaMappings() { Map schemaMappings = this.schemaMappings; // 雙重檢查鎖,實現 schemaMappings 單例 if (schemaMappings == null) { synchronized (this) { schemaMappings = this.schemaMappings; if (schemaMappings == null) { if (logger.isTraceEnabled()) { logger.trace("Loading schema mappings from [" + this.schemaMappingsLocation + "]"); } try { // 讀取 `schemaMappingsLocation`,也就是當前 JVM 環境下所有的 `META-INF/spring.handlers` 檔案的內容都會讀取到 Properties mappings = PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader); if (logger.isTraceEnabled()) { logger.trace("Loaded schema mappings: " + mappings); } // 將 mappings 初始化到 schemaMappings 中 schemaMappings = new ConcurrentHashMap<>(mappings.size()); CollectionUtils.mergePropertiesIntoMap(mappings, schemaMappings); this.schemaMappings = schemaMappings; } catch (IOException ex) { throw new IllegalStateException( "Unable to load schema mappings from location [" + this.schemaMappingsLocation + "]", ex); } } } } return schemaMappings; } ``` 邏輯不復雜,會讀取當前 JVM 環境下所有的 `META-INF/spring.schemas` 檔案,將裡面的內容以 key-value 的形式儲存在 Map 中返回,例如儲存如下資訊: ```properties key=http://www.springframework.org/schema/context/spring-context.xsd value=org/springframework/context/config/spring-context.xsd ``` 這樣一來,會先獲取本地 `org/springframework/context/config/spring-context.xsd` 檔案,不存在則嘗試獲取 `http://www.springframework.org/schema/context/spring-context.xsd` 檔案,避免無網情況下無法獲取 XSD 檔案 ### 自定義標籤實現示例 例如我們有一個 User 例項類和一個 City 列舉: ```java package org.geekbang.thinking.in.spring.ioc.overview.domain; import org.geekbang.thinking.in.spring.ioc.overview.enums.City; public class User implements BeanNameAware { private Long id; private String name; private City city; // ... 省略 getter、setter 方法 } package org.geekbang.thinking.in.spring.ioc.overview.enums; public enum City { BEIJING, HANGZHOU, SHANGHAI } ``` #### 編寫 XML Schema 檔案(XSD 檔案) `org\geekbang\thinking\in\spring\configuration\metadata\users.xsd` ```xml-dtd ``` #### 自定義 NamespaceHandler 實現 ```java package org.geekbang.thinking.in.spring.configuration.metadata; import org.springframework.beans.factory.xml.NamespaceHandler; import org.springframework.beans.factory.xml.NamespaceHandlerSupport; public class UsersNamespaceHandler extends NamespaceHandlerSupport { @Override public void init() { // 將 "user" 元素註冊對應的 BeanDefinitionParser 實現 registerBeanDefinitionParser("user", new UserBeanDefinitionParser()); } } ``` #### 自定義 BeanDefinitionParser 實現 ```java package org.geekbang.thinking.in.spring.configuration.metadata; import org.geekbang.thinking.in.spring.ioc.overview.domain.User; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.util.StringUtils; import org.w3c.dom.Element; public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { @Override protected Class getBeanClass(Element element) { return User.class; } @Override protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { setPropertyValue("id", element, builder); setPropertyValue("name", element, builder); setPropertyValue("city", element, builder); } private void setPropertyValue(String attributeName, Element element, BeanDefinitionBuilder builder) { String attributeValue = element.getAttribute(attributeName); if (StringUtils.hasText(attributeValue)) { builder.addPropertyValue(attributeName, attributeValue); // -> } } } ``` #### 註冊 XML 擴充套件(spring.handlers 檔案) `META-INF/spring.handlers` ```properties ## 定義 namespace 與 NamespaceHandler 的對映 http\://time.geekbang.org/schema/users=org.geekbang.thinking.in.spring.configuration.metadata.UsersNamespaceHandler ``` #### 編寫 Spring Schema 資源對映檔案(spring.schemas 檔案) `META-INF/spring.schemas` ```properties http\://time.geekbang.org/schema/users.xsd = org/geekbang/thinking/in/spring/configuration/metadata/users.xsd ``` #### 使用示例 ```xml ``` 至此,通過使用 **users** 名稱空間下的 **user** 標籤也能定義一個 Bean Mybatis 對 Spring 的整合專案中的 `` 標籤就是這樣實現的,可以參考:[NamespaceHandler](https://github.com/mybatis/spring/blob/master/src/main/java/org/mybatis/spring/config/NamespaceHandler.java)、[MapperScannerBeanDefinitionParser](https://github.com/mybatis/spring/blob/master/src/main/java/org/mybatis/spring/config/MapperScannerBeanDefinitionParser.java)、[XSD 等檔案](https://github.com/mybatis/spring/tree/master/src/main/resources) ### 總結 Spring 預設名稱空間為 `http://www.springframework.org/schema/beans`,也就是 `` 標籤,解析過程在上一篇[**《BeanDefinition 的解析階段(XML 檔案)》**](https://www.cnblogs.com/lifullmoon/p/14439274.html)文章中已經分析過了。 非預設名稱空間的處理方式需要單獨的 NamespaceHandler 名稱空間處理器進行處理,這中方式屬於擴充套件 Spring XML 元素,也可以說是自定義標籤。在 Spring 內部很多地方都使用到這種方式。例如 ``、``、AOP 相關標籤都有對應的 NamespaceHandler 名稱空間處理器 對於這種**自定義** Spring XML 元素的實現步驟如下: 1. 編寫 XML Schema 檔案(XSD 檔案):定義 XML 結構 2. 自定義 NamespaceHandler 實現:定義名稱空間的處理器,實現 **NamespaceHandler** 介面,我們通常繼承 **NamespaceHandlerSupport** 抽象類,Spring 提供了通用實現,只需要實現其 init() 方法即可 3. 自定義 BeanDefinitionParser 實現:繫結名稱空間下不同的 XML 元素與其對應的解析器,因為一個名稱空間下可以有很多個標籤,對於不同的標籤需要不同的 **BeanDefinitionParser** 解析器,在上面的 init() 方法中進行繫結 4. 註冊 XML 擴充套件(`META-INF/spring.handlers` 檔案):名稱空間與名稱空間處理器的對映 5. XML Schema 檔案通常定義為網路的形式,在無網的情況下無法訪問,所以一般在本地的也有一個 XSD 檔案,可通過編寫 `META-INF/spring.schemas` 檔案,將網路形式的 XSD 檔案與本地的 XSD 檔案進行對映,這樣會**優先**從本地獲取對應的 XSD 檔案 關於上面的實現步驟的**原理**本文進行了比較詳細的分析,稍微總結一下: 1. Spring 會掃描到所有的 `META-INF/spring.schemas` 檔案內容,每個名稱空間對應的 XSD 檔案優先從本地獲取,用於 XML 檔案的校驗 2. Spring 會掃描到所有的 `META-INF/spring.handlers` 檔案內容,可以找到名稱空間對應的 NamespaceHandler 處理器 3. 根據找到的 NamespaceHandler 處理器找到標籤對應的 BeanDefinitionParser 解析器 4. 根據 BeanDefinitionParser 解析器解析該元素,生成對應的 BeanDefinition 並註冊 ------ 本文還分析了 `` 的實現原理,底層會 **ClassPathBeanDefinitionScanner** 掃描器,用於掃描指定路徑下符合條件的 BeanDefinition 們(帶有 @Component 註解或其派生註解的 Class 類)。@ComponentScan 註解底層原理也是基於 **ClassPathBeanDefinitionScanner** 掃描器實現的,這個掃描器和解析 @Component 註解定義的 Bean 相關。有關於**面向註解**定義的 Bean 在 Spring 中是如何解析成 BeanDefinition 在後續文章進行分析。 最後用一張圖來結束**面向資源(XML)**定義 Bean 的 BeanDefinition 的解析過程:

相關推薦

SpringIoC - 解析定義標籤XML 檔案

> 該系列文章是本人在學習 Spring 的過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 [Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-framework) 進行閱讀 > > Spring 版

SpringIoC - BeanDefinition 的載入階段XML 檔案

> 該系列文章是本人在學習 Spring 的過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 [Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-framework) 進行閱讀 > > Spring 版

SpringIoC - BeanDefinition 的解析階段XML 檔案

> 該系列文章是本人在學習 Spring 的過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 [Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-framework) 進行閱讀 > > Spring 版

SpringIoC - BeanDefinition 的解析過程面向註解

> 該系列文章是本人在學習 Spring 的過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 [Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-framework) 進行閱讀 > > Spring 版

SpringIoC - 深入瞭解Spring IoC面試題

> 該系列文章是本人在學習 Spring 的過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 [Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-framework) 進行閱讀 > > Spring 版

SpringIoC - 除錯環境的搭建

> 該系列文章是本人在學習 Spring 的過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 [Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-framework) 進行閱讀 > > Spring 版

SpringIoC - 開啟 Bean 的載入

> 該系列文章是本人在學習 Spring 的過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 [Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-framework) 進行閱讀 > > Spring 版

SpringIoC - 單例 Bean 的迴圈依賴處理

> 該系列文章是本人在學習 Spring 的過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 [Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-framework) 進行閱讀 > > Spring 版

SpringIoC - Bean 的屬性填充階段

> 該系列文章是本人在學習 Spring 的過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 [Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-framework) 進行閱讀 > > Spring 版

SpringIoC - @Autowired 等註解的實現原理

> 該系列文章是本人在學習 Spring 的過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 [Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-framework) 進行閱讀 > > Spring 版

SpringIoC - Spring 應用上下文 ApplicationContext

> 該系列文章是本人在學習 Spring 的過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 [Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-framework) 進行閱讀 > > Spring 版

SpringIoC - @Bean 等註解的實現原理

> 該系列文章是本人在學習 Spring 的過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 [Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-framework) 進行閱讀 > > Spring 版

Spring】—– IOC 解析Bean:解析 import 標籤

在部落格【死磕Spring】----- IOC 之 註冊 BeanDefinition中分析到,Spring 中有兩種解析 Bean 的方式。如果根節點或者子節點採用預設名稱空間的話,則呼叫 parseDefaultElement() 進行預設標籤解析,否則呼叫 delegate.parseCustomEl

Spring】----- IOC 解析 bean 標籤:開啟解析程序

import 標籤解析完畢了,再看 Spring 中最複雜也是最重要的標籤 bean 標籤的解析過程。 在方法 parseDefaultElement() 中,如果遇到標籤 為 bean 則呼叫 processBeanDefinition() 方法進行 bean 標籤解析,如下: protected

Spring】----- IOC 註冊解析的 BeanDefinition

DefaultBeanDefinitionDocumentReader.processBeanDefinition() 完成 Bean 標籤解析的核心工作,如下: protected void processBeanDefinition(Element el

Spring】—– IOC 解析 bean 標籤:constructor-arg、property 子元素

上篇部落格(【死磕 Spring】—– IOC 之解析 bean 標籤:meta、lookup-method、replace-method)分析了 meta 、 lookup-method、replace-method 三個子元素,這篇部落格分析 constr

Spring】----- IOC 獲取驗證模型

close 步驟 call buffere n) 規範 frame create ring 原文出自:http://cmsblogs.com 在上篇博客【死磕Spring】----- IOC 之 加載 Bean 中提到,在核心邏輯方法 doLoadBeanDefinit

Spring】----- IOC 深入理解 Spring IoC

在一開始學習 Spring 的時候,我們就接觸 IoC 了,作為 Spring 第一個最核心的概念,我們在解讀它原始碼之前一定需要對其有深入的認識,本篇為【死磕 Spring】系列部落格的第一篇博文,主要介紹 IoC 基本概念和各個元件。 IOC 理論 Io

Spring】----- IOC 獲取 Document 物件

在 XmlBeanDefinitionReader.doLoadDocument() 方法中做了兩件事情,一是呼叫 getValidationModeForResource() 獲取 XML 的驗證模式,二是呼叫 DocumentLoader.loadDocument() 獲取 Document 物件。上篇

Spring】----- IOC 註冊 BeanDefinition

獲取 Document 物件後,會根據該物件和 Resource 資源物件呼叫 registerBeanDefinitions() 方法,開始註冊 BeanDefinitions 之旅。如下: pu