内容


处理 XML 解析中的错误

使用 SAX 处理 XML 解析中的错误

Comments

在更新的 Java 语言 API(JAXP、JAXB 和 JAX-WS 等)中解析 XML 变得如此容易,以至于 XML 解析已成为 Java 编程的重要方面。但是,更高级的 API 中的抽象使得无法细粒度控制解析器和数据之间的交互,因此存在潜在的问题。在本文中,我将展示 SAX 如何提供一种易于使用的方法来处理这些错误,即使您没有直接使用 SAX,仍然可以使用这种方法。

错误处理避免程序崩溃

每个应用程序首先是一个应用程序用户。无论是 vi、emacs、DreamWeaver® 还是 Adobe® Photoshop®,在决定如何构建某个应用程序时,主要靠参考对其他应用程序的体验。因此,现代应用程序(特别是 Web 应用程序)中的错误处理就是在屏幕中显示无用的数字和字典中找不到的奇怪字母。如果幸运的话,还会出现某种字体格式的致歉。这是一种非常拙劣的在应用程序中处理问题的方式。

您的应用程序 出现错误,这是人人都懂的道理,但是会使情况变得更糟。正如您找到了通过使用不同类路径启动同步构建而使 Eclipse 崩溃的独特方法,应用程序的用户,即您的程序将设法中止未结束的线程,没有从请求变量中获得数据就访问 servlet,或者使 MySQL® 数据库承担大量开放连接。

当涉及 XML 时,用户经常在字段中填充错误的数据,或试图使用仍然无效的数据跳过验证。如果您的程序使用另外一个公司的 XML,则发生错误的可能性涉及多方面。现在您信任另外一家公司,为保证数据格式的每个细节都正确,他们的程序员和您们一样在超负荷工作。在这些情况下(普遍且各种各样),XML 解析将中断,并抛出一个不明确的异常。将功能打包到这样的块中:

try {
  // some interesting and complex XML processing code
} catch (Exception e) {
  System.err.println(e.getMessage());
}

这样的错误处理方式绝对不能接受!使用这样的代码将会惹恼用户、激怒老板,如果 CEO 收到大客户谴责的邮件,可能会惹火身边那些不得不加班查找问题的人。

首先,实际上您已经编写了错误处理代码。不精确地讲,包含 System.err.println() 语句的 catch 块可以被认为是错误处理,这是一个非常拙劣的方法。错误处理代码不仅仅是一个错误报告 — 高质量的错误处理应该是:

  • 用户友好性。
  • 无中断(除非必须如此)。
  • 具有告知性。

错误处理是用户友好的

最重要的是,错误处理代码针对的是应用程序的用户。实际上,程序最终都是为用户服务的。甚至您自己的调试语句都可以帮助您了解发生的问题,这样就可以修复功能。功能是为用户服务的。错误处理代码同样也是。

当然,“用户” 这个词可以具有多种不同的含义,尤其是如果不编写面向用户的代码的话。如果您的应用系统是在公司和银行之间传输财务数据的后端系统,那么,您的用户可能是一些公司的内部部门或银行。如果代码仅仅是供另外一个部门使用的基础代码,那么,这个部门就是您的用户。所以,首先要确定的是,谁是您的用户?

一旦确定您的客户是新泽西州的电脑用户,还是三楼的 Web 开发人员,或是纽约证券交易所的主席,就可以编写对那些用户(或用户类)友好的代码。对于消费者,需要提供不涉及编程术语的、易读的错误消息;对于网站开发人员,需要提供本部门或系统管理小组的联系方式;对于银行的 CEO,错误处理最好不要中断他们的工作。实际上,在过多考虑错误消息之前,最好认识到并不是所有的错误处理代码都必须报告 错误。

错误处理是不中断的(除非必须如此)

如果在您开车上班的路上,遇到一个大型的施工项目阻塞了交通,您不用把车靠到路边,熄火,在心里思考如何修饰自己的简历,因为您担心这次迟到而被解雇。这是很愚蠢的。您可以找到下一个出口并确定备用路线。这也许会花去一些额外的时间,甚至要打电话告知某人您将不能按时参加早上 8 点的会议。但是尽管受到施工问题的影响,您仍然找到了解决方法。

最好的错误处理方式和您遇到问题的情况完全 相同:试图找到避开问题的方法。程序中断,并弹出 “对不起,您运气不好” 的消息或打印出堆栈跟踪,这不是 错误处理,而是错误报告。作为程序员,您要做的是即使有问题存在,也要全力保证用户正常工作。

在 XML 的世界中,这意味着获取那些可能产生错误的数据并对其进行处理。有时,实际上您能够忽略某些低级的错误,或保证不使整个程序崩溃的前提下,将消息写入一个文件。而有时您可能需要向程序用户(可以是另一个程序而不是人类用户)请求更多或替代信息。在这些情况中,设法维持并继续处理。

只有真正出现灾难的情况下才可以完全中断用户的工作。如果一个文件完全丢失了,数据已经篡改而无法修复,或在 XML 文档中缺少必需元素,这时可以中断用户的工作。这就好比 4 或 5 条路被洪水淹没,小路也被完全淹没的情况。换句话说,除非遇到真正的灾难,否则不要完全中断。

这就是 SAX 的真正用途所在:在程序中断处理过程并进行修复之前,允许接收可能错误的数据或者错误状态。

错误处理要具有告知性

当错误发生时,无论采取什么方法处理,都必须提供有用的信息。在最好的情况下,这个信息是程序的用户最初需要的、请求的或想要生成的。如果不能进行很好的恢复,而且必须改变处理过程,那么需要标明下一步要做什么。对于那些罕见的必须完全中断处理过程的情况,需要提供更加有用的信息。

但是关键问题是必须要了解您的用户。如果您的程序是一个通过其他处理组件调用的企业对企业(business-to-business)组件,日志文件的堆栈跟踪和技术性错误消息则非常有帮助。这为利用程序进行交互的程序员提供了详细信息。然而,如果您的程序是面向用户的 Web 应用程序的一部分,您不能 抛出堆栈跟踪和错误代码。相反,需要提供人类可读的(这与程序员易读性不同)消息以及与谁联系得到更多帮助的信息。如果这些信息您并不知道,那么确保抛出一个提供有用信息的异常,以便调用程序能够(但愿)做出类似的智能决策。

SAX 支持大多数的 XML 处理

优秀的基于 XML 的错误处理的关键问题在很大程度上与 SAX 有关。并不是因为 SAX 天生比其他 API 有优势,也不是只有它适合错误处理 — SAX 是关键要素,仅仅因为几乎所有的 XML 处理都在一定程度上与 SAX 有关。

原因很简单:SAX 得到广泛应用已有很长的时间,并在迅速传播。对 XML 进行操作相当容易,但从很多方面来看,它并不是一种直观的语言。XML 有很多结构和奇怪的语法习惯,很难进行解析。对于多数的解析器和处理器供应商来说,构建一个自定义的解析器 API 来处理 XML(从文本数据到元素,再到名称空间、实体引用),这种想法完全不可取或者非常难以实现。这些供应商(甚至 API 作者)转而使用 SAX,因为 SAX 表现相当优秀。— SAX 在很多方面并不擅长,但在 XML 解析方面表现非常突出。因此,如果了解了在 SAX 中如何处理错误,那么就会学会如何在任意 XML 处理 API 中处理错误。

现在,了解一些 SAX 基础知识(附有代码示例)。第一步是从您使用的处理 API 转向使用 SAX。

使用 SAX 进行 SAX 解析(显而易见)

如果您已经是一个 SAX 老手,那么就不用再从头熟悉 SAX,您已经使用过这个 API 了。明确地说,您也许正编写与清单 1 相似的代码,使用 SAX 解析 XML 文档的程序的一个片段。

清单 1. 使用 SAX 解析 XML 文档
XMLReader reader = XMLReaderFactory.createXMLReader();
ContentHandler handler = new PrintingContentHandler();
reader.setContentHandler(handler);
reader.parse(new InputSource(new FileReader(xmlFilename)));

如果使用这样的代码,那么在 XML 处理中就可以很好地处理错误。如果您已经了解如何通过 SAX 或其他 API 访问 XMLReader 实现,那么就朝着成功的错误处理迈进了第一大步。

DOM 一般也使用 SAX

很多 DOM 解析器实际上是 SAX 解析器建立了一个 DOM 树。DOM API 本身并不公开底层 SAX 解析器,因为 DOM API 主要是处理一个 DOM 树,而不是生成树的结构。很多实现 DOM 的解析器至少提供一个供应商专用的方法来访问一个底层 SAX 解析器。

