該系列文章是本人在學習 Spring 的過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 Spring 原始碼分析 GitHub 地址 進行閱讀
Spring 版本:5.1.14.RELEASE
開始閱讀這一系列文章之前,建議先檢視《深入瞭解 Spring IoC(面試題)》這一篇文章
該系列其他文章請檢視:《死磕 Spring 之 IoC 篇 - 文章導讀》
解析自定義標籤(XML 檔案)
上一篇《BeanDefinition 的解析階段(XML 檔案)》文章分析了 Spring 處理 org.w3c.dom.Document
物件(XML Document)的過程,會解析裡面的元素。預設名稱空間(為空或者 http://www.springframework.org/schema/beans
)的元素,例如 <bean />
標籤會被解析成 GenericBeanDefinition 物件並註冊。本文會分析 Spring 是如何處理非預設名稱空間的元素,通過 Spring 的實現方式我們如何自定義元素
先來了解一下 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>
上述 XML 檔案 <beans />
的預設名稱空間為 http://www.springframework.org/schema/beans
,內部的 <bean />
標籤沒有定義名稱空間,則使用預設名稱空間
<beans />
還定義了 context 名稱空間為 http://www.springframework.org/schema/context
,那麼內部的 <context:component-scan />
標籤就不是預設名稱空間,處理方式也不同。其實 Spring 內部自定義了很多的名稱空間,用於處理不同的場景,原理都一樣,接下來會進行分析。
自定義標籤的實現步驟
擴充套件 Spring XML 元素的步驟如下:
-
編寫 XML Schema 檔案(XSD 檔案):定義 XML 結構
-
自定義 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 內部自定義標籤預覽
在 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 解析器,這裡先看一下,後面再具體分析
Spring 如何處理非預設名稱空間的元素
回顧到 《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
名稱空間 - 通過 DefaultNamespaceHandlerResolver 根據
namespaceUri
獲取相應的 NamespaceHandler 處理器 - 根據 NamespaceHandler 名稱空間處理器處理該標籤
關鍵就在與 DefaultNamespaceHandlerResolver 是如何找到該名稱空間對應的 NamespaceHandler 處理器,我們只是在 spring.handlers
檔案中進行關聯,它是怎麼找到的呢,我們進入 DefaultNamespaceHandlerResolver 看看
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 方法
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);
}
}
}
過程如下:
- 獲取所有已經配置的名稱空間與 NamespaceHandler 處理器的對映,呼叫
getHandlerMappings()
方法 - 根據
namespaceUri
名稱空間獲取 NamespaceHandler 處理器 - 接下來對 NamespaceHandler 進行初始化,因為定義在
spring.handler
檔案中,可能還沒有轉換成 Class 類物件- 不存在則返回空物件
- 否則,已經初始化則直接返回
- 否則,根據 className 建立一個 Class 物件,然後進行例項化,還呼叫其
init()
方法
該方法可以找到名稱空間對應的 NamespaceHandler 處理器,關鍵在於第 1
步如何將 spring.handlers
檔案中的內容返回的
getHandlerMappings 方法
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 />
標籤的具體實現
ContextNamespaceHandler
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 方法
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 方法
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 解析器,然後進行解析
ComponentScanBeanDefinitionParser
org.springframework.context.annotation.ComponentScanBeanDefinitionParser
,實現了 BeanDefinitionParser 介面,<context:component-scan />
標籤的解析器
parse 方法
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
屬性,處理佔位符,根據分隔符進行分割 - 建立 ClassPathBeanDefinitionScanner 掃描器,用於掃描指定路徑下符合條件的 BeanDefinition 們,呼叫
configureScanner(ParserContext parserContext, Element element)
方法 - 通過掃描器掃描
basePackages
指定包路徑下的 BeanDefinition(帶有 @Component 註解或其派生註解的 Class 類),並註冊 - 將已註冊的
beanDefinitions
在當前 XMLReaderContext 上下文標記為已註冊,避免重複註冊
上面的第 3
步的解析過程和本文的主題有點不符,過程也比較複雜,下一篇文章再進行分析
configureScanner 方法
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;
}
過程如下:
- 預設使用過濾器(過濾出 @Component 註解或其派生註解的 Class 類)
- 建立 ClassPathBeanDefinitionScanner 掃描器
scanner
,用於掃描指定路徑下符合條件的 BeanDefinition 們 - 設定生成的 BeanDefinition 物件的相關預設屬性
- 根據標籤的屬性進行相關配置
resource-pattern
屬性的處理,設定資原始檔表示式,預設為**/*.class
,即classpath*:包路徑/**/*.class
name-generator
屬性的處理,設定 Bean 的名稱生成器,預設為 AnnotationBeanNameGeneratorscope-resolver
、scoped-proxy
屬性的處理,設定 Scope 的模式和元資訊處理器exclude-filter
、include-filter
屬性的處理,設定.class
檔案的過濾器
- 返回
scanner
掃描器
至此,對於 <context:component-scan />
標籤的解析過程已經分析完
spring.schemas 的原理
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 檔案進行驗證
ResourceEntityResolver
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 方法
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 模式,則先通過 PluggableSchemaResolver 解析
- 如果沒有獲取到本地 XSD 檔案資源,則嘗試通直接通過 systemId 獲取(網路形式)
- 將 systemId 解析成一個 URL 地址
- 如果 URL 地址解析成功,則根據該地址獲取對應的 Resource 檔案資源
- 否則,再次嘗試直接根據 systemId(如果是 "http" 則會替換成 "https")獲取 XSD 檔案(網路形式)
先嚐試獲取本地的 XSD 檔案,獲取不到再獲取遠端的 XSD 檔案
PluggableSchemaResolver
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 方法
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;
}
過程如下:
- 獲得對應的 XSD 檔案位置
resourceLocation
,從所有META-INF/spring.schemas
檔案中獲取對應的本地 XSD 檔案位置,會先呼叫getSchemaMappings()
解析出本地所有的 XSD 檔案的位置資訊 - 根據
resourceLocation
建立 ClassPathResource 物件 - 建立 InputSource 物件,設定 publicId、systemId 屬性,返回
getSchemaMappings 方法
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
}
編寫 XML Schema 檔案(XSD 檔案)
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>
自定義 NamespaceHandler 實現
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 實現
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=""/>
}
}
}
註冊 XML 擴充套件(spring.handlers 檔案)
META-INF/spring.handlers
## 定義 namespace 與 NamespaceHandler 的對映
http\://time.geekbang.org/schema/users=org.geekbang.thinking.in.spring.configuration.metadata.UsersNamespaceHandler
編寫 Spring Schema 資源對映檔案(spring.schemas 檔案)
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 檔案
關於上面的實現步驟的原理本文進行了比較詳細的分析,稍微總結一下:
- Spring 會掃描到所有的
META-INF/spring.schemas
檔案內容,每個名稱空間對應的 XSD 檔案優先從本地獲取,用於 XML 檔案的校驗 - Spring 會掃描到所有的
META-INF/spring.handlers
檔案內容,可以找到名稱空間對應的 NamespaceHandler 處理器 - 根據找到的 NamespaceHandler 處理器找到標籤對應的 BeanDefinitionParser 解析器
- 根據 BeanDefinitionParser 解析器解析該元素,生成對應的 BeanDefinition 並註冊
本文還分析了 <context:component-scan />
的實現原理,底層會 ClassPathBeanDefinitionScanner 掃描器,用於掃描指定路徑下符合條件的 BeanDefinition 們(帶有 @Component 註解或其派生註解的 Class 類)。@ComponentScan 註解底層原理也是基於 ClassPathBeanDefinitionScanner 掃描器實現的,這個掃描器和解析 @Component 註解定義的 Bean 相關。有關於面向註解定義的 Bean 在 Spring 中是如何解析成 BeanDefinition 在後續文章進行分析。
最後用一張圖來結束面向資源(XML)定義 Bean 的 BeanDefinition 的解析過程: