Spring5源码分析(008)——IoC篇之获取XML的验证模式

注:《Spring5源码分析》汇总可参考:Spring5源码分析(002)——博客汇总


  上一篇《Spring5源码分析(007)——IoC篇之加载 BeanDefinition(的大致流程)》 中提到,加载 bean 的核心方法 doLoadBeanDefinitions(InputSource inputSource, Resource resource) 中,分为3个步骤来进行:
1、通过调用 getValidationModeForResource(Resource resource) 来获取指定 XML 资源的验证模式,也即是 xml 开头中见到的各种 DTD 和 XSD 了。
2、通过调用 DocumentLoader.loadDocument(InputSource inputSource, EntityResolver entityResolver, ErrorHandler errorHandler, int validationMode, boolean namespaceAware) 来获取实际的 Document 实例。
3、调用 registerBeanDefinitions(Document doc, Resource resource),根据 Document 解析和注册 BeanDefinition。


  本文主要介绍第1个步骤,也就是获取 XML 资源文件的验证模式,目录结构如下:

  • 1、为什么需要获取 XML 的验证模式
  • 2、DTD 和 XSD 的区别
    • 2.1、DTD
    • 2.2、XSD
  • 3、getValidationModeForResource(Resource resource)
  • 4、XmlValidationModeDetector.detectValidationMode(InputStream inputStream)
  • 5、总结
  • 6、参考

1、为什么需要获取 XML 的验证模式

  XML 文件的验证模式保证了 XML 文件的正确性。换句话说,只有符合验证模式的 XML 文件,才能根据约定进行正确的解析。这就跟 Java(或者其他编程语言)一样,需要有语法和词汇等进行约束和规范,只有遵守了,编译器才能正确编译一样,验证模式正是这样的约束和规范。比较常用的 XML 验证模式有两种:DTD 和 XSD。下面将分别进行介绍。

2、DTD 和 XSD 的区别

2.1、DTD

  DTD (Document Type Definition)即文挡类型定义,是一种 XML 约束模式语言,是 XML 文件的验证机制,属于 XML 文件组成的一部分。DTD 是一种保证 XML 文档格式正确的有效验证机制,可以通过比较 XML 文档和 DTD 文件来看文档是否符合规范,元素和标签使用是否正确。一个 DTD 文档包含:元素的定义规则、元素间关系的定义规则、元素可使用的属性、可使用的实体或符号规则。它定义了 XML 文档相关的元素、属性、实体、排列方式、元素的内容类型以及元素的层次结构。

  需要使用 DTD 验证模式时,可以在 XML 配置文件中增加如下代码(Spring-beans-2.0.dtd):

<?xml versioπ= "1.0" encod1ng="UTF8"?>
<!DOCTYPE beans PUBLIC "-//Spring//DTD BEAN 2.0//EN" "http://www.Springframework.org/dtd/Spring-beans-2.O.dtd">

PS:笔者接触过的 Spring 项目中未曾遇到过 DTD 相关配置 ^_^

  DTD 有一定的作用,但其设计本身有些缺陷(参考:DTD的局限性):

  • 语法结构:DTD 不遵守 XML 语法,它自定义了一套与 XML 文档实例不一样的语法结构,这导致解析策略(解析器,DOM、XPath等)难以重用
  • 元素类型:DTD 对元素类型支持有限,不能自由扩充,不利于XML数据交换场合验证,扩展性差
  • 文档结构:DTD中,所有元素、属性都是全局的,无法声明仅与上下文位置相关的元素或属性
  • 命名空间:DTD 不支持命名空间

2.2、XSD

  针对 DTD 的缺陷,W3C 在 2001 年推出 XSD(XML Schemas Definition),即 XML Schema 定义,来对 DTD 进行替代。XML Schema 本身就是一个 XML文档,使用的是 XML 语法,因此可以很方便地使用通用的 XML 解析器来解析 XSD 文档。相对于 DTD,XSD 具有如下优势:

  • XML Schema 基于 XML ,没有专门的语法。
  • XML Schema 可以象其他 XML 文件一样解析和处理。
  • XML Schema 比 DTD 提供了更丰富的数据类型。
  • XML Schema 提供可扩充的数据模型。
  • XML Schema 支持综合命名空间。
  • XML Schema 支持属性组。
  • XML中DTD,XSD的区别与应用

PS:这部分算是对 DTD 和 XSD 做了个大致的介绍,稍微了解即可 ^_^

3、getValidationModeForResource(Resource resource)

  回到 Spring 中用于获取指定 XML 资源文件的验证模式的方法 getValidationModeForResource(Resource resource) 上来: 

/**
 * Indicates that the validation should be disabled.
 * 禁止验证模式
 */
public static final int VALIDATION_NONE = XmlValidationModeDetector.VALIDATION_NONE;

/**
 * Indicates that the validation mode should be detected automatically.
 * 自动检测验证模式
 */
public static final int VALIDATION_AUTO = XmlValidationModeDetector.VALIDATION_AUTO;

/**
 * Indicates that DTD validation should be used.
 * DTD 验证模式
 */
public static final int VALIDATION_DTD = XmlValidationModeDetector.VALIDATION_DTD;

/**
 * Indicates that XSD validation should be used.
 * XSD 验证模式
 */
public static final int VALIDATION_XSD = XmlValidationModeDetector.VALIDATION_XSD;

// 指定的验证模式,默认是自动检测
private int validationMode = VALIDATION_AUTO;

/**
 * Determine the validation mode for the specified {@link Resource}.
 * If no explicit validation mode has been configured, then the validation
 * mode gets {@link #detectValidationMode detected} from the given resource.
 * <p>Override this method if you would like full control over the validation
 * mode, even when something other than {@link #VALIDATION_AUTO} was set.
 * <p>确定指定资源的验证模式。如果没有显式配置验证模式,则从给定资源检测获取验证模式。
 * <p>如果需要完全控制验证模式,请覆盖此方法,即使在设置了 VALIDATION_AUTO 以外的内容时也是如此。
 * @see #detectValidationMode
 */
protected int getValidationModeForResource(Resource resource) {
    // 1、获取指定的验证模式
    int validationModeToUse = getValidationMode();
    // 如果显式指定了验证模式则使用指定的验证模式
    if (validationModeToUse != VALIDATION_AUTO) {
        return validationModeToUse;
    }
    // 2、如果未指定则使用自动检测
    int detectedMode = detectValidationMode(resource);
    if (detectedMode != VALIDATION_AUTO) {
        return detectedMode;
    }
     // 3、还是没有找到验证模式的显示声明,则最后默认使用 XSD 验证模式
     // 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;
}
  •  1、获取指定的验证模式:这里首先是通过 getValidationMode() 先获取指定的验证模式,没有进行显式配置时,返回的验证模式是默认的 VALIDATION_AUTO 。开发者可以通过以下方法设置和获取指定的验证模式: 
/**
 * Set the validation mode to use. Defaults to {@link #VALIDATION_AUTO}.
 * <p>Note that this only activates or deactivates validation itself.
 * If you are switching validation off for schema files, you might need to
 * activate schema namespace support explicitly: see {@link #setNamespaceAware}.
 * <p>设置验证模式
 */
public void setValidationMode(int validationMode) {
    this.validationMode = validationMode;
}

/**
 * Return the validation mode to use.
 */
public int getValidationMode() {
    return this.validationMode;
}
  • 2、自动检测验证模式:从代码可以看到,默认的是 VALIDATION_AUTO,因此需要进行验证模式的自动检测(根据文档的内部声明来获取):detectValidationMode(Resource resource) 
/**
 * Detect which kind of validation to perform on the XML file identified
 * by the supplied {@link Resource}. If the file has a {@code DOCTYPE}
 * definition then DTD validation is used otherwise XSD validation is assumed.
 * <p>Override this method if you would like to customize resolution
 * of the {@link #VALIDATION_AUTO} mode.
 * <p>检测 Resource 资源对应的 XML 文件需要执行的验证模式。
 * 如果 XML 有 DOCTYPE 声明,则使用 DTD,否则使用 XSD
 * <p>如果需要定制个性化的检测策略,得重写这个方法。
 */
protected int detectValidationMode(Resource resource) {
    // 资源已被打开,抛出 BeanDefinitionStoreException 异常
     if (resource.isOpen()) {
        throw new BeanDefinitionStoreException(
                "Passed-in Resource [" + resource + "] contains an open stream: " +
                "cannot determine validation mode automatically. Either pass in a Resource " +
                "that is able to create fresh streams, or explicitly specify the validationMode " +
                "on your XmlBeanDefinitionReader instance.");
    }
    // 打开输入流
    InputStream inputStream;
    try {
        inputStream = resource.getInputStream();
    }
    catch (IOException ex) {
        throw new BeanDefinitionStoreException(
                "Unable to determine validation mode for [" + resource + "]: cannot open InputStream. " +
                "Did you attempt to load directly from a SAX InputSource without specifying the " +
                "validationMode on your XmlBeanDefinitionReader instance?", ex);
    }

    try {
        // 获取 XML 文件相应的验证模式
          return this.validationModeDetector.detectValidationMode(inputStream);
    }
    catch (IOException ex) {
        throw new BeanDefinitionStoreException("Unable to determine validation mode for [" +
                resource + "]: an error occurred whilst reading from the InputStream.", ex);
    }
}

   detectValidationMode 中将实际的自动检测验证模式的工作委托给了处理类 org.springframework.util.xml.XmlValidationModeDetector,调用了 detectValidationMode(InputStream inputStream) 方法。

  • 3、最终没有找到对应的验证模式时,则使用 XSD 作为默认的验证模式

