Spring 源码阅读 07:加载 BeanDefinition 的过程(资源加载阶段)

这是我参与「掘金日新计划 · 8 月更文挑战」的第24天,点击查看活动详情

基于 Spring Framework v5.2.6.RELEASE

接上篇:Spring 源码阅读 06:加载 BeanDefinition 的过程(准备阶段)

前情提要

上一篇介绍了 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());
}
复制代码

这里调用了成员变量documentLoaderloadDocument方法,为了便于之后的分析,先看一下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 文件解析过程,不在本文的讨论范畴,这里就不详细介绍了。

扫描二维码关注公众号,回复: 14493961 查看本文章

接下来再来分析loadDocument方法中的几个方法参数,从方法体中的代码看出,这几个参数传入的具体信息会影响 XML 解析的配置,其中,前三个参数的类型都属于org.xml.sax包,并不是 Spring 的一部分。

DocumentLoader#loadDocument方法的参数

inputSource

这里的inpustSource是对资源的输入流inputStream的封装,它其实就代表了要被解析的 XML 的输入流。除了输入流、编码、字符集以外,它还包含了两个成员变量:publicIdsystemId,这两个都是 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 成员变量,分别是BeansDtdResolverPluggableSchemaResolver类型,它们分别负责 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 创建和初始化的代码,在AbstractXmlApplicationContextloadBeanDefinitions(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 的过程,放在下一篇。

猜你喜欢

转载自juejin.im/post/7133250145029718029