这是篇细探 JAXP,Sun 的 Java API for XML 的文章,帮助解除了有关 JAXP 本质和服务目的的疑惑。本文讲解了 JAXP 的基本概念,演示 XML 语法分析为什么需要 JAXP,并显示如何轻易更改 JAXP 使用的语法分析器。本文还进一步讲述了 SAX 和 DOM 这两个流行的与 JAXP 相关的 Java 和 XML API。
Java 和 XML 在每一个技术领域都制造了新闻,并且对于软件开发人员来说,似乎是 1999 年和 2000 年最重要的发展。结果,Java 和 XML API 的数量激增。其中两个最流行的 DOM 和 SAX 还引起极大兴趣,而 JDOM 和数据绑定 API 也接踵而来。只透彻理解这些技术中的一个或两个就是一项艰巨任务,而正确使用所有这些技术就会使您成为专家。但在去年,另一个 API 给人留下了深刻印象,它就是 Sun 的 Java API for XML,通常称为 JAXP。如果考虑到 Sun 在其平台上还没有任何特定于 XML 的产品,那么这个进展就不足为奇。而令人惊奇的是人们对 JAXP 了解的缺乏。多数使用它的开发人员在他们所用的这个 API 的概念理解上都有错误。
什么是 JAXP?
本文假设您有 SAX 和 DOM 的基本知识。这里实在没有足够篇幅来解释 SAX、DOM 和 JAXP。如果您是 XML 语法分析的新手,那么可能要通过联机资源阅读 SAX 和 DOM,或者浏览我的书。( 参考资源 一节中有至 API 和我的书的链接。)获得基本知识后再看本文会比较好。
API 还是抽象?
在讲解代码之前,介绍一些基本概念很重要。严格地说,JAXP 是 API,但是将其称为抽象层更准确。它不提供处理 XML 的新方式,不补充 SAX 或 DOM,也不向 Java 和 XML 处理提供新功能。(如果在这点上理解有误,则本文正好适合您!)它只是使通过 DOM 和 SAX 处理一些困难任务更容易。如果在使用 DOM 和 SAX API 时遇到特定于供应商的任务,它还使通过独立于供应商的方式处理这些任务成为可能。
虽然要分别讲述所有这些特性,但是真正需要掌握的是:JAXP 不提供语法分析功能 !没有 SAX、DOM 或另一个 XML 语法分析 API,就 无法分析 XML 语法 。有很多人曾让我将 DOM、SAX 或 JDOM 与 JAXP 进行对比。但进行这些对比是不可能的,因为前三个 API 与 JAXP 的目的完全不同。SAX、DOM 和 JDOM 都分析 XML 语法。而 JAXP 却提供到达这些语法分析器和结果的方式。它自身不提供分析文档语法的新方法。如果要正确使用 JAXP,则一定要弄清这点。这将使您比其它 XML 开发人员领先一大截。
如果仍然怀疑(或认为我故弄玄虚),请从 Sun 的 Web 站点下载 JAXP 分发(请参阅 参考资料 一节),然后就会知道基本 JAXP 是什么。在包括的 jar ( jaxp.jar ) 中 只有六个类 !这个 API 会有多难哪?所有这些类( javax.xml.parsers 包的一部分)都位于现有语法分析器之上。这些类中的两个还用于错误处理。JAXP 比人们想象的要简单得多。那么,为什么还感到困惑哪?
Sun 的 JAXP 和 Sun 的语法分析器
JAXP 下载时包括 Sun 的语法分析器。所有 parser 器类作为 com.sun.xml.parser 包和相关子包的一部分位于 parser.jar 档案中。应该知道,该语法分析器(代码名为 Crimson) 不 是 JAXP 自身的一部分。它是 JAXP 版本的一部分,但不是 JAXP API 的一部分。令人困惑吗?有一点。换这种方式想想:JDOM 与 Apache Xerces 语法分析器一起提供。该语法分析器不是 JDOM 的一部分,但由 JDOM 使用,所以包括它,以确保 JDOM 可以单独使用。JAXP 也是如此,但不象 JDOM 那样好表达:JAXP 与 Sun 的语法分析器一起提供,以便可以立即使用。但是,很多人将 Sun 的语法分析器中包括的类当成 JAXP API 的一部分。例如,新闻组中一个常见的问题是:“怎样使用 JAXP 中的 XMLDocument 类?其目的是什么?”这个答案可有些复杂。
首先, com.sun.xml.tree.XMLDocument 类不是 JAXP 的一部分。它是 Sun 语法分析器的一部分。所以,这个问题从一开始就给人以误导。其次,JAXP 的整个意义在于在处理语法分析器时提供供应商独立性。使用 JAXP 的同一代码可以与 Sun 的 XML 语法分析器、Apache 的 Xerces XML 语法分析器和 Oracle 的 XML 语法分析器一起使用。而使用特定于 Sun 的类是个坏主意。这与 JAXP 的整个意义相背离。现在看出来这个问题怎样混淆概念了吗?语法分析器和 JAXP 发行版本(至少是 Sun 的版本)中的 API 被混为一谈,开发人员将其中一个的类和特性当成是另一个的了,反之亦然。
旧和新
关于 JAXP,最后需要指出的是:使用 JAXP 有一些缺陷。例如,JAXP 只支持 SAX 1.0 和 DOM 第一层规范。SAX 2.0 从 2000 年 5 月起就完成,DOM 第二层规范支持甚至在大多数语法分析器中存在更长时间。DOM 第二层规范还没有完成,但确实足够稳定以用于生产。这两个 API 的新版本都有重大改进,最明显的是对 XML 名称空间的支持。该支持还允许“XML Schema 确认”,这个与 XML 相关的另一热门技术。公平地说,当 JAXP 发布 1.0 最终发行版时,SAX 2.0 和 DOM 第一层规范都还没有完成。但是,由于没有包括这些新版本,确实为开发人员带来很大不便。
还可以使用 JAXP,但是也可以等待 JAXP 1.1,它支持 SAX 2.0 和 DOM第二层规范 。否则,将发现,JAXP 提供的优点以 SAX 和 DOM 最新版本中的功能为代价,并使应用程序更加难以编码。无论是否等待下一个 JAXP 发行版,都要留意这个问题。如果将 JAXP 与语法分析器一起使用,而语法分析器支持的 DOM 和 SAX 版本比 JAXP 支持的要高,则可能会有类路径问题。所以,事先留意一下,并且,一旦有 JAXP 1.1,马上升级。基本理解 JAXP 之后,让我们看一下 JAXP 依赖的 API:SAX 和 DOM。
从 SAX 开始
SAX (Simple API for XML)是用于处理 XML 的事件驱动方法。它基本由许多回调函数组成。例如,每当 SAX 语法分析器遇到元素的开始标记时就调用 startElement() 。对于字符串,将调用 characters() 回调函数,然后在元素结束标记处调用 endElement() 。还有很多回调函数用于文档处理、错误和其它词汇结构。现在知道这是怎么回事了。SAX 程序员实现一个定义这些回调函数的 SAX 接口。SAX 还实现一个名为 HandlerBase 的类,该类实现所有这些回调函数,并提供所有这些回调方法的缺省空实现。(提到这一点是因为它在后面讲到的 DOM 中很重要。)SAX 开发人员只需扩展这个类,然后实现需要插入特定逻辑的方法。所以,SAX 的关键在于为这些不同的回调函数提供代码,然后允许语法分析器在适当的时候触发这些回调函数中的每一个。
因此,典型的 SAX 过程如下:
- 用特定供应商的语法分析器实现创建一个 SAXParser 实例
- 注册回调实现(例如,通过使用扩展 HandlerBase 的类)
- 开始进行语法分析,然后在触发回调实现时等待
JAXP 的 SAX 组件提供执行所有这些步骤的简单方式。如果没有 JAXP,SAX 语法分析器要直接从供应商类(如 org.apache.xerces.parsers.SAXParser )进行实例化,或者必须使用名为 ParserFactory 的帮助类。第一个方法的问题很明显:不独立于供应商。第二个方法的问题在于类厂需要一个自变量,即要使用的语法分析器类的字符串名称(还是那个 Apache 类 org.apache.xerces.parsers.SAXParser )。可以通过将不同语法分析器作为 String 传递来更改语法分析器。使用这种方法不必更改任何 import 语句,但是还是要重新编译类。这显然不是最佳解决方案。如果能够不重新编译类而更改语法分析器,可能会简单得多,是不是这样呢?
JAXP 提供了更好的替代方法:它允许将语法分析器作为 Java 系统属性来提供。当然,当从 Sun 下载版本时,将得到使用 Sun 语法分析器的 JAXP 实现。可以从 Apache XML Web 站点下载在 Apache Xerces 上构建其实现的相同 JAXP 接口。因此(无论哪一种情况),更改正在使用的语法分析器需要更改类路径设置,即从一种语法分析器实现更改到另一个,但是 不要求重新编译代码。这就是 JAXP 的魔力,或抽象性。
SAX 语法分析器一瞥
JAXP SAXParserFactory 类是能够轻易更改语法分析器实现的关键所在。必须创建这个类的新实例(等一会将讲到)。创建新实例之后,类厂提供一个方法来获得支持 SAX 的语法分析器。在内部,JAXP 实现处理依赖于供应商的代码,使您的代码不受影响。这个类厂还提供其它一些优秀特性。
除创建 SAX 语法分析器实例的基本工作之外,类厂还允许设置配置选项。这些选项影响所有通过类厂获得的语法分析器实例。JAXP 1.0 中两个可用的功能是设置名称空间敏感性 ( setNamespaceAware (boolean awareness)),和打开确认 ( setValidating (boolean validating))。请记住,一旦设置了这些选项,在调用该方法之后,它们将影响 所有从 类厂获得的实例。
设置了类厂之后,调用 newSAXParser() 将返回一个随时可用的 JAXP SAXParser 类实例。这个类封装了一个下层的 SAX 语法分析器(SAX 类 org.xml.sax.Parser 的实例)。它还防止向语法分析器类添加任何特定于供应商的附加功能。(还记得以前对 XmlDocument 的讨论吗?)这个类可以开始进行实际的语法分析。以下清单显示如何创建、配置和使用 SAX 类厂。
清单 1. 使用 SAXParserFactory
import java.io.File;import java.io.IOException;import java.io.OutputStreamWriter;import java.io.Writer;// JAXPimport javax.xml.parsers.FactoryConfigurationError;import javax.xml.parsers.ParserConfigurationException;import javax.xml.parsers.SAXParserFactory;import javax.xml.parsers.SAXParser;// SAXimport org.xml.sax.AttributeList;import org.xml.sax.HandlerBase;import org.xml.sax.SAXException;public class TestSAXParsing {public static void main(String[] args) {try {if (args.length != 1) {System.err.println ("Usage: java TestSAXParsing [filename]");System.exit (1);}// 获得SAX 语法分析器类厂SAXParserFactory factory = SAXParserFactory.newInstance();//设置设置名称空间敏感性选项,关掉确认选项factory.setValidating(true);factory.setNamespaceAware(false);SAXParser parser = factory.newSAXParser();parser.parse(new File(args[0]), new MyHandler());} catch (ParserConfigurationException e) {System.out.println("The underlying parser does not support " +" the requested features.");} catch (FactoryConfigurationError e) {System.out.println("Error occurred obtaining SAX Parser Factory.");} catch (Exception e) {e.printStackTrace();}}}class MyHandler extends HandlerBase {//通过 DocumentHandler, ErrorHandler等实现的SAX回调函数}
请注意,在这段代码中,在使用类厂时可能发生两个特定于 JAXP 的问题:无法获得或配置 SAX 类厂,以及无法配置 SAX 语法分析器。当无法获得 JAXP 实现中指定的语法分析器或系统属性时,通常会发生第一个问题 FactoryConfigurationError 。当正在使用的语法分析器中的特性不可用时,会发生第二个问题 ParserConfigurationException 。这两个问题都容易处理,应该不会对 JAXP 的使用造成任何困难。
在获得类厂、关闭名称空间并打开“确认”之后,将获得 SAXParser ,然后开始语法分析。请注意, SAX 语法分析器的 parse() 方法取得前面提到的 SAX HandlerBase 类的一个实例。(可以通过完整的 Java 清单 查看该类的实现 。)还要传递要进行语法分析的文件。但是, SAXParser 所包含的远不止这一个方法。
使用 SAX 语法分析器
获得 SAXParser 类的实例之后,除了向语法分析器传递 File 进行语法分析之外,还可以用它做更多的事。由于如今大型应用中的应用程序组件之间通信方式,“对象实例创建者就是其使用者”这样的假定并不总是安全的。换句话说,一个组件可能创建 SAXParser 实例,而另一组件(可能由另一开发人员编码)可能需要使用那个实例。由于这个原因,提供了一些方法来确定语法分析器的设置。执行此任务的两个方法是 isValidating() ,它通知调用程序:语法分析器将要、或不要执行“确认”,以及 isNamespaceAware() ,它返回一个指示,说明语法分析器可以或不可以处理 XML 文档中的名称空间。虽然这些方法能提供有关语法分析器可以执行功能的信息,但是无法更改这些特性。必须在语法分析器类厂级别执行该操作。
另外,有多种方法来请求对文档进行语法分析。除了只接受 File 和 SAX HandlerBase 实例,SAXParser 的 parse() 方法还能以 String 形式接受 SAX InputSource 、Java InputStream 或 URL,所有这些都要与 HandlerBase 实例一起提供。所以,不同类型的输入文档可以用不同方式的语法分析来处理。
最后,可以直接通过 SAXParser 的 getParser() 方法获得和使用下层的 SAX 语法分析器( org.xml.sax.Parser 的实例)。获得这个下层实例之后,就可以获得通常的 SAX 方法。下一个清单显示 SAXParser 类(这个 JAXP 中 SAX 语法分析的核心类)的各种使用示例。
清单 2. 使用 JAXP SAXParser
//获得SAXP的一个实例SAXParser saxParser = saxFactory.newSAXParser();//查看是否支持 Validate 选项boolean isValidating = saxParser.isValidating();//查看是否支持 namespace 选项boolean isNamespaceAware = saxParser.isNamespaceAware();// 运用一个File 和一个SAX HandlerBase 的实例进行多种形式的语法分析saxParser.parse(new File(args[0]), myHandlerBaseInstance);// 运用一个 SAX InputSource实例 和一个 SAX HandlerBase 实例saxParser.parse(mySaxInputSource, myHandlerBaseInstance);//运用一个 InputStream 实例和一个SAX HandlerBase 实例saxParser.parse(myInputStream, myHandlerBaseInstance);// 运用一个 URI 和一个SAX HandlerBase 实例saxParser.parse("http://www.newInstance.com/xml/doc.xml", myHandlerBaseInstance);//获得底层的(封装)SAX 语法分析器org.xml.sax.Parser parser = saxParser.getParser();//利用底层的语法分析器parser.setContentHandler(myContentHandlerInstance);parser.setErrorHandler(myErrorHandlerInstance);parser.parse(new org.xml.sax.InputSource(args[0]));
目前为止,关于 SAX 已经讲了很多,但是还没有揭示任何不寻常或令人惊奇的东西。事实上,JAXP 的功能很少,特别是当 SAX 也牵涉进来时。这很好,因为有最少的功能性意味着代码可移植性更强,并可以由其他开发人员与任何与 SAX 兼容的 XML 语法分析器一起使用,无论是免费(通过开放源码,希望如此)还是通过商业途径。就是这样。在 JAXP 中使用 SAX 没有更多的东西。如果已经知道 SAX,那么现在已经掌握大约 98% 的内容。只需学习两个新类和两个 Java 异常,您就可以开始了。如果从没使用过 SAX,那也很简单,现在就可以开始。
处理 DOM
如果要休息以迎接 DOM 挑战,那么先别休息。在 JAXP 中使用 DOM 的过程与 SAX 几乎相同,所要做的全部只是更改两个类名和一个返回类型,这样就差不多了。如果理解 SAX 的工作原理和 DOM 是什么,则不会有任何问题。
DOM 和 SAX 的主要差异是它们的 API 结构。SAX 包含一个基于事件的回调函数集,而 DOM 有一个内存中的树状结构。换句话说,在 SAX 中,从不需要操作数据结构(除非开发人员手工创建)。因此,SAX 不提供修改 XML 文档的功能。而 DOM 正好提供这种类型的功能。 org.w3c.dom.Document 类表示 XML 文档,它由表示元素、属性和其它 XML 结构的 DOM 节点 组成。所以,JAXP 无需触发 SAX 回调,它只负责从语法分析返回一个 DOM Document 对象。
DOM 语法分析器类厂一瞥
基本理解 DOM 以及 DOM 和 SAX 的差异之后,就没什么好说的了。以下代码看起来与 SAX 代码类似。首先,获得 DocumentBuilderFactory (与 SAX 中的方式相同)。然后,配置类厂来处理确认和名称空间(与 SAX 中的方式相同)。下一步,从类厂中检索 DocumentBuilder (它与 SAXParser 类似)(与 SAX 中的方式相同. . . 啊,您都知道了)。然后,就可以进行语法分析了,产生的 DOM Document 对象传递给打印 DOM 树的方法。
清单 3. 使用文档构建器类厂
import java.io.File;import java.io.IOException;import java.io.OutputStreamWriter;import java.io.Writer;// JAXPimport javax.xml.parsers.FactoryConfigurationError;import javax.xml.parsers.ParserConfigurationException;import javax.xml.parsers.DocumentBuilderFactory;import javax.xml.parsers.DocumentBuilder;// DOMimport org.w3c.dom.Document;import org.w3c.dom.DocumentType;import org.w3c.dom.NamedNodeMap;import org.w3c.dom.Node;import org.w3c.dom.NodeList;public class TestDOMParsing {public static void main(String[] args) {try {if (args.length != 1) {System.err.println ("Usage: java TestDOMParsing [filename]");System.exit (1);}// 获得 Document Builder FactoryDocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();//打开确认选项,关掉名称空间敏感性选项。factory.setValidating(true);factory.setNamespaceAware(false);DocumentBuilder builder = factory.newDocumentBuilder();Document doc = builder.parse(new File(args[0]));// 从DOM 数中打印文档,并加一初始空格printNode(doc, "");// 在这里也可以对 DOM 文档进行修改} catch (ParserConfigurationException e) {System.out.println("The underlying parser does not support the requested features.");} catch (FactoryConfigurationError e) {System.out.println("Error occurred obtaining Document Builder Factory.");} catch (Exception e) {e.printStackTrace();}}private static void printNode(Node node, String indent) {// 打印 DOM 树}
此代码中可能会出现两个不同的问题(与 JAXP 中的 SAX 类似): FactoryConfigurationError 和 ParserConfigurationException 。每一个的原因与 SAX 中的相同。不是实现类 ( FactoryConfigurationError ) 中有问题,就是语法分析器不支持请求的特性 ( ParserConfigurationException )。DOM 和 SAX 的唯一差异是:在 DOM 中,用 DocumentBuilderFactory 替代 SAXParserFactory ,用 DocumentBuilder 替代 SAXParser 。就这么简单!(可以 查看完整代码清单 ,该清单包括用于打印 DOM 树的方法。)
使用 DOM 语法分析器
有了 DOM 类厂之后,就可以获得 DocumentBuilder 实例。 DocumentBuilder 实例可以使用的方法与 SAX 的非常类似。主要差异是 parse() 的变种不需要 HandlerBase 类的实例。它们返回表示语法分析之后的 XML 文档的 DOM Document 实例。另一唯一不同之处是:为类似于 SAX 的功能提供了两个方法:用 SAX ErrorHandler 实现来处理语法分析时可能出现的问题的 setErrorHandler() ,和用 SAX EntityResolver 实现来处理实体解析的 setEntityResolver() 。如果不熟悉这些概念,则需要通过联机或在我的书中学习 SAX。以下清单显示使用这些方法的示例。
清单 4. 使用 JAXP DocumentBuilder
//获得一个 DocumentBuilder 实例DocumentBuilder builder = builderFactory.newDocumentBuilder();//查看是否支持 Validate 选项boolean isValidating = builder.isValidating(); //查看是否支持 namespace 选项boolean isNamespaceAware = builder.isNamespaceAware();// 设置一个 SAX ErrorHandlerbuilder.setErrorHandler(myErrorHandlerImpl);// 设置一个 SAX EntityResolverbuilder.setEntityResolver(myEntityResolverImpl);// 运用多种方法对 file 进行语法分析Document doc = builder.parse(new File(args[0]));// 运用 SAX InputSourceDocument doc = builder.parse(mySaxInputSource);// 运用 InputStreamDocument doc = builder.parse(myInputStream, myHandlerBaseInstance);// 运用 URIDocument doc = builder.parse("http://www.newInstance.com/xml/doc.xml");
是不是感到 DOM 这一节有些令人厌烦?有这种想法的不止您一个,写 DOM 代码有些令人厌烦是因为它是直接取得所学的 SAX 知识,然后将其用于 DOM。因此,和朋友、同事打赌吧,说使用 JAXP 只是小菜一碟。
更改语法分析器
最后要探讨的主题是 JAXP 轻易更改类厂类使用的语法分析器的能力。更改 JAXP 使用的语法分析器实际意味着更改 类厂,因为所有 SAXParser 和 DocumentBuilder 实例都来自这些类厂。既然确定装入哪个语法分析器的是类厂,因此,必须更改类厂。可以通过设置 Java 系统属性 javax.xml.parsers.SAXParserFactory 来更改要使用的 SAXParserFactory 接口实现。如果没有定义该属性,则返回缺省实现(供应商指定的任何语法分析器)。相同原理适用于 DocumentBuilderFactory 实现。在这种情况下,将查询 javax.xml.parsers.DocumentBuilderFactory 系统属性。就这么简单,我们已经学完了!这就是 SAXP 的全部:提供到 SAX 的挂钩,提供到 DOM 的挂钩,并允许轻易更改语法分析器。
结束语
如您所见,没多少复杂的东西。更改系统属性,通过类厂、而不是语法分析器或构建器来设置“确认”,以及弄清楚JAXP实际上不是人们通常所认为的那样,这些是使用 JAXP 的最困难部分。除了没有 SAX 2.0 和 DOM第二层规范支持之外,JAXP 在两个流行的 Java 和 XML API 之上提供一个有帮助的可插入层。它使代码独立于供应商,并允许不编译语法分析代码而更改语法分析器。那么,从 Sun、Apache XML 或其它方便之处下载 JAXP,并使用它吧!