例如,在 Xerces 中,构建 DOM 树的类称为 org.apache.xerces.parsers.DOMParser。可以调用该对象的 parse() 方法,以 Document 对象的形式获得一个 DOM 树,如清单 2 中的代码段所示。

清单 2. 使用 SAX 创建一个 DOM 树
DOMParser parser = new DOMParser();
parser.parse(new InputSource(xmlFilename));
Document doc = parser.getDocument();

乍一看,这与 SAX 中的解析过程相似。实际上,InputSource 类(将文档导入 DOMParser 实例的方法之一)是一个 SAX 结构。但是更凑巧的是,如果打开 Xerces-J API 文档或跟踪源代码,将会注意到 DOMParser 扩展了另外一个 Xerces 类 org.apache.xerces.framework.XMLParser。该类成为了 DOMParser Xerces 中的 SAX 解析类 SAXParser 的基础。

以上的描述是令人困惑的,但是基本意思是:XML 解析的实现为 Xerces-J 中的 SAX 解析服务,也是 DOM 解析类的基础。所以,虽然不能将 DOMParser 类追溯到 SAX 的 XMLReader,但支撑 Xerces-J 的 XMLReader 实现的代码同样支持 DOMParser。因此,有很多有价值的方法可用于 DOMParser

  • setEntityResolver():该方法使用一个 SAX 结构 —— EntityResolver,用来处理 XML 文档中的实体。
  • setFeature():这是另外一个起源于 SAX 的方法,允许设置解析器的与 DOM 相关的 与 SAX 相关的特性。
  • setErrorHandler():这是错误处理的关键方法。该方法接收一个 SAX ErrorHandler 实现,允许截取和响应错误。

即使不能直接访问 SAX XMLReader 实现,也可获得一些与 SAX 有关的方法,它们是本文中错误处理的核心方法。

JAXP 也采用 SAX

很多开发人员不会笨到直接使用 SAX 或 DOM。他们使用 JAXP API(Java API for XML Processing)。清单 3 中的代码段使用 JAXP 进行 SAX 解析。

清单 3. 使用 JAXP 进行 SAX 解析
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setValidating(true);
factory.setNamespaceAware(false);
SAXParser parser = factory.newSAXParser();
parser.parse(new File(args[0]), new MyHandler());

这些代码看起来是很熟悉,它与 清单 1 中的 SAX 解析几乎相同,尽管类名有所不同,许多选项是固定的。但是,这里可以使用这些 JAXP 结构,而且更接近 SAX。具体来讲,JAXP SAXParser 类提供了一种叫做 getXMLReader() 的方法,可以返回底层 SAX XMLReader 实现:

SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setValidating(true);
factory.setNamespaceAware(false);
SAXParser parser = factory.newSAXParser();
XMLReader reader = parser.getXMLReader();

接下来可以像处理一个简单 SAX 应用程序一样处理 XMLReader 实例,并设置一个错误处理程序。

使用 JAXP 进行 DOM 解析时也同样如此,提供了一种更为简便的 DOM 到 SAX 的 API 分层。清单 4 中的代码段说明了一般的 DOM 解析过程。

清单 4. 使用 JAXP 进行 DOM 解析
DocumentBuilderFactory factory = 
  DocumentBuilderFactory.newInstance();

factory.setValidating(true);
factory.setNamespaceAware(false);

DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new File(args[0]));

虽然这是基于 DOM 的解析,但是转换为 SAX 非常容易。实际上,在考虑将代码由 DOM 转换为 SAX 之前,首先要考虑更基本的事实:如果使用 JAXP,就已经获取 SAX 类(参见 清单 3)。所以,如果使用 JAXP,则不用考虑将 DOM 转换为 SAX 的问题(参考资料 中列出的 developerWorks 技巧提供了详细的说明)。只需要重写一些代码,使用 SAXParser 代替 DOMBuilder 即可,这样就可以开始添加与 SAX 有关的错误处理了。

更高级的 API(如 JAXB)同样使用 SAX

至此,应该可以得出一些 SAX 广泛应用的结论。SAX 可用于 DOM 处理、JAXP 解析。那么顺理成章地,使用 DOM 的高层 API 或模型 JAXP 也能够使用 SAX。当然对于这些 API 需要做一些处理,但是可以从中得到一个基础的 SAX。

