Lectura de código fuente de Spring 07: El proceso de carga de BeanDefinition (etapa de carga de recursos)

Este es el día 24 de mi participación en el "Nuevo plan diario de Nuggets · Desafío de actualización de agosto", haga clic para ver los detalles del evento

Basado en Spring Framework v5.2.6.RELEASE

Continuación del artículo anterior: Lectura del código fuente de Spring 06: El proceso de carga de BeanDefinition (etapa de preparación)

Resumen

El artículo anterior presentó el trabajo realizado en la fase de preparación del proceso de carga de BeanDefinition, y este artículo explora el proceso posterior. Una vez finalizada la fase de preparación, se ingresa XmlBeanDefinitionReaderel método de la clase doLoadBeanDefinitionsy hay dos llamadas de método clave:

Document doc = doLoadDocument(inputSource, resource);
int count = registerBeanDefinitions(doc, resource);
复制代码

Como puede verse por el nombre del método, la primera llamada al método es para cargar los recursos de configuración y finalmente obtener un objeto Document, y la segunda llamada al método es para registrar BeanDefinition en el contenedor.

Este artículo analiza primero la parte de la carga de recursos.

Análisis de recursos XML

Primero, ve al doLoadDocumentmétodo:

protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
   return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
         getValidationModeForResource(resource), isNamespaceAware());
}
复制代码

documentLoaderEl método de las variables miembro se llama aquí loadDocumentPara facilitar el análisis posterior, veamos en documentLoaderqué consiste. En la definición de variables miembro se puede encontrar:

private DocumentLoader documentLoader = new DefaultDocumentLoader();
复制代码

Aquí se crea uno directamente a través del constructor DefaultDocumentLoader, en su definición de clase no se incluye la definición del constructor, por lo que podemos pensar que aquí no se hace nada más que crear este objeto. Lo hace DocumentLoader, como sugiere el nombre, se trata de un cargador de documentos XML.

A continuación, busque DefaultDocumentLoaderel loadDocumentmétodo en:

@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);
}
复制代码

Aquí, el flujo de entrada de recursos se carga en un objeto Documento. El proceso de analizar un archivo XML en un objeto de documento es solo un proceso de análisis de archivos XML, que está más allá del alcance de este artículo y no se describirá en detalle aquí.

接下来再来分析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

不过,在之前也给这个成员变量赋过一个值。

En el análisis anterior del código fuente ( Lectura del código fuente de Spring 04: Inicialización de BeanFactory ), puede encontrar el código para la creación e inicialización de XmlBeanDefinitionReader, AbstractXmlApplicationContextexiste loadBeanDefinitions(DefaultListableBeanFactory beanFactory)un código de este tipo:

XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);

beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

initBeanDefinitionReader(beanDefinitionReader);
复制代码

Este es el código donde se crea inicialmente el beanDefinitionReader y se llama al método initBeanDefinitionReader en la última línea del código anterior:

protected void initBeanDefinitionReader(XmlBeanDefinitionReader reader) {
   reader.setValidating(this.validating);
}
复制代码

this.validatingEl valor aquí es trueque XmlBeanDefinitionReaderla validación del archivo XML está activada. Entonces, ¿qué tiene namespaceAwareque ver con eso? Echemos un vistazo a su setValidatingmétodo:

public void setValidating(boolean validating) {
   this.validationMode = (validating ? VALIDATION_AUTO : VALIDATION_NONE);
   this.namespaceAware = !validating;
}
复制代码

Puede ver que namespaceAwareel valor se asigna aquí al mismo tiempo, para false.

hacer un seguimiento

Este artículo analiza el proceso de carga de recursos del archivo de configuración XML como objetos Document. Aunque el proceso de carga específico no pertenece al alcance del framework Spring, analizamos algunos contenidos relacionados con la configuración del analizador. El siguiente paso es el proceso de analizar el objeto Document en un BeanDefinition, que se ubica en el siguiente artículo.

Supongo que te gusta

Origin juejin.im/post/7133250145029718029
Recomendado
Clasificación