我们在用 Java 解析 XML,当文档不是一个合法的 XML 时,可能会收到 [Fatal Error] 的控制台输出,即使把整个代码都 catch 住,仍然不能抑制住 [Fatal Error] 的信息输出。比如常见到这样的输出:
[Fatal Error] :1:1: Content is not allowed in prolog.
为什么不能禁掉它呢,本来 catch 了异常对程序已经有了很好的保护,想眼不见心不烦,但还是避之不及。
因为,因为这个 XML 解析器用 System.error.print() 输出来了,当然你可以用 System.setErr(PrintStream) 重定向掉错误输出,但不现实,波及面太大。我们需要找到源头,首先交代解决方案就是覆盖掉默认的 ErrorHandler。
看下这段 XML 解析代码:
import java.io.*;
import org.xml.sax.*;
import org.w3c.dom.Document;
import javax.xml.parsers.DocumentBuilderFactory;
public class Test {
public static void main(String[] args) {
String xml = "abcd";
try {
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
Document doc = builder.parse(new InputSource(new ByteArrayInputStream(xml.getBytes)))
} catch(SaxParseException|IOException e) {
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
importjava.io.*;
importorg.xml.sax.*;
importorg.w3c.dom.Document;
importjavax.xml.parsers.DocumentBuilderFactory;
publicclassTest{
publicstaticvoidmain(String[]args){
Stringxml="abcd";
try{
DocumentBuilderbuilder=DocumentBuilderFactory.newInstance().newDocumentBuilder()
Documentdoc=builder.parse(newInputSource(newByteArrayInputStream(xml.getBytes)))
}catch(SaxParseException|IOExceptione){
}
}
}
上面的代码就会输出
[Fatal Error] :1:1: Content is not allowed in prolog.
如果 xml 的值是空字符串 "",输出为
[Fatal Error] :-1:-1: Premature end of file.
如果 xml 的值是 "ss&Emal",输出为
[Fatal Error] :1:12: The reference to entity "Email" must end with the ';' delimiter.
是不是对上面的错误输出很熟悉啊。
那就要看 DocumentBuilderFactory.newInstance.newDocumentBuilder 的两个过程,首先看 DocumentBuilderFactory.newInstance() 方法,见
看到它依次以四种方式找到 DocumentBuilderFactory 的实现类
系统属性 javax.xml.parsers.DocumentBuilderFactory
JRE 目录下的属性文件 "lib/jaxp.properties" 中的 javax.xml.parsers.DocumentBuilderFactory
SPI 形式,classpath 下加载 META-INF/services/javax.xml.parsers.DocumentBuilderFactory, 一般在 jar 包中
平台默认的 DocumentBuilderFactory 实例
默认的 JDK7 环境中 DocumentBuilderFactory.newInstance 是 com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl,
DocumentBuilderFactory.newInstance.newDocumentBuilder 是 com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl
比如在 xercesImpl-2.11.0.jar 包下就有文件 META-INF/services/javax.xml.parsers.DocumentBuilderFactory, 内容是
org.apache.xerces.jaxp.DocumentBuilderFactoryImpl, 相应的 DocumentBuilder 是 org.apache.xerces.jaxp.DocumentBuilderImpl
在实例化 DocumentBuilderImpl 时并没有给 DocumentBuilder 或 DOMParser 设置 ErrorHandler,而是在解析发现问题是设置上 ErrorHandler,然后输出错误,见
上面设置了 DefaultErrorHandler, 点击 DefaultErrorHandler 看它的实现。它在 warning 和 error 时只打印错误,fatal 时除打印还抛出了异常。打印目的地是 System.err
但是我们可以设置一个 java.xml.parsers.DocumentBuilderFactory 系统属性从而使用自定义的 DocumentBuilderFactory 实现,在自己的 DocumentBuilderFactory 中初始化 DocumentBuilder 时设置自己的 ErrorHandler。
现在我们来实现自定义的 DocumentBuilderFactory:
CustomDocumentBuilderFactory.java
package helper;
import com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl;
import com.sun.org.apache.xml.internal.utils.DefaultErrorHandler;
import org.xml.sax.*;
import javax.xml.parsers.*;
public class CustomDocumentBuilderFactory extends DocumentBuilderFactoryImpl {
@Override
public DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
DocumentBuilder documentBuilder = super.newDocumentBuilder(); // 取回原本的 DocumentBuilder
documentBuilder.setErrorHandler(new DefaultErrorHandler() { // 只为替换掉 DefaultErrorHandler 的 fatalError() 方法
@Override
public void fatalError(SAXParseException exception) throws SAXException {
System.err.println("CustomDocumentBuilderFactory Caught the XML fatal error: " + exception.getMessage());
throw exception;
}
});
return documentBuilder;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
packagehelper;
importcom.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl;
importcom.sun.org.apache.xml.internal.utils.DefaultErrorHandler;
importorg.xml.sax.*;
importjavax.xml.parsers.*;
publicclassCustomDocumentBuilderFactoryextendsDocumentBuilderFactoryImpl{
@Override
publicDocumentBuildernewDocumentBuilder()throwsParserConfigurationException{
DocumentBuilderdocumentBuilder=super.newDocumentBuilder();// 取回原本的 DocumentBuilder
documentBuilder.setErrorHandler(newDefaultErrorHandler(){// 只为替换掉 DefaultErrorHandler 的 fatalError() 方法
@Override
publicvoidfatalError(SAXParseExceptionexception)throwsSAXException{
System.err.println("CustomDocumentBuilderFactory Caught the XML fatal error: "+exception.getMessage());
throwexception;
}
});
returndocumentBuilder;
}
}
然后重写获得 DocumentBuilder 部分代码为
System.setProperty("javax.xml.parsers.DocumentBuilderFactory", "helper.CustomDocumentBuilderFactory");
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = builder.parse(new InputSource(new ByteArrayInputStream("".getBytes())));
1
2
3
System.setProperty("javax.xml.parsers.DocumentBuilderFactory","helper.CustomDocumentBuilderFactory");
DocumentBuilderbuilder=DocumentBuilderFactory.newInstance().newDocumentBuilder();
Documentdoc=builder.parse(newInputSource(newByteArrayInputStream("".getBytes())));
再次执行就只看到控制台的输出为
TestDocumentBuilderFactoryImpl Caught the XML fatal error: Premature end of file.
表明我们已经成功捕获到了 fataError 了,怎么输出这个错误是可控制的了。
在不同的环境下,例如 Tomcat 中可以去查看下默认的 DocumentBuilderFactory 实现,然后自定义的 Factory 就可以继承它,只为设置自己的 ErrorHandler,同时也保证了不破坏该环境下原有其他的行为。
比如在 Play2 中 DocumentBuilderFactory 实现是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl, DocumentBuilder 实现是 org.apache.xerces.jaxp.DocumentBuilderImpl,它们来自 xercesImpl 包,这时候我们自定义的 DocumentBuilderFactory 就可以继承自 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl, 其中设置自己的 ErrorHandler。
另外,我们也可以通过调用 DefaultErrorHandler 的另一个构造方法 DefaultErrorHandler(PrintWriter out) 来创造实例,通过 传递一个 PrintWriter 实例来接受输出,这样也能控制不把 Fatal Error 输出到控制台上。