另外一个满足该模式的流行 API 是 Sun 公司用于 XML 绑定的 Java API(JAXB)(或架构,取决于所使用的版本)。JAXB 允许将一个 XML 文档转换为一个 Java 对象模型,然后将对象模型再转换回 XML。将 XML 转换到 Java 的过程叫做解组,可分为两步:

  1. 解析 XML 文档,保存文档中的数据、元素和属性的名称,以及元素、属性和数据之间的关系。
  2. 将数据转换为成员变量和 Java 对象模型的实例。

在解析的过程中,可能会产生错误,因此必然会用到 SAX。JAXB 就是这样,并且会间接得到一个 SAX XMLReader,清单 5 展示如何获得用于解组的核心类 Unmarshaller,并实现对基于 SAX 的解析过程的细粒度控制。

清单 5. 从 JAXB 中获得 SAX XMLReader
JAXBContext context = JAXBContext.newInstance("dw.ibm");
Unmarshaller unmarshaller = context.createUnmarshaller();

// Get the lower level handler from the Unmarshaller
UnmarshallerHandler unmarshallerHandler = 
  unmarshaller.getUnmarshallerHandler();

// Now use JAXP to get a SAX parser
SAXParserFactory factory = SAXParserFactory.newInstance();

// Set options on the factory, using standard JAXP calls
factory.setNamespaceAware(true);
 
XMLReader reader = factory.newSAXParser().getXMLReader();

// We can use the handler from the unmarshaller as the content handler
reader.setContentHandler(unmarshallerHandler);

// Now parse, using the unmarshalling handler from JAXB
reader.parse(new InputSource(new FileReader(xmlDocument)));

MyCustomObject topObject = (MyCustomObject)unmarshallerHandler.getResult();

这个例子要比前面的例子更复杂一点,但是原理仍然非常简单。最大的不同之处在于没有直接使用 JAXB 框架,而是通过 Unmarshaller.unmarshal() 方法得到 JAXB 的处理程序,其中包含所有的解组代码。下面是代码实现的功能:

// Get the lower level handler from the Unmarshaller
UnmarshallerHandler unmarshallerHandler = 
  unmarshaller.getUnmarshallerHandler();

然后,使用返回的处理程序手动处理 SAX 解析,对内容进行处理。这里可以插入自定义的错误处理代码 —下一节 将详细讨论。

最后,进行解析,通过这个调用返回到 JAXB 框架中:

MyCustomObject topObject = 
  (MyCustomObject)unmarshallerHandler.getResult();

当进行解组时,可以实现严格的细粒度控制。这样您可以积累经验、避免或顺利报告错误,为应用程序用户提供更好的经验。

其他的 API 同样使用 SAX

显然,不可能涵盖所有 XML API 并详细讨论从 API 的最高级应用到基础 SAX 等众多内容,但是现在您应该已经较好地了解了所需的模式分类。检查 API 文档,查找扩展或实现 org.xml.sax.XMLReader、或将参数作为 SAX ErrorHandler(在 org.xml.sax 包中)的类。甚至可以使用 Google 搜索 “[您的 API 名称] SAX” 或 “[您的 API 名称] ErrorHandler”,将会发现将当前使用的 API 和 SAX 建立联系是多么容易!

使用 ErrorHandler 接口处理错误

SAX 错误处理的核心部分是 org.xml.sax.ErrorHandler 接口。它是一个简单的、包含三种方法的接口,可以实现和处理所有类型的错误。它可以很容易地注册 SAX,处理错误时只需要几行代码。

针对三种错误类型的三种处理方法

ErrorHandler 接口的代码极其简单。清单 6 显示了全部内容(不包括注释)。

清单 6. SAX ErrorHandler 接口
package org.xml.sax;

public interface ErrorHandler {
  public void warning(SAXParseException exception) throws Exception {
  }

  public void error(SAXParseException exception) throws Exception {
  }

  public void fatalError(SAXParseException exception) throws Exception {
  }
}

以上就是错误处理的三种方法。在解析过程中的错误可以传递给其中的任何一个方法。如果想要实现自己的 ErrorHandler,可以自定义错误处理。

忽略警告

根据 XML 1.0 推荐标准定义,SAX 中的警告 被定义为不属于错误或致命错误的问题。这样的定义是很模糊的。但是有一种更好的规定方法 —警告表示不会阻止解析器继续解析的问题。对于警告,通常默认的处理方式是完全忽略,因为它不会阻止解析和处理过程;或者是弹出一个通知消息并继续解析。

