「ナゲッツデイリー新プラン・8月アップデートチャレンジ」参加24日目、イベント詳細はこちら
Spring Framework v5.2.6.RELEASE に基づく
要約
前回の記事では、BeanDefinition の読み込みプロセスの準備フェーズで行われる作業について紹介しました。この記事では、その後のプロセスについて説明します。準備フェーズが終了するXmlBeanDefinitionReader
と、クラスのメソッドが入力doLoadBeanDefinitions
され、2 つの主要なメソッド呼び出しがあります。
Document doc = doLoadDocument(inputSource, resource);
int count = registerBeanDefinitions(doc, resource);
复制代码
メソッドの名前からわかるように、最初のメソッド呼び出しは構成リソースをロードし、最終的に Document オブジェクトを取得することであり、2 番目のメソッド呼び出しはコンテナーに 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
の値は、XML ファイルの検証がオンになっていることです。それで、それはそれと何の関係があるのですか?その方法を見てみましょう。true
XmlBeanDefinitionReader
namespaceAware
setValidating
public void setValidating(boolean validating) {
this.validationMode = (validating ? VALIDATION_AUTO : VALIDATION_NONE);
this.namespaceAware = !validating;
}
复制代码
namespaceAware
に対して、ここで同時に値が割り当てられていることがわかりますfalse
。
ファローアップ
この記事では、XML 構成ファイル リソースを Document オブジェクトとしてロードするプロセスを分析します. 特定のロード プロセスは Spring フレームワークの範囲には属しませんが、パーサーの構成に関連するいくつかのコンテンツを分析します. 次のステップは、Document オブジェクトを解析して BeanDefinition にするプロセスです。これは次の記事に記載されています。