死磕Spring之IoC篇 - 解析自定义标签(XML 文件)
该系列文章是本人在学习 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 的解析过程: