• Spring5源码分析(008)——IoC篇之加载BeanDefinition:获取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 的验证模式

      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、参考

  • 相关阅读:
    JSP实现数据传递(web基础学习笔记三)
    Spring Boot 参数校验
    Spring AOP实践
    Spring AOP介绍
    2018年春节
    InnoDB索引
    Kafka基本知识回顾及复制
    Kakfa消息投递语义
    Kafka Consumer
    Kafka Producer Consumer
  • 原文地址:https://www.cnblogs.com/wpbxin/p/13207581.html
Copyright © 2020-2023  润新知