Spring4.2.4 版本中的一个关于XML验证模式获取的bug

在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类的大概逻辑如下:

  1. detectValidationMode方法中接收到传递过来的输入流,并定义变量isDtdValidated =flase,
    即默认为XSD验证模式
  2. 由BufferedReader类实例对象reader每次读取一行信息,传递给consumeCommentTokens(content)方法,把读取到的内容去掉注释和空的部分,将剩下的内容返回
  3. if (hasDoctype(content)) {
    isDtdValidated = true;
    break;
    }
    判断返回的内容中是否含"DOCTYPE",如果含有,则为DTD约束,不含有则为XSD约束
  4. 根据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>将会执行以下流程:

  1. 由于inComment默认值为false, 在consume()方法中会调用startComment(line)方法;
  2. 最终执行的方法为:commentToken("<a>aa</a><!-- 注释部分–><a>bb</a>", “<!- -”,true) ;
  3. 在commentToken方法中通过line.indexOf()方法获取到 “<!- -” 的下标(假设为9) , 那么返回的数值为:9+token.length()=13;
  4. 因此在consume()方法中返回的字符串line.substring(13)= " 注释部分 --><a>bb</a>“在后续的循环中会返回”<a>bb</a>" ,那么 "<a>aa</a>"部分数据被line.substring(index)舍弃了;
  5. 在测试类中,因为头文件后面的一行注释,导致头文件信息被删除掉,那么由于isDtdValidated的初始值为false,所以detectValidationMode()方法将按照XSD模式进行返回,也就导致了xml文件的读取失败,所以会报出找不到元素 'beans’的异常;

目前spring中该问题好像还没有被修复,所以还是要注意下…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值