这是我参与「掘金日新计划 · 8 月更文挑战」的第24天,点击查看活动详情
基于 Spring Framework v5.2.6.RELEASE
前情提要
上一篇介绍了 BeanDefinition 加载过程准备阶段所做的工作,这一篇探索后续的流程。在准备阶段结束之后,就进入了XmlBeanDefinitionReader
类的doLoadBeanDefinitions
方法,其中有两句关键的方法调用:
Document doc = doLoadDocument(inputSource, resource);
int count = registerBeanDefinitions(doc, resource);
复制代码
从方法的名称可以看出,第一个方法调用是为了加载配置资源,最终得到一个 Document 对象,第二个方法调用是将 BeanDefinition 注册到容器中。
本文先分析资源加载的部分。
XML 资源解析
首先,进入到doLoadDocument
方法:
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
复制代码
这里调用了成员变量documentLoader
的loadDocument
方法,为了便于之后的分析,先看一下documentLoader
是什么。在成员变量的定义中可以找到:
private DocumentLoader documentLoader = new DefaultDocumentLoader();
复制代码
这里直接通过构造方法创建了一个DefaultDocumentLoader
,在它的类定义中,并未包含构造方法的定义,因此我们可以认为这里除了创建这个对象什么也没有做。它实现了DocumentLoader
,从名字可以看出这是一个 XML 文档的加载器。
接下来,找到DefaultDocumentLoader
中的loadDocument
方法:
@Override
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
if (logger.isTraceEnabled()) {
logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
}
DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
return builder.parse(inputSource);
}
复制代码
这里把资源输入流加载成了 Document 对象。将 XML 文件解析成 Document 对象的过程,只是一个 XML 文件解析过程,不在本文的讨论范畴,这里就不详细介绍了。
接下来再来分析loadDocument
方法中的几个方法参数,从方法体中的代码看出,这几个参数传入的具体信息会影响 XML 解析的配置,其中,前三个参数的类型都属于org.xml.sax
包,并不是 Spring 的一部分。
DocumentLoader#loadDocument
方法的参数
inputSource
这里的inpustSource
是对资源的输入流inputStream
的封装,它其实就代表了要被解析的 XML 的输入流。除了输入流、编码、字符集以外,它还包含了两个成员变量:publicId
和systemId
,这两个都是 XML 文档的参数,这里的inpustSource
在创建的时候,并没有给这两个成员变量赋值,所以我们先跳过,之后遇到了再做详细介绍。
entityResolver
在loadDocument
方法中有一个EntityResolver
类型的参数,EntityResolver
是一个接口,想要知道这里调用方法的时候具体传入了一个什么样的对象,我们需要找到XmlBeanDefinitionReader
调用方法时获取参数的 getEntityResolver()
方法:
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;
}
复制代码
方法体中获取resourceLoader
的方法getResourceLoader()
其实就是读取了当前对象的resourceLoader
成员变量。在之前的源码阅读(Spring 源码阅读 04:BeanFactory 初始化 )中,我们已经知道,这里的XmlBeanDefinitionReader
在创建的时候,就给它设置了resourceLoader
的值,因此,以上代码中if
语句块会进入第一个条件中,创建一个ResourceEntityResolver
类型的 EntityResolver 对象。
继续深入,找到创建ResourceEntityResolver
的构造方法:
public ResourceEntityResolver(ResourceLoader resourceLoader) {
super(resourceLoader.getClassLoader());
this.resourceLoader = resourceLoader;
}
复制代码
这里调用了父类的构造方法,并且设置了自己的resourceLoader
成员变量的值。下面来到它的父类DelegatingEntityResolver
中,查看对应的构造方法:
public DelegatingEntityResolver(@Nullable ClassLoader classLoader) {
this.dtdResolver = new BeansDtdResolver();
this.schemaResolver = new PluggableSchemaResolver(classLoader);
}
复制代码
从它的名称和构造方法体来看,它是一个代理类型,并且持有了BeansDtdResolver和PluggableSchemaResolver两个类型的EntityResolver
。没错,这两个也是EntityResolver
的实现类,这几个类的关系是这样的:
说了这么多,这个EntityResolver
是用来干什么的呢?
Spring 的 XML 配置文件,对内容和格式都有严格的约束,如果配置文件不符合这些约束要求,就会导致 Spring 初始化失败,因此,在初始化之前,Spring 需要对这些文件进行校验。
XML 的约束文件通常声明在文件中,比如下面的配置文件内容:
跟节点 beans 中这些 URL 就是约束文件的地址。比如其中的spring-beans.xsd
,这里的.xsd
后缀表示它是一个 XSD 文件,XSD 是 XML Schemas Definition 的简写,也就是用来定义 XML 模式的。
除了 XSD 之外,Spring 还支持 DTD(Document Type Definition,文档类型定义)类型的约束文件。
默认情况下,在需要校验 XML 文件的时候,会根据这个路径,下载对应的约束文件,来对 XML 配置进行校验。这种情况下,并不需要 EntityResolver 的参与。
但是这里有一个问题,如果我们的程序运行在离线环境或者网络受限的环境中,如何对 XML 文件进行校验呢?解决办法就是把这些文件直接继承在 Spring 项目当中。在 Spring 的工程中,可以找到这些文件,比如下面这几个:
那如何让程序找到 Spring 工程中的约束文件,并使用这些文件对相应的 XML 文件进行校验呢?这便是 EntityResolver 的作用,通过实现EntityResolver
接口,可以自定义约束文件获取的逻辑。
在上面的代码中可以发现,实际完成这项工作的就是getEntityResolver()
方法中创建的DelegatingEntityResolver
,它会把具体的任务委托给它持有的两个 EntityResolver 成员变量,分别是BeansDtdResolver
和PluggableSchemaResolver
类型,它们分别负责 DTD 格式的约束文件和 XSD 格式的约束文件的解析。
因此,有了这几个 Spring 定义的EntityResolver
之后,就可以让 XML 解析器在获取约束文件的时候,用 Spring 中的离线文件代替需要下载的在线文件。
EntityResolver
具体是如何处理约束文件的,不属于本篇所讨论的流程,这里挖个坑,之后单独开一篇来写,写完之后会把链接贴到这里:
errorHandler
接下来在看errorHandler
找个参数,从名字就可以看出来,它是一个错误处理器,负责处理 XML 解析过程中出现的异常情况。在 XmlBeanDefinitionReader 中调用this.documentLoader.loadDocument
方法的时候,这个参数直接传入了this.errorHandler
。我们查看这个成员变量:
private ErrorHandler errorHandler = new SimpleSaxErrorHandler(logger);
复制代码
它是在 XmlBeanDefinitionReader 中直接初始化好的一个 SimpleSaxErrorHandler 类型的对象,并把logger
作为参数传递了进去。在查看一下 SimpleSaxErrorHandler 的源码:
public class SimpleSaxErrorHandler implements ErrorHandler {
private final Log logger;
/**
* Create a new SimpleSaxErrorHandler for the given
* Commons Logging logger instance.
*/
public SimpleSaxErrorHandler(Log logger) {
this.logger = logger;
}
@Override
public void warning(SAXParseException ex) throws SAXException {
logger.warn("Ignored XML validation warning", ex);
}
@Override
public void error(SAXParseException ex) throws SAXException {
throw ex;
}
@Override
public void fatalError(SAXParseException ex) throws SAXException {
throw ex;
}
}
复制代码
其实这里什么都没有处理,只是在需要警告提示的时候写入了一条日志,其他的异常直接抛出了。这里应该也是留给其他的实现类扩展用的。
validationMode
我们之前说到,在 XML 文件被解析的时候,会根据其对应的约束文件,对 XML 进行校验,确保它是符合预设的模式的。Spring 支持 DTD 和 XSD 两种约束文件,两者的约束逻辑是不一样的,那在解析的时候,怎么知道该采样哪种校验方式呢?这就是validationMode
参数指定的。
这个参数传入的值是getValidationModeForResource(resource)
,接下来我们就通过源码分析一下,这个方法是怎么通过资源来判断验证模式的。先看这个方法的源码:
protected int getValidationModeForResource(Resource resource) {
int validationModeToUse = getValidationMode();
if (validationModeToUse != VALIDATION_AUTO) {
return validationModeToUse;
}
int detectedMode = detectValidationMode(resource);
if (detectedMode != VALIDATION_AUTO) {
return detectedMode;
}
// Hmm, we didn't get a clear indication... Let's assume XSD,
// since apparently no DTD declaration has been found up until
// detection stopped (before finding the document's root tag).
return VALIDATION_XSD;
}
复制代码
第一步,先调用getValidationMode()
获取默认要使用的模式,这里获取到的是当前类的成员变量validationMode
,它的值是VALIDATION_AUTO
,因此,第一个if语句块不会被执行。我们接着往下看。
下面又调用了detectValidationMode(resource)
方法,通过资源自动探测它的验证模式,如果与VALIDATION_AUTO
的值不同,则使用探测到的模式,否则采用 XSD 的验证方式。这里就需要看一下detectValidationMode
方法是如何进行探测的。
protected int detectValidationMode(Resource resource) {
if (resource.isOpen()) {
throw new BeanDefinitionStoreException(...);
}
InputStream inputStream;
try {
inputStream = resource.getInputStream();
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(..., ex);
}
try {
return this.validationModeDetector.detectValidationMode(inputStream);
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(..., ex);
}
}
复制代码
以上代码中核心的就两行,先获取到资源的输入流,然后调用this.validationModeDetector.detectValidationMode
方法。这里的validationModeDetector
也是直接初始化好的成员变量:
private final XmlValidationModeDetector validationModeDetector = new XmlValidationModeDetector();
复制代码
我们找到它的detectValidationMode方法:
public int detectValidationMode(InputStream inputStream) throws IOException {
// Peek into the file to look for DOCTYPE.
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
try {
boolean isDtdValidated = false;
String content;
while ((content = reader.readLine()) != null) {
content = consumeCommentTokens(content);
if (this.inComment || !StringUtils.hasText(content)) {
continue;
}
if (hasDoctype(content)) {
isDtdValidated = true;
break;
}
if (hasOpeningTag(content)) {
// End of meaningful data...
break;
}
}
return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
}
catch (CharConversionException ex) {
// Choked on some character encoding...
// Leave the decision up to the caller.
return VALIDATION_AUTO;
}
finally {
reader.close();
}
复制代码
这里的逻辑比较简单,就是判断 XML 文件中是不是包含DOCTYPE
,如果包含就采用 DTD 校验,否则采用 XSD 校验。为什么呢?我们分别看一下采用两种约束文件的 XML 文件内容就知道了。
一个采用 XSD 约束的 XML 配置文件是这样的:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- bean 配置 -->
</beans>
复制代码
一个采用 DTD 约束的 XML 配置文件是这样的:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- bean 配置 -->
</beans>
复制代码
因此可以通过是不是包含 DOCTYPE 来判断了两者的类型。
namespaceAware
最后来看一下namespaceAware
这个参数,它用来设置 XML 解析器对命名空间的支持,这里传入的值是通过 XmlBeanDefinitionReader 的isNamespaceAware()
方法获取的,这个方法直接读取了namespaceAware
成员变量,它的默认值是false
。
不过,在之前也给这个成员变量赋过一个值。
在之前的源码分析(Spring 源码阅读 04:BeanFactory 初始化 )中,可以找到 XmlBeanDefinitionReader 创建和初始化的代码,在AbstractXmlApplicationContext
的loadBeanDefinitions(DefaultListableBeanFactory beanFactory)
有这样一段代码:
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
initBeanDefinitionReader(beanDefinitionReader);
复制代码
这是 beanDefinitionReader 最初被创建的代码,在上面这段代码的最后一行调用了一个initBeanDefinitionReader方法:
protected void initBeanDefinitionReader(XmlBeanDefinitionReader reader) {
reader.setValidating(this.validating);
}
复制代码
这里的this.validating
的值是true
,也就是给XmlBeanDefinitionReader
开启了 XML 文件的校验。那它跟namespaceAware
有什么关系呢?我们看一下它的setValidating
方法:
public void setValidating(boolean validating) {
this.validationMode = (validating ? VALIDATION_AUTO : VALIDATION_NONE);
this.namespaceAware = !validating;
}
复制代码
可以看到这里同时给namespaceAware
赋了值,为false
。
后续
这一篇分析了将 XML 配置文件资源加载为 Document 对象的过程,虽然具体的加载过程不属于 Spring 框架的范畴,但是我们分析了一些解析器配置相关的内容。下一步就是将 Document 对象解析为 BeanDefinition 的过程,放在下一篇。