前情提要
上一篇介绍了 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 中的离线文件代替需要下载的在线文件。
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
。