在Spring4.2.4源码的学习中发现了在Spring-core包下的org.springframework.util.xml.XmlValidationModeDetector类中对DTD验证模式获取的逻辑错误
测试程序:
1.先定义一个bean
package bean;
public class MyTestBean {
private String testStr="testStr";
public String getTestStr() {
return testStr;
}
public void setTestStr(String testStr) {
this.testStr = testStr;
}
}
2.配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="myTestBean" class="bean.MyTestBean"/>
</beans>
3.测试类:
public class TestClass {
@Test
public void testSimpleLoad(){
Resource resource = new ClassPathResource("beanFactoryTest.xml");
XmlBeanFactory bf = new XmlBeanFactory(resource);
MyTestBean bean=(MyTestBean) bf.getBean("myTestBean");
System.out.println(bean.getTestStr());
}
}
运行结果:
控制台输出: testStr
但是如果对配置文件做一个小小的更改:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"><!-- 这里添加一行注释 -->
<beans>
<bean id="myTestBean" class="bean.MyTestBean"/>
</beans>
也就是在头文件的最后面添加一行注释, 然后再次运行测试类:
org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException: Line 3 in XML document from class path resource [beanFactoryTest.xml] is invalid; nested exception is org.xml.sax.SAXParseException; lineNumber: 3; columnNumber: 8; cvc-elt.1: 找不到元素 'beans' 的声明。
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:399)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:336)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:304)
at org.springframework.beans.factory.xml.XmlBeanFactory.<init>(XmlBeanFactory.java:79)
at org.springframework.beans.factory.xml.XmlBeanFactory.<init>(XmlBeanFactory.java:67)
at test.TestClass.testSimpleLoad(TestClass.java:17)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
Caused by: org.xml.sax.SAXParseException; lineNumber: 3; columnNumber: 8; cvc-elt.1: 找不到元素 'beans' 的声明。
at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException(ErrorHandlerWrapper.java:203)
at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.error(ErrorHandlerWrapper.java:134)
at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:396)
at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:327)
at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:284)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.handleStartElement(XMLSchemaValidator.java:1900)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.startElement(XMLSchemaValidator.java:740)
at com.sun.org.apache.xerces.internal.impl.dtd.XMLDTDValidator.startElement(XMLDTDValidator.java:745)
at com.sun.org.apache.xerces.internal.impl.XMLNSDocumentScannerImpl.scanStartElement(XMLNSDocumentScannerImpl.java:380)
at com.sun.org.apache.xerces.internal.impl.XMLNSDocumentScannerImpl$NSContentDriver.scanRootElementHook(XMLNSDocumentScannerImpl.java:614)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl$FragmentContentDriver.next(XMLDocumentFragmentScannerImpl.java:3135)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl$PrologDriver.next(XMLDocumentScannerImpl.java:880)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(XMLDocumentScannerImpl.java:606)
at com.sun.org.apache.xerces.internal.impl.XMLNSDocumentScannerImpl.next(XMLNSDocumentScannerImpl.java:118)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(XMLDocumentFragmentScannerImpl.java:510)
at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:848)
at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:777)
at com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(XMLParser.java:141)
at com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(DOMParser.java:243)
at com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:339)
at org.springframework.beans.factory.xml.DefaultDocumentLoader.loadDocument(DefaultDocumentLoader.java:76)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadDocument(XmlBeanDefinitionReader.java:429)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:391)
... 28 more
结果: 控制台报错,提示找不到元素 ‘beans’ 的声明,但是在添加注释前是可以找到,并正常运行
原因: 这是因为XmlValidationModeDetector类在获取XML验证模式时逻辑有点问题
以下为org.springframework.util.xml.XmlValidationModeDetector类的代码:
public class XmlValidationModeDetector {
public static final int VALIDATION_NONE = 0;
public static final int VALIDATION_AUTO = 1;
public static final int VALIDATION_DTD = 2;
public static final int VALIDATION_XSD = 3;
private static final String DOCTYPE = "DOCTYPE";
private static final String START_COMMENT = "<!--";
private static final String END_COMMENT = "-->";
private boolean inComment;
//inputStream为读取的xml文件的输入流
public int detectValidationMode(InputStream inputStream) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
try {
//默认为XSD约束
boolean isDtdValidated = false;
String content;
while ((content = reader.readLine()) != null) {
//将读取到的内容去掉注释和空的部分,将剩下的内容返回
content = consumeCommentTokens(content);
if (this.inComment || !StringUtils.hasText(content)) {
continue;
}
//如果该行含有DOCTYPE,则就是DTD,否则就是XSD
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();
}
}
private boolean hasDoctype(String content) {
return content.contains(DOCTYPE);
}
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)));
}
private String consumeCommentTokens(String line) {
//如果不含有注释,则返回该行内容
if (!line.contains(START_COMMENT) && !line.contains(END_COMMENT)) {
return line;
}
//如果含有注释,则将注释内容去掉,返回剩余内容
//例如:<a>aa</a><!-- 注释部分 --><a>bb</a>
while ((line = consume(line)) != null) {
// inComment:指示当前解析位置是否位于XML注释中
if (!this.inComment && !line.trim().startsWith(START_COMMENT)) {
return line;
}
}
return line;
}
private String consume(String line) {
int index = (this.inComment ? endComment(line) : startComment(line));
// line.substring(index):返回该标记之后的内容
return (index == -1 ? null : line.substring(index));
}
private int startComment(String line) {
return commentToken(line, START_COMMENT, true);
}
private int endComment(String line) {
return commentToken(line, END_COMMENT, false);
}
private int commentToken(String line, String token, boolean inCommentIfPresent) {
//找到开始或结束标签在该行位置
//<a>aa</a><!-- 注释部分 --><a>bb</a>
int index = line.indexOf(token);
//如果<!-- 符号在该行中,那么将inComment改为true;
//如果--> 符号在该行中,那么将inComment改为false;
if (index > - 1) {
this.inComment = inCommentIfPresent;
}
return (index == -1 ? index : index + token.length());
}
}
XmlValidationModeDetector类的大概逻辑如下:
- detectValidationMode方法中接收到传递过来的输入流,并定义变量isDtdValidated =flase,
即默认为XSD验证模式 - 由BufferedReader类实例对象reader每次读取一行信息,传递给consumeCommentTokens(content)方法,把读取到的内容去掉注释和空的部分,将剩下的内容返回
- if (hasDoctype(content)) {
isDtdValidated = true;
break;
}
判断返回的内容中是否含"DOCTYPE",如果含有,则为DTD约束,不含有则为XSD约束 - 根据isDtdValidated 变量ture/false 来决定返回是XSD约束还是DTD约束
return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
问题出现在consumeCommentTokens(content)方法中的consume方法上:
private String consumeCommentTokens(String line) {
//如果不含有注释,则返回该行内容
if (!line.contains(START_COMMENT) && !line.contains(END_COMMENT)) {
return line;
}
//如果含有注释,则将注释内容去掉,返回剩余内容
while ((line = consume(line)) != null) {
// inComment:指示当前解析位置是否位于XML注释中(默认值为false)
if (!this.inComment && !line.trim().startsWith(START_COMMENT)) {
return line;
}
}
return line;
}
private String consume(String line) {
int index = (this.inComment ? endComment(line) : startComment(line));
// line.substring(index):返回该标记之后的内容
return (index == -1 ? null : line.substring(index));
}
private int startComment(String line) {
return commentToken(line, START_COMMENT, true);
}
private int endComment(String line) {
return commentToken(line, END_COMMENT, false);
}
private int commentToken(String line, String token, boolean inCommentIfPresent) {
//找到开始或结束标签在该行位置
int index = line.indexOf(token);
//如果<!-- 符号在该行中,那么将inComment改为true;
//如果--> 符号在该行中,那么将inComment改为false;
if (index > - 1) {
this.inComment = inCommentIfPresent;
}
return (index == -1 ? index : index + token.length());
}
假如读取到的一行数据为: <a>aa</a><!-- 注释部分 --><a>bb</a>将会执行以下流程:
- 由于inComment默认值为false, 在consume()方法中会调用startComment(line)方法;
- 最终执行的方法为:commentToken("<a>aa</a><!-- 注释部分–><a>bb</a>", “<!- -”,true) ;
- 在commentToken方法中通过line.indexOf()方法获取到 “<!- -” 的下标(假设为9) , 那么返回的数值为:9+token.length()=13;
- 因此在consume()方法中返回的字符串line.substring(13)= " 注释部分 --><a>bb</a>“在后续的循环中会返回”<a>bb</a>" ,那么 "<a>aa</a>"部分数据被line.substring(index)舍弃了;
- 在测试类中,因为头文件后面的一行注释,导致头文件信息被删除掉,那么由于isDtdValidated的初始值为false,所以detectValidationMode()方法将按照XSD模式进行返回,也就导致了xml文件的读取失败,所以会报出找不到元素 'beans’的异常;
目前spring中该问题好像还没有被修复,所以还是要注意下…