該系列文章是本人在學習 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" >
<property name="id" value="https://www.cnblogs.com/lifullmoon/p/1"/>
<property name="name" value="https://www.cnblogs.com/lifullmoon/p/小馬哥"/>
</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 檔案,如下:
-
META-INF/spring.handlers
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
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*:包路徑/**/*.classname-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=https://www.cnblogs.com/lifullmoon/p/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="https://www.cnblogs.com/lifullmoon/p/BEIJING"/>
<xsd:enumeration value="https://www.cnblogs.com/lifullmoon/p/HANGZHOU"/>
<xsd:enumeration value="https://www.cnblogs.com/lifullmoon/p/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 = https://www.cnblogs.com/lifullmoon/p/element.getAttribute(attributeName);
if (StringUtils.hasText(attributeValue)) {
builder.addPropertyValue(attributeName, attributeValue); // ->
注冊 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" >
<property name="id" value="https://www.cnblogs.com/lifullmoon/p/1"/>
<property name="name" value="https://www.cnblogs.com/lifullmoon/p/小馬哥"/>
<property name="city" value="https://www.cnblogs.com/lifullmoon/p/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 的決議程序:
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/263647.html
標籤:Java
上一篇:mybatis(1)
下一篇:HTTP常用請求頭大揭秘