4、XmlValidationModeDetector.detectValidationMode(InputStream inputStream)

  org.springframework.util.xml.XmlValidationModeDetector:XML 验证模式检测器 

/**
 * Detect the validation mode for the XML document in the supplied {@link InputStream}.
 * Note that the supplied {@link InputStream} is closed by this method before returning.
 * <p>通过提供的 XML 文档对应的 InputStream 来检测验证模式
 * @param inputStream the InputStream to parse
 * @throws IOException in case of I/O failure
 * @see #VALIDATION_DTD
 * @see #VALIDATION_XSD
 */
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;
        // 1、逐行读取 XML 文件读的内容
        while ((content = reader.readLine()) != null) {
              // 
            content = consumeCommentTokens(content);
           // 如果读取的行是空或者是注释,则略过
            if (this.inComment || !StringUtils.hasText(content)) {
                continue;
            }
           // 2、检查是否包含关键字 "DOCTYPE" ,是的话就是 DTD 验证模式
            if (hasDoctype(content)) {
                isDtdValidated = true;
                break;
            }
            // 3、读取到 < 开始符号,验证模式一定会在开始符号之前
            if (hasOpeningTag(content)) {
                // End of meaningful data...
                break;
            }
        }
        return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
    }
    catch (CharConversionException ex) {
          // 4、解析异常,还是返回 VALIDATION_AUTO 模式,由调用者决策
          // Choked on some character encoding...
        // Leave the decision up to the caller.
        return VALIDATION_AUTO;
    }
    finally {
        reader.close();
    }
}
  • 1、逐行读取 XML 文件读的内容,然后下一步就是根据读取的内容来判断
  • 2、调用 hasDoctype(String content),检查是否包含关键字 "DOCTYPE" ,是的话就是 DTD 验证模式: 
/**
 * The token in a XML document that declares the DTD to use for validation
 * and thus that DTD validation is being used.
 */
private static final String DOCTYPE = "DOCTYPE";

/**
 * Does the content contain the DTD DOCTYPE declaration?
 */
private boolean hasDoctype(String content) {
    return content.contains(DOCTYPE);
}
  • 3、调用 hasOpeningTag(String content) 方法,判断如果这一行包含 < ,并且 < 紧跟着的是字母,则为 XSD 验证模式: 
private boolean hasOpeningTag(String content) {
    if (this.inComment) {
        return false;
    }
    int openTagIndex = content.indexOf('<');
    // < 存在 且 < 后面还有内容
    return (openTagIndex > -1 && (content.length() > openTagIndex + 1) &&
            Character.isLetter(content.charAt(openTagIndex + 1)));    // < 后面的内容是字母
}
  • 4、解析异常,还是返回 VALIDATION_AUTO 模式,由调用者决策。
  • 关于获取 consumeCommentTokens(String line) ,这个是将给定的字符串去掉前导和后导的注释,然后返回剩下的内容,代码如下,可自行研究: 
/**
 * Consume all leading and trailing comments in the given String and return
 * the remaining content, which may be empty since the supplied content might
 * be all comment data.
 * <p>去掉给定字符串的前导和后导注释,并返回剩余的内容。结果可能是空,例如全是注释的情况下。
 */
@Nullable
private String consumeCommentTokens(String line) {
    int indexOfStartComment = line.indexOf(START_COMMENT);
    if (indexOfStartComment == -1 && !line.contains(END_COMMENT)) {
        return line;
    }

    String result = "";
    String currLine = line;
    if (indexOfStartComment >= 0) {
        result = line.substring(0, indexOfStartComment);
        currLine = line.substring(indexOfStartComment);
    }

    while ((currLine = consume(currLine)) != null) {
        if (!this.inComment && !currLine.trim().startsWith(START_COMMENT)) {
            return result + currLine;
        }
    }
    return null;
}

/**
 * Consume the next comment token, update the "inComment" flag
 * and return the remaining content.
 */
@Nullable
private String consume(String line) {
    int index = (this.inComment ? endComment(line) : startComment(line));
    return (index == -1 ? null : line.substring(index));
}

/**
 * Try to consume the {@link #START_COMMENT} token.
 * @see #commentToken(String, String, boolean)
 */
private int startComment(String line) {
    return commentToken(line, START_COMMENT, true);
}

private int endComment(String line) {
    return commentToken(line, END_COMMENT, false);
}

这部分可以参考:

5、总结

  本文主要介绍如何获取 XML 资源文件的验证模式,简单点总结就是:XML 文档中有关键字 "DOCTYPE" 声明的,就是用 DTD 验证模式,其他情况下基本使用 XSD 验证模式。

6、参考

猜你喜欢

转载自www.cnblogs.com/wpbxin/p/13207581.html