我们在用 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 解析代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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) { } } } |
上面的代码就会输出
[Fatal Error] :1:1: Content is not allowed in prolog.
如果 xml 的值是空字符串 "",输出为
[Fatal Error] :-1:-1: Premature end of file.
如果 xml 的值是 "<s>ss&Emal</s>",输出为
[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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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; } } |
然后重写获得 DocumentBuilder 部分代码为
1 2 3 |
System.setProperty("javax.xml.parsers.DocumentBuilderFactory", "helper.CustomDocumentBuilderFactory"); DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document doc = builder.parse(new InputSource(new ByteArrayInputStream("".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 输出到控制台上。