与注释有关的问题,能够继续处理的异常值,以及很多意想不到的小缺陷。在过多考虑警告概念之前,先回忆一下前面提到的高质量的错误处理的原则:

  • 错误处理要具有用户友好性。
  • 错误处理是不中断的(除非必须如此)。
  • 错误处理要具有告知性。

对警告应用任何一个(或全部)原则,将很快发现忽略警告的默认处理方式可能是最好的方法。如果想在调试应用程序或日志文件中记录信息,这样处理是非常合适的。即使在当时,您花费宝贵的处理时间处理不重要的任务。warning() 方法的最优实现如清单 7 所示。

清单 7. 一个添加了 warning() 方法的 ErrorHandler 实现
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

public class DefaultErrorHandler implements ErrorHandler {
  public void warning(SAXParseException exception) 
    throws SAXException {

    // Do nothing
  }

  public void error(SAXParseException exception) throws SAXException {
    // to be filled in later
  }

  public void fatalError(SAXParseException exception) 
    throws SAXException {

    // to be filled in later
  }
}

设法从错误中恢复

错误是最难处理的 XML 问题。警告可以忽略或写入日志。致命错误(后面将详细讨论)需要停止解析,并采取大量操作进行处理。错误(与致命错误不同)是一个模糊的中间问题。SAX 中的错误与 XML 1.0 规范中的错误类似,被模糊地描述为:

破坏规范的规则,结果是不确定的。除非另有规定,如果不能遵守 “一定、必需、绝不是,应该,不应该” 这些关键词标明的规范的规定,那么算作一个错误。标准软件可以发现并报告错误,而且可以进行恢复。

可惜,描述中没有指出什么会导致 错误。这里特别值得注意的一个词就是 “恢复”。换句话说,程序应该 能够从这类错误中恢复,而不是停止解析和处理过程。

实际应用中,当 XML 内容(而不是 格式或结构)出现了意想不到的问题就会报告一个错误。因此,当真正发生错误时,就表示您可能得到一个不完整的文档或解析文档中的数据可能丢失、篡改或错误。

在报告错误时不能进行恢复。SAX 不允许提前或延后读取,所以很难(多数情况下)从以往的 SAXParseException 中收集上下文。但是好的一面是不用停止处理。所以不用担心能够 做什么,最重要的是不需要 做什么 — 停止处理。您的用户,不管是用户还是其他开发人员 — 不会看到出现差错,多数情况下会从程序中得到有用的结果。

那么,您需要怎样做呢?首先应该将错误信息进行记录。虽然不想让用户知道他们可能会收到有缺陷的数据,但是也要记录一些事件的信息。避免使用 System.outSystem.err,要将其记录到一个文件或者使用一种类似 Log4J (参见 参考资料 获得链接)的 API。这样可以很好地跟踪事件,允许您或其他人预防问题再次发生。

在使用 error() 消息处理错误时的另外一个必要步骤是使用您的应用程序的业务逻辑制定明智的决策。例如,假设应用程序员使用 contact-info 元素中嵌套的 email 元素来存储电子邮件。如果 error() 方法表示出现的问题与 contact-infoemail 元素有关,您可能会丢失邮件地址文本数据。您必须通知(通过一个不会产生中断的方法)调用程序,一个邮件地址将要丢失。如果编写的是一个面向用户的应用程序,而且信息来自于用户输入,您需要通过一个简单的 Web 表单,要求用户重新输入他们的电子邮件地址。

讨论错误处理的最大的难题是给出具体示例。这些例子完全取决于业务逻辑和域。所以您必须自己提出实现 error() 的方法。关键是不使程序崩溃或抛出异常而中止处理。一定要记住,根据定义,错误(与致命错误相反)不会导致处理中止。

报告致命错误

SAX 中问题的最后一种类型是致命错误。根据 XML 1.0 规范的定义,致命错误绝对会干扰和阻止解析过程继续进行。最常见的例子是缺乏良好格式的文档。换句话说,firstName 元素总是缺少结束标识,或缺少开头的尖括号。这些情况下,解析器不能恢复,因为文档的整个结构都有问题。SAX API 文档甚至提出,只要报告一个致命错误,应用程序必须假定文档是不可用的。所以致命错误是非常大的问题。

乍一看,似乎可以鉴别致命错误。例如,如果缺少 </firstName>,需要根据就近的内容进行猜测查找。可能有一个奇怪的闭合元素叫做 “girstName”,这可能是一个输入错误。所以,很多致命错误通过查找可以修复。

然而,问题是 SAX 是一个只读的、连续的解析器。它不能提前读,也不能记录读过的内容。所以基本上不可能预测一个潜在的输入错误或返回查看读过的内容。所以,必须要编写一个相当于内存缓冲器的处理程序来记录已读的内容,也同时需要编写一些代码进行提前读。这种开销是巨大的,并且在最好的情况下,您仍然不明白最初的文档作者的意图。所以,致命错误归于第二个优秀错误处理原则:错误处理是不中断的,除非有必要。

下面是错误处理的第三个原则:错误处理要有告知性。对于真正的中断,错误处理要有告知性。这里给出一个例子说明不要 做什么(不存在代码清单,因此人们不会将它误认为是一个良好实践):

  public void fatalError(SAXParseException exception) 
    throws SAXException {

    // typical, but terrible, error handling

    // Bring things to a crashing halt
    System.out.println("**Parsing Fatal Error**" + "\n" +
                       "  Line:    " + 
                          exception.getLineNumber() + "\n" +
                       "  URI:     " + 
                          exception.getSystemId() + "\n" +
                       "  Message: " + 
                          exception.getMessage());        
    throw new SAXException("Fatal Error encountered");
  }

这对于程序员来说可能是具有告知性的,但是会让其他人很费解。您要使用自己的方法来处理这些异常,因为每个应用程序都是不同的。不管怎样,必须为调用程序以 SAXException 的形式返回信息,因此有很多的限制。例如,您可能不希望直接控制用户体验,而只是想返回给一个调用类,并使用它为用户提供反馈。

通过自定义的异常类传递有用的错误

为程序提供有意义的错误信息的最简单方法是,自己创建异常类并传递给 SAXExceptionSAXException 异常表示 ErrorHandler 的三种方法都可抛出,而且从根本上来说,它是与调用程序通信的惟一途径。默认情况下,SAXException 提供了以下几种方法:

  • getException():该方法返回一个 Exception,允许在其中嵌套用于后期恢复的异常,可以将自定义的异常填充在这里,为调用程序传递信息。
  • getMessage():可以在这里放置易于理解的消息,但是这是一个传送良好错误信息的相当原始的方法。
  • getString():它重写了超类的 getString() 方法,打印出嵌套的异常信息。

假设您准备了一个在 Web 程序中使用的 XML 处理组件。可以定义一个如清单 8 所示的自定义类。

清单8. 向 Web 应用程序提供报告的自定义异常
public class WebException extends Exception {
  private int httpStatusCode = 400;
  private String redirectURL;

  public WebException(String message, int httpStatusCode, String redirectURL) {
    super(message);
    this.httpStatusCode = httpStatusCode;
    this.redirectURL = redirectURL;
  }

  public WebException(String message, Throwable cause, 
                      int httpStatusCode, String redirectURL) {
    super(message, cause);
    this.httpStatusCode = httpStatusCode;
    this.redirectURL = redirectURL;
  }

  public int getHttpStatusCode() {
    return httpStatusCode;
  }

  public String getRedirectURL() {
    return redirectURL;
  }
}

这是非常基本的操作,但是需要一些与 Web 有关的信息:HTTP 状态码(比如 404 或 401)和重定向用户的 URL。在错误处理中可以使用这些信息,如清单 9 所示。

清单 9. 报告一个嵌套自定义异常的异常
  public void fatalError(SAXParseException exception) throws SAXException {
    //Report through a Web-specific exception
    WebException webException = new WebException("There was a problem converting your " +
      "response into a format our server could read. Please contact our customer " +
      "service team at 1-800-555-0972, and we'd love to help you in person.", 
      exception, 406, "/errorPages/xmlError.php?reason=" + exception.getMessage());
    throw new SAXException(webException);
  }

代码并不复杂,但实现了几个重要的功能:

  1. 返回的错误信息是特定的、用户可读的和告知性的。它是为一般用户准备的,而不是程序员。
  2. 为调用应用程序提供了特定于应用程序的信息,比如状态码和出错页面。通过这样的方法,可以清楚了解要执行的操作,并并且在出现问题时,调用 XML 处理组件的代码可以获知信息,而不用费劲地进行堆栈跟踪。
  3. 重定向页面也提供了有用的信息:基本的错误消息可能被记入日志,甚至自动为程序员生成 bug 报告,用于后续处理。

所有这一切都只需几行自定义异常代码并思考如何实现 fatalError()

根据应用程序调整异常

很明显,WebException 不会为不基于 Web 的应用程序提供服务。实际上,它甚至也不适合基于 Web 的应用程序。此时便需要用到领域级和主题级别的知识。需要了解您的 应用程序的需求。

您需要了解当前的用户和他们需要的信息种类。即使编写一个低层处理组件,也需要了解当突然发生问题时,调用代码可能需要什么。构建自己的特定于应用程序的异常,并通过 SAXException 在错误处理方法中将这个异常打包。通过自定义的异常传递与所发生问题相关或有用的信息。

如果没有创建一个面向用户的组件,下一步是联系(通过电话或电子邮件、或是书面文档)使用您的代码的开发人员。因此,如果内部网的 Java servlet 人员使用您的代码,要告知他们在 SAXException 中有一个包含很多有用信息的自定义的异常包。将异常的源代码、一些基本文档和使用建议发送给他们,通过交流可以阻止他们抛弃 getMessage() 方法,而在错误处理方面犯错。

不要扩展 SAXException!

一个常见的错误是扩展 SAXException,而不是在该类中嵌套一个自定义异常。所以,在前面的例子中(清单 8 和清单 9),WebException 可以扩展 SAXException,并在 fatalError() 中抛出。这看起来是可行的,但会有一些麻烦的问题。

首先,扩展 SAXException 就将自定义异常绑定到 SAX。这使异常在那些没有 XML 解析、处理或 SAX 功能的组件中是不可用的。最好保持异常在整个应用程序内都具有通用性和可用性。这就是为什么 SAXException 允许另外一个不相关的异常在其中嵌套。其次同样重要的是,在很多使用 SAX 的现有应用程序中已经自动获得嵌套异常的信息,所以按设计目的使用 SAXException,可以令代码更好地处理其他现有代码组件。

注册 ErrorHandler 实现

一旦实现了 ErrorHandler 中的三种方法,就可以进行错误处理了。剩下的惟一步骤是将错误处理实现注册 到解析过程。这是非常简单,但很关键的步骤。创建大量的错误处理方法但又不告知 SAX 解析器,这样是没有益处的。

对 XMLReader 调用 setErrorHandler() 方法

如果想从 XML API 或处理层中获取一个 XMLReader ,这是很简单的。XMLReader 提供 setErrorHandler() 方法将 ErrorHandler 的实现作为参数,可以像清单 10 那样调用它。

清单 10. 在 XMLReader 中设置一个错误处理程序
JAXBContext context = JAXBContext.newInstance("dw.ibm");
Unmarshaller unmarshaller = context.createUnmarshaller();

// Get the lower level handler from the Unmarshaller
UnmarshallerHandler unmarshallerHandler = 
  unmarshaller.getUnmarshallerHandler();

// Now use JAXP to get a SAX parser
SAXParserFactory factory = SAXParserFactory.newInstance();

// Set options on the factory, using standard JAXP calls
factory.setNamespaceAware(true);
 
XMLReader reader = factory.newSAXParser().getXMLReader();

// We can use the handler from the unmarshaller as the content handler
reader.setContentHandler(unmarshallerHandler);

// Register a custom ErrorHandler implementation
reader.setErrorHandler(new MyJAXBErrorHandler("/logs/logfile.txt"));

//Now parse, using the unmarshalling handler from JAXB
reader.parse(new InputSource(new FileReader(xmlDocument)));

MyCustomObject topObject = (MyCustomObject)unmarshallerHandler.getResult();

清单 10 中,更高级的 API 是 JAXB(在 清单 5 中第一次出现)。一旦获得 XMLReader 实例,就可以通过 setErrorHandler() 方法注册一个新的自定义的 ErrorHandler 实现。在本例中,自定义的处理程序叫做 MyJAXBErrorHandler。该程序将记录错误的文件的路径作为参数。完成之后,解析(通过 parse())便开始了,任何问题都会传递给 MyJAXBErrorHandler 中的方法。

可以使用 getErrorHandler 检查当前的 ErrorHandler

可以查看您的 XMLReader 使用的 ErrorHandler 的实现。只要调用 getErrorHandler() 方法,就会得到当前注册的 ErrorHandler 实现。然后可以处理任何类或打印出它的相关信息。请参阅清单 11 中的简单示例。

清单 11. 检查当前的错误处理程序
JAXBContext context = JAXBContext.newInstance("dw.ibm");
Unmarshaller unmarshaller = context.createUnmarshaller();

// Get the lower level handler from the Unmarshaller
UnmarshallerHandler unmarshallerHandler = 
  unmarshaller.getUnmarshallerHandler();

// Now use JAXP to get a SAX parser
SAXParserFactory factory = SAXParserFactory.newInstance();

// Set options on the factory, using standard JAXP calls
factory.setNamespaceAware(true);
 
XMLReader reader = factory.newSAXParser().getXMLReader();

// We can use the handler from the unmarshaller as the content handler
reader.setContentHandler(unmarshallerHandler);

// See what's handling errors right now
ErrorHandler handler = reader.getErrorHandler();
System.out.println("Error handler is currently: " + 
  handler.getClass().getName());

// Now parse, using the unmarshalling handler from JAXB
reader.parse(new InputSource(new FileReader(xmlDocument)));

MyCustomObject topObject = (MyCustomObject)unmarshallerHandler.getResult();

它只具有信息价值,因为对那个类不能采取任何操作。然而,如果对您所钟爱的一些 XML API 如何处理信息感到好奇,这是一个找到答案的好办法。

结束语

错误处理被认为是应用程序开发的重要部分。与应用程序或网站的其他组件相比,许多用户都对一个程序的错误 — 以及这些错误是如何处理的 — 记忆尤深。提供优秀的特性固然不错,但是糟糕的错误处理非常令人讨厌。这听起来很简单,但是用 10% 的额外时间来轻松处理错误或阻止错误发生将极大地改进用户体验。

当涉及处理 XML 解析和处理错误,关键问题不只是一个特定的 SAX 接口,还需要理解是什么驱动 XML 处理。只要意识到 SAX 是多数 XML 处理的基础,那么将会明白 SAX 是高质量错误处理的核心。如果五年之后,另外一个 XML 解析 API 取代了 SAX,您就会明白 API 也可以进行处理了。访问 SAX XMLReader 很简单,但是了解可以用这个接口做什么就很难了。实际上这就是错误处理的关键:了解所使用的系统和系统的底层。您不需要使用汇编语言操作入栈和出栈,但是需要知道 SAX 在如今是关键的 XML 解析 API。

错误处理成为一个有关实现和执行的问题。在 SAX 中,可以使用 XMLReaderErrorHandler 接口来接收错误信息。可以立即处理这些错误,或把它们传送给一个调用程序,或将它们与附加信息打包到自定义对象中,或采取任何对于您和应用程序有意义的操作。有很多错误处理的原则,但是最关键的就是不要惹恼您的用户。如果您能很好处理这个问题,将会战胜您的同行和竞争者。

直接面对您的错误,将它们变为优势而不是对用户造成破坏,您听到的抱怨会更少,会建立一个更愉快的经理和用户基础。告诉我您找到了哪些有趣的解决错误的方法,并加入 developerWorks 论坛(请参阅 参考资料 获得链接)中的专家在线,讲述您如何使自己的程序避免灾难。


相关主题

  • 您可以参考本文在 developerWorks 全球站点上的 英文原文
  • JAXP 全部内容:阅读 JAXP 的详细内容。
  • XML 1.0 规范:了解什么是错误、致命错误以及其他类型问题(如警告)。
  • Tip: Converting from DOM(Brett McLaughlin,developerWorks,2001 年 4 月):在这个有用的技巧中,学习将 DOM 结构转换为 SAX 和 JDOM,允许与不使用 DOM 的应用程序进行通信。
  • 了解有关本文讨论的 API 的更多内容。从 SAX 网站的 SAX 2 for Java 开始,然后浏览 W3C 网站的 DOM
  • Java 5 或更新的 Java 6 软件:如果刚接触 Java 编程,利用这些下载结合使用 JAXP 和完整的 JDK。
  • Log4J:这是一个开源项目,是基于 IBM 的 API。允许轻松地将日志写到各种文件中,包括文本文件和网络资源。日志记录是构成出色错误处理应用程序的重要部分。
  • Java 和 XML, 第三版(Brett McLaughlin 和 Justin Edelson,O'Reilly Media, Inc.):全文主要讨论 XML,包括有关 XML、XSL、相关 XML 规范的大量信息。
  • XML 技术资源库:访问 developerWorks XML 专区,获得广泛的技术文章和技巧、教程、标准和 IBM 红皮书。

评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML, Java technology
ArticleID=341689
ArticleTitle=处理 XML 解析中的错误
publish-date=09252008