注:《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、参考
- spring 官方文档 5.2.3.RELEASE:https://docs.spring.io/spring-framework/docs/5.2.3.RELEASE/spring-framework-reference/core.html
- Spring源码深度解析(第2版),郝佳,P33-P37,2.6 获取XML的验证模式
- 死磕Spring系列:【死磕 Spring】----- IOC 之 获取验证模型
- 芋道源码:http://svip.iocoder.cn/Spring/IoC-Validation-Mode-For-Resource/(死磕 Spring系列的略微修改)