該系列文章是本人在學習 Spring 的過程當中總結下來的,裏面涉及到相關源碼,可能對讀者不太友好,請結合個人源碼註釋 Spring 源碼分析 GitHub 地址 進行閱讀html
Spring 版本:5.1.14.RELEASEjava
開始閱讀這一系列文章以前,建議先查看《深刻了解 Spring IoC(面試題)》這一篇文章git
該系列其餘文章請查看:《死磕 Spring 之 IoC 篇 - 文章導讀》github
上一篇《BeanDefinition 的解析階段(XML 文件)》文章分析了 Spring 處理 org.w3c.dom.Document
對象(XML Document)的過程,會解析裏面的元素。默認命名空間(爲空或者 http://www.springframework.org/schema/beans
)的元素,例如 <bean />
標籤會被解析成 GenericBeanDefinition 對象並註冊。本文會分析 Spring 是如何處理非默認命名空間的元素,經過 Spring 的實現方式咱們如何自定義元素面試
先來了解一下 XML 文件中的命名空間:spring
<?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>
上述 XML 文件 <beans />
的默認命名空間爲 http://www.springframework.org/schema/beans
,內部的 <bean />
標籤沒有定義命名空間,則使用默認命名空間緩存
<beans />
還定義了 context 命名空間爲 http://www.springframework.org/schema/context
,那麼內部的 <context:component-scan />
標籤就不是默認命名空間,處理方式也不一樣。其實 Spring 內部自定義了不少的命名空間,用於處理不一樣的場景,原理都同樣,接下來會進行分析。網絡
擴展 Spring XML 元素的步驟以下:mybatis
編寫 XML Schema 文件(XSD 文件):定義 XML 結構app
自定義 NamespaceHandler 實現:定義命名空間的處理器,實現 NamespaceHandler 接口,咱們一般繼承 NamespaceHandlerSupport 抽象類,Spring 提供了通用實現,只須要實現其 init() 方法便可
自定義 BeanDefinitionParser 實現:綁定命名空間下不一樣的 XML 元素與其對應的解析器,由於一個命名空間下能夠有不少個標籤,對於不一樣的標籤須要不一樣的 BeanDefinitionParser 解析器,在上面的 init() 方法中進行綁定
註冊 XML 擴展(META-INF/spring.handlers
文件):命名空間與命名空間處理器的映射
編寫 Spring Schema 資源映射文件(META-INF/spring.schemas
文件):XML Schema 文件一般定義爲網絡的形式,在無網的狀況下沒法訪問,因此通常在本地的也有一個 XSD 文件,可經過編寫 spring.schemas
文件,將網絡形式的 XSD 文件與本地的 XSD 文件進行映射,這樣會優先從本地獲取對應的 XSD 文件
在 spring-context
模塊的 ClassPath 下能夠看到有 META-INF/spring.handlers
、META-INF/spring.schemas
以及對應的 XSD 文件,以下:
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
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 處理器,先來看一下:
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 解析器,這裏先看一下,後面再具體分析
回顧到 《BeanDefinition 的加載階段(XML 文件)》 文章中的 XmlBeanDefinitionReader#registerBeanDefinitions 方法,解析 Document 前會先建立 XmlReaderContext 對象(讀取 Resource 資源的上下文對象),建立方法以下:
// 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 文件)》 文章中的 DefaultBeanDefinitionDocumentReader#parseBeanDefinitions 方法,若是不是默認的命名空間,則執行自定義解析,調用 BeanDefinitionParserDelegate#parseCustomElement(Element ele)
方法,方法以下
// 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)); }
過程以下:
namespaceUri
命名空間namespaceUri
獲取相應的 NamespaceHandler 處理器關鍵就在與 DefaultNamespaceHandlerResolver 是如何找到該命名空間對應的 NamespaceHandler 處理器,咱們只是在 spring.handlers
文件中進行關聯,它是怎麼找到的呢,咱們進入 DefaultNamespaceHandlerResolver 看看
org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver
,命名空間的默認處理器
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<String, Object> 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(String namespaceUri)
方法,根據命名空間找到對應的 NamespaceHandler 處理器,方法以下:
@Override @Nullable public NamespaceHandler resolve(String namespaceUri) { // <1> 獲取全部已經配置的命名空間與 NamespaceHandler 處理器的映射 Map<String, Object> 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); } } }
過程以下:
getHandlerMappings()
方法namespaceUri
命名空間獲取 NamespaceHandler 處理器spring.handler
文件中,可能尚未轉換成 Class 類對象
init()
方法該方法能夠找到命名空間對應的 NamespaceHandler 處理器,關鍵在於第 1
步如何將 spring.handlers
文件中的內容返回的
getHandlerMappings()
方法,從全部的 META-INF/spring.handlers
文件中獲取命名空間與處理器之間的映射,方法以下:
private Map<String, Object> getHandlerMappings() { // 雙重檢查鎖,延遲加載 Map<String, Object> 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 文件中的自定義標籤的處理邏輯你是否是清晰了,接下來咱們來看看 <context:component-scan />
標籤的具體實現
org.springframework.context.config.ContextNamespaceHandler
,繼承 NamespaceHandlerSupport 抽象類,context 命名空間(http://www.springframework.org/schema/context
)的處理器,代碼以下:
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(String elementName, BeanDefinitionParser parser)
,註冊標籤的解析器,方法以下:
// NamespaceHandlerSupport.java private final Map<String, BeanDefinitionParser> parsers = new HashMap<>(); protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) { this.parsers.put(elementName, parser); }
將標籤名稱和對應的解析器保存在 Map 中
parse(Element element, ParserContext parserContext)
方法,解析標籤節點,方法以下:
@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<String, BeanDefinitionParser> parsers
找到標籤對象的 BeanDefinitionParser 解析器,而後進行解析
org.springframework.context.annotation.ComponentScanBeanDefinitionParser
,實現了 BeanDefinitionParser 接口,<context:component-scan />
標籤的解析器
parse(Element element, ParserContext parserContext)
方法,<context:component-scan />
標籤的解析過程,方法以下:
@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<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages); // <4> 將已註冊的 `beanDefinitions` 在當前 XMLReaderContext 上下文標記爲已註冊,避免重複註冊 registerComponents(parserContext.getReaderContext(), beanDefinitions, element); return null; }
過程以下:
base-package
屬性,處理佔位符,根據分隔符進行分割configureScanner(ParserContext parserContext, Element element)
方法basePackages
指定包路徑下的 BeanDefinition(帶有 @Component 註解或其派生註解的 Class 類),並註冊beanDefinitions
在當前 XMLReaderContext 上下文標記爲已註冊,避免重複註冊上面的第 3
步的解析過程和本文的主題有點不符,過程也比較複雜,下一篇文章再進行分析
configureScanner(ParserContext parserContext, Element element)
方法,建立 ClassPathBeanDefinitionScanner 掃描器,方法以下:
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; }
過程以下:
scanner
,用於掃描指定路徑下符合條件的 BeanDefinition 們resource-pattern
屬性的處理,設置資源文件表達式,默認爲 **/*.class
,即 classpath*:包路徑/**/*.class
name-generator
屬性的處理,設置 Bean 的名稱生成器,默認爲 AnnotationBeanNameGeneratorscope-resolver
、scoped-proxy
屬性的處理,設置 Scope 的模式和元信息處理器exclude-filter
、include-filter
屬性的處理,設置 .class
文件的過濾器scanner
掃描器至此,對於 <context:component-scan />
標籤的解析過程已經分析完
META-INF/spring.handlers
文件的原理在 DefaultNamespaceHandlerResolver 中已經分析過,那麼 Sping 是如何處理 META-INF/spring.schemas
文件的?
先回到 《BeanDefinition 的加載階段(XML 文件)》 中的 XmlBeanDefinitionReader#doLoadDocument 方法,以下:
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 文件進行驗證
org.springframework.beans.factory.xml.ResourceEntityResolver
,XML 資源實例解析器,獲取對應的 DTD 或 XSD 文件
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(@Nullable String publicId, @Nullable String systemId)
方法,獲取命名空間對應的 DTD 或 XSD 文件,方法以下:
// 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; }
過程以下:
先嚐試獲取本地的 XSD 文件,獲取不到再獲取遠程的 XSD 文件
org.springframework.beans.factory.xml.PluggableSchemaResolver
,獲取 XSD 文件(網絡形式)對應的本地的文件資源
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<String, String> 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(@Nullable String publicId, @Nullable String systemId)
方法,獲取命名空間對應的 DTD 或 XSD 文件(本地),方法以下:
@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; }
過程以下:
resourceLocation
,從全部 META-INF/spring.schemas
文件中獲取對應的本地 XSD 文件位置,會先調用 getSchemaMappings()
解析出本地全部的 XSD 文件的位置信息resourceLocation
建立 ClassPathResource 對象getSchemaMappings()
方法, 解析當前 JVM 環境下全部的 META-INF/spring.handlers
文件的內容,方法以下:
private Map<String, String> getSchemaMappings() { Map<String, String> 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 中返回,例如保存以下信息:
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 枚舉:
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 }
org\geekbang\thinking\in\spring\configuration\metadata\users.xsd
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <xsd:schema xmlns="http://time.geekbang.org/schema/users" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://time.geekbang.org/schema/users"> <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/> <!-- 定義 User 類型(複雜類型) --> <xsd:complexType name="User"> <xsd:attribute name="id" type="xsd:long" use="required"/> <xsd:attribute name="name" type="xsd:string" use="required"/> <xsd:attribute name="city" type="City"/> </xsd:complexType> <!-- 定義 City 類型(簡單類型,枚舉) --> <xsd:simpleType name="City"> <xsd:restriction base="xsd:string"> <xsd:enumeration value="BEIJING"/> <xsd:enumeration value="HANGZHOU"/> <xsd:enumeration value="SHANGHAI"/> </xsd:restriction> </xsd:simpleType> <!-- 定義 user 元素 --> <xsd:element name="user" type="User"/> </xsd:schema>
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()); } }
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); // -> <property name="" value=""/> } } }
META-INF/spring.handlers
## 定義 namespace 與 NamespaceHandler 的映射 http\://time.geekbang.org/schema/users=org.geekbang.thinking.in.spring.configuration.metadata.UsersNamespaceHandler
META-INF/spring.schemas
http\://time.geekbang.org/schema/users.xsd = org/geekbang/thinking/in/spring/configuration/metadata/users.xsd
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:users="http://time.geekbang.org/schema/users" 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://time.geekbang.org/schema/users http://time.geekbang.org/schema/users.xsd"> <!-- <bean id="user" class="org.geekbang.thinking.in.spring.ioc.overview.domain.User"> <property name="id" value="1"/> <property name="name" value="小馬哥"/> <property name="city" value="HANGZHOU"/> </bean> --> <users:user id="1" name="小馬哥" city="HANGZHOU"/> </beans>
至此,經過使用 users 命名空間下的 user 標籤也能定義一個 Bean
Mybatis 對 Spring 的集成項目中的 <mybatis:scan />
標籤就是這樣實現的,能夠參考:NamespaceHandler、MapperScannerBeanDefinitionParser、XSD 等文件
Spring 默認命名空間爲 http://www.springframework.org/schema/beans
,也就是 <bean />
標籤,解析過程在上一篇《BeanDefinition 的解析階段(XML 文件)》文章中已經分析過了。
非默認命名空間的處理方式須要單獨的 NamespaceHandler 命名空間處理器進行處理,這中方式屬於擴展 Spring XML 元素,也能夠說是自定義標籤。在 Spring 內部不少地方都使用到這種方式。例如 <context:component-scan />
、<util:list />
、AOP 相關標籤都有對應的 NamespaceHandler 命名空間處理器
對於這種自定義 Spring XML 元素的實現步驟以下:
編寫 XML Schema 文件(XSD 文件):定義 XML 結構
自定義 NamespaceHandler 實現:定義命名空間的處理器,實現 NamespaceHandler 接口,咱們一般繼承 NamespaceHandlerSupport 抽象類,Spring 提供了通用實現,只須要實現其 init() 方法便可
自定義 BeanDefinitionParser 實現:綁定命名空間下不一樣的 XML 元素與其對應的解析器,由於一個命名空間下能夠有不少個標籤,對於不一樣的標籤須要不一樣的 BeanDefinitionParser 解析器,在上面的 init() 方法中進行綁定
註冊 XML 擴展(META-INF/spring.handlers
文件):命名空間與命名空間處理器的映射
XML Schema 文件一般定義爲網絡的形式,在無網的狀況下沒法訪問,因此通常在本地的也有一個 XSD 文件,可經過編寫 META-INF/spring.schemas
文件,將網絡形式的 XSD 文件與本地的 XSD 文件進行映射,這樣會優先從本地獲取對應的 XSD 文件
關於上面的實現步驟的原理本文進行了比較詳細的分析,稍微總結一下:
META-INF/spring.schemas
文件內容,每一個命名空間對應的 XSD 文件優先從本地獲取,用於 XML 文件的校驗META-INF/spring.handlers
文件內容,能夠找到命名空間對應的 NamespaceHandler 處理器本文還分析了 <context:component-scan />
的實現原理,底層會 ClassPathBeanDefinitionScanner 掃描器,用於掃描指定路徑下符合條件的 BeanDefinition 們(帶有 @Component 註解或其派生註解的 Class 類)。@ComponentScan 註解底層原理也是基於 ClassPathBeanDefinitionScanner 掃描器實現的,這個掃描器和解析 @Component 註解定義的 Bean 相關。有關於面向註解定義的 Bean 在 Spring 中是如何解析成 BeanDefinition 在後續文章進行分析。
最後用一張圖來結束面向資源(XML)定義 Bean 的 BeanDefinition 的解析過程: