Spring5源码分析(008)——IoC篇之加载BeanDefinition:获取XML的验证模式 -- 转载
注:《Spring5源码分析》汇总可参考:Spring5源码分析(002)——博客汇总
作者: wpbxin
出处:https://www.cnblogs.com/wpbxin/p/13207581.html原文: https://www.cnblogs.com/wpbxin/p/13207581.html
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。
上一篇《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 的区别
- 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 支持属性组。
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 or 【死磕 Spring】----- IOC 之 获取验证模型
- 芋道源码:http://svip.iocoder.cn/Spring/IoC-Validation-Mode-For-Resource/(死磕 Spring系列的略微修改)
- 相关注释可参考笔者 github 链接:https://github.com/wpbxin/spring-framework
- 相关的 UML 图都可以子模块的 diagram 目录下查找
(PS: 这篇写得比较详细也好理解,很赞!)