上一篇说到loadBeanDefinitions这个方法主要干了三件事儿,第一件是获取xml文件的验证模式;第二件是加载xml文件,获得对应的Document;第三件是对bean信息进行注册。本篇先对第一件事进行介绍。
在程序中如何判定某个文件是xml文件,只凭后缀肯定是不行的,只有对文件中的格式进行验证,才能证明确实是xml文件,这也为接下来的程序提供了保障。常用的验证模式有DTD验证和schema验证两种。
DTD验证模式说白了,就是使用DTD文档中规定的格式对xml文件的格式进行验证。DTD文档可以分为内部DTD、外部DTD和内外结合DTD三种。DTD文档包含三个部分:元素的定义规则,元素之间的关系规则和属性的定义规则。
1).内部DTD就是在xml文档内部嵌入DTD格式的内容,具体形式是:,比如:
]>
小明
男
24
可以看到,通过使用将DTD文档的内容包含在内,我们也可以看到xml文件的格式是根据DTD文档的定义来的。
2}.外部DTD文档就是将DTD的内容放到另一个文件中,这样的用意在于内部DTD只能对嵌套的xml进行验证,而外部DTD可以对任何符合该DTD的xml进行验证,引用格式:。
3).内外结合的DTD引用格式:,很显然,就是上面二者的结合。
DTD文档自己拥有一套属于自己的语言,用来进行编写,如上面代码所示,我们可以通过这个语言实现元素、实体、属性的定义。我们可以通过DTD文档对xml文件进行很好的管理,但同时,DTD文档也有自己的局限性:首先,DTD文档有自己的一套语法,与xml的语法不同,这为我们带来了一定的不便;其次,DTD文档的数据类型有限,无法和数据库等的数据类型进行完全匹配;再次,DTD文档无法扩展,这种僵化的编程是我们不希望看到的;最后,DTD文档不支持命名空间,也就是说会产生命名冲突的问题,也就是说,在DTD文档中,每一个标签的名称都是唯一的,但在实际应用中,我们可以发现,同一个属性可能会被不同的实体所包含,但DTD由于不支持命名空间无法解决这一问题。
由于DTD存在着上述的局限性,人们开始寻求一种更好的方式来对xml文件进行定义和验证,schema文档就出现了。Schema验证模式又叫做xml Schema模式,也就是XSD模式。XSD模式就是利用xml Schema文档来对xml文件进行规定,它具有DTD的所有功能,同时又比DTD强大。前文的Demo中我们的spring config文件中就使用了xsd模式:
我们重点关注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"
第一行声明了schema的命名空间,第二行是所有schema文档的根命名空间,第三行标识了schema文档所在的位置。
上面是spring-beans.xsd的部分代码,可以看到xsd文件和xml文件的语法基本一样。
介绍完两种验证模式,继续回到doLoadBeanDefinition方法中。
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
int validationMode = getValidationModeForResource(resource);
Document doc = this.documentLoader.loadDocument(
inputSource, getEntityResolver(), this.errorHandler, validationMode, isNamespaceAware());
return registerBeanDefinitions(doc, resource);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (SAXParseException ex) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(),
"Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
}
catch (SAXException ex) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(),
"XML document from " + resource + " is invalid", ex);
}
catch (ParserConfigurationException ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"Parser configuration exception parsing XML from " + resource, ex);
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"IOException parsing XML document from " + resource, ex);
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"Unexpected exception parsing XML document from " + resource, ex);
}
}
/**
* Gets the validation mode for the specified {@link Resource}. If no explicit
* validation mode has been configured then the validation mode is
* {@link #detectValidationMode detected}.
* Override this method if you would like full control over the validation
* mode, even when something other than {@link #VALIDATION_AUTO} was set.
*/
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;
}
可以看到第一行就调用了getValidationModeForResource方法来对xml进行验证,进方法里可以看到,首先获取resource对象的validationMode属性,然后判断如果指定了验证模式,就选择相应的模式,否则就调用detectValidationMode方法对验证模式进行检测。进入这个方法:
protected int detectValidationMode(Resource resource) {
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 {
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);
}
}
可以看到,获取了resource对象的inputStream后,调用了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文件,然后调用consumeCommentTokens来对文件中的注释进行跳过,代码如下:
private String consumeCommentTokens(String line) {
if (!line.contains(START_COMMENT) && !line.contains(END_COMMENT)) {
return line;
}
while ((line = consume(line)) != null) {
if (!this.inComment && !line.trim().startsWith(START_COMMENT)) {
return line;
}
}
return line;
}
其中,START_COMMENT的值是"",如果没有注释,直接返回这一行;xmlValidationModeDetector维护了一个布尔变量inComment用来标识某一行是否在注释中,如果在,则置true,并返回注释的内容到上一层方法,上一层方法对文档遍历的逻辑会直接跳过这一行对下一行进行遍历。继续往下走。获取到的一行会首先判断是否含有DOC验证模式的标志:"DOCTYPE",如果有则直接返回DTD验证模式,如果没有,那么就读取xml文件的开始符号,因为我们在编写xml文件之前,一定会先确定验证模式。
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)));
}
从代码里看到,为了获取xml文档的开始符号,先获取"<"的位置,保证它的位置在一行里面,然后判断它后面跟的是不是字母,这从而过滤了所有xml文件的第一行:
这一行是用来标识xml文件的版本和编码的,其中的<后面跟的是?,而xml文件的真正开头后面肯定跟的是字母。判断出来以后,返回相应的值就好了,最后探测的结果会在getValidationModeForResource中进行判定,只要不是DOC模式,就会返回XSD模式。至此,xml文件的模式验证过程结束。