XML 是由 Tim Bray 和 Michael Sperberg-McQueen 于 1996 年引入的。它的潜力已获得广泛公认,但是很难想象那时候会有哪个人能够知道 XML 会成为怎样的一种主要技术。企业 Java 开发人员将使用 XML 用于配置、用作数据存储以及最常见的是用作数据交换的格式。它是 Web 服务和 SOAP 的基础,从而也是现代面向服务的架构(Service-Oriented Architecture,SOA)设计模式的基础。但是 XML 并没有止步在那里。它将 X 融入 Ajax 或 Asynchronous JavaScript + XML 中,成为现代 Web 应用程序能够提供前所未有的丰富体验的关键。
但是,XML 也不是包治百病的灵丹妙药;它也有不足之处。XML 文档往往都很大。XML 文档都有通用树结构,但是这些 XML 文档的可扩展性意味着它的模式可以千变万化。这些方面向高效解析 XML 提出了挑战。克服 XML 解析挑战的传统方法有两种:DOM 和 SAX。
DOM 和 SAX 是解析 XML 的两种典型策略。它们在许多方面都是性质对立的策略。DOM 将为 XML 文档提供一个简单的对象模型。DOM 解析器将把 XML 文档转换成表示文档中所有数据的易于使用的对象。但是,这样如实地表示 XML 文档需要付出一定的代价:DOM 解析往往需要占用很多内存。
内存对 SAX 来说不是问题。SAX 解析器将生成一系列解析事件。注册这些事件的回调并随后对来自这些事件的数据执行某种逻辑都由 handler 来完成。它快速且高效,但是要求有复杂的编程模型。
了解使用 DOM 与 SAX 之间差异的最简单方法 —— 并且因而了解 StAX 的动机和优点 —— 是查看具体示例。
找到一些 XML 来解析并不难。到处都在使用 XML。现在的大多数 Web 站点都提供了某种基于 XML 的 Web 服务。Flickr 是归 Yahoo 所有的一个流行的照片分享站点,它拥有强大而灵活的 API。让我们来看一看访问 Flickr 的 “有趣” 照片的一些简单代码(要获得本文中使用的所有源代码,请参阅下载,并确保把 Woodstox 放入类路径中或者使用 JDK 1.6)。代码如清单 1 所示:
String apiKey = "c4579586f41a90372f762cb65c78be5d"; String urlStr = "http://api.flickr.com/services/rest/?" + "method=flickr.interestingness.getList&per_page=20&api_key="+apiKey; URL request = new URL(urlStr); InputStream input = request.openStream(); |
这段代码将使用 Flickr 的代表性状态传输(Representational State Transfer,REST)API(有关 Flickr 的 API 和 REST 格式的更多信息,请参阅参考资料 部分)。以上调用的一些样例输出如清单 2 所示:
<?xml version="1.0" encoding="utf-8" ?> <rsp stat="ok"> <photos page="1" pages="25" per_page="20" total="500"> <photo id="469774979" owner="35373726@N00" secret="c8a1be2012" server="183" farm="1" title="Where will it lead me......?" ispublic="1" isfriend="0" isfamily="0" /> <photo id="470281793" owner="73955226@N00" secret="49612a2794" server="212" farm="1" title="Island Beauty" ispublic="1" isfriend="0" isfamily="0" /> <photo id="469808244" owner="43568064@N00" secret="26b71544a3" server="227" farm="1" title="" ispublic="1" isfriend="0" isfamily="0" /> </photos> </rsp> |
注意清单 2 只显示了三张照片。API 调用实际上将返回 20(URL 字符串中的 per_page
参数)。结果十分简单,因此来看一看如何解析这个 XML。在示例中,将解析出每张照片的标题及其 ID。该 ID 可用于创建该照片的 URL,因此不难想象 Web 应用程序(可能是 mashup)只使用此信息。首先需要使用 DOM 来提取此数据。
要使用 DOM,需要把文档解析成文档对象。这是表示已被解析的 XML 文档的内存中树结构。随后遍历 DOM 树来查找每张照片的标题和 ID。将此数据放入简单映射中。完成此过程的代码如清单 3 所示:
Map<String,String> map = new HashMap<String,String>(); DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document dom = builder.parse(input); Element root = dom.getDocumentElement(); NodeList childNodes = root.getChildNodes(); Node photosNode = null; for (int i=0;i<childNodes.getLength();i++){ Node node = childNodes.item(i); if (node.getNodeName().equalsIgnoreCase("photos")){ photosNode = node; break; } } childNodes = photosNode.getChildNodes(); for (int i=0;i<childNodes.getLength();i++){ Node node = childNodes.item(i); if (node.getNodeName().equalsIgnoreCase("photo")){ String title = node.getAttributes().getNamedItem("title").getTextContent(); String id = node.getAttributes().getNamedItem("id").getTextContent(); map.put(id,title); } } |
DOM 十分流行,因为它非常易于使用。只需将输入源传入解析器中,然后解析器将为您提供 document
对象。然后,您可以遍历子节点,直至找到照片节点。每个照片节点都是照片节点的子节点,因此您将遍历每个照片节点,然后访问每个照片节点的title
和 id
属性并将其存储到映射中。
但是,DOM 也有一些明显的效率低下之处。您要存储大量的可能并不关心的数据,例如每张照片的所有者。您还将浏览所有数据两次:第一次浏览用于将其读入文档对象,然后在遍历文档对象时进行第二次浏览。避免这些效率低下之处的传统方法是使用 SAX。
SAX 解析器并不像 DOM 解析器一样返回一个精密的 document
对象。相反,SAX 解析器将在遍历 XML 文档时给出一系列事件。必须通过实现接口或者扩展DefaultHandler
类并根据需要重写它的方法,创建这些事件的 handler。清单 4 将演示 Flickr XML 文档的 SAX 解析。
SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); DefaultHandler handler = new DefaultHandler(){ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (qName.equalsIgnoreCase("photo")){ String title = attributes.getValue("title"); String id = attributes.getValue("id"); // map is static so we can access it here map.put(id, title); } } }; parser.parse(input, handler); |
很明显,清单 4 中所示的代码比 清单 3 中的 DOM 代码难于理解一些。您需要使用 ContentHandler
来处理 SAX 事件,因此创建了 DefaultHandler
并重写了它的 startElement
回调方法。查看它是不是一个照片元素,并且如果是照片元素,则访问它的title
和 id
属性。
代码十分简洁并且在运行时非常高效。它仅存储您所关心的数据,并且只遍历文档一次。它是更为复杂的代码,要求扩展类才能注册事件侦听程序。如果能够高效地解析 XML 而且可以使用更直观的编程模型那就太好了。StAX 于是应运而生。
SAX 中的复杂度来自它实现的 Observer 设计模式。它是一个 push 模型,因为解析器将把事件 push 到随后作用于事件的 observer 中。StAX 模型类似于 SAX。它将来自 XML 文档的数据和事件流线化,使其可以像 SAX 一样快速且高效。最大的不同之处是它使用 pull 模型。这将允许应用程序代码从解析器中 pull 事件。
这可能听起来像是一个细微的差异,但是它将允许使用更简单的编程模型。查阅清单 5 以查看 StAX 的运作。
Map<String,String> map = new HashMap<String,String>(); XMLInputFactory inputFactory = XMLInputFactory.newInstance(); QName qId = new QName("id"); QName qTitle = new QName("title"); QName qPhoto = new QName("photo"); XMLEventReader reader = inputFactory.createXMLEventReader(input); while (reader.hasNext()){ XMLEvent event = reader.nextEvent(); if (event.isStartElement()){ StartElement element = event.asStartElement(); if (element.getName().equals(qPhoto)){ String id = element.getAttributeByName(qId).getValue(); String title = element.getAttributeByName(qTitle).getValue(); map.put(id,title); } } } reader.close(); |
首先,您不必扩展任何类。那是因为您无需为事件进行注册。使用 StAX,您可以控制事件流,因为将从解析器中 pull 这些事件流。您可以使用一种熟悉的类似迭代器的语法来搜索整个文档以查找所需的数据。您仍将只存储所需的数据,并且只需浏览 XML 文档一次。您将获得与使用 SAX 时一样的效率,但是代码将直观得多。
使用 Woodstox 作为 Geronimo 的 StAX 提供程序
现在您已经看到了 StAX 解析的优点。它被广泛公认为 XML 技术中的重大进步。因而,当它成为 Java EE 5 规范的一部分时并不令人惊讶(它甚至还包含在 Java Platform, Standard Edition [Java SE] 6 中)。由于它是 Java EE 5 的一部分,因此它必须由 Geronimo 2.0 来实现。
Geronimo 团队十分幸运,有若干个开源 StAX 实现可供选择。团队选取了 Woodstox 作为 Geronimo 所附带的 StAX 解析器。Woodstox 被视为执行效果最佳的 StAX 实现之一(要比较各种 StAX 解析器,请参阅参考资料)。此外,Woodstox 是在 Lesser General Public License (LGPL) 和 Apache 2.0 许可下双重授权的。因此您可以不受任何限制地将 Woodstox 及其源代码集成到 Geronimo 中。
性能很明显是 Woodstox 带给 Geronimo 的优点之一。就像使用其他高性能技术一样,了解如何使用 Woodstox 才能获得最佳性能非常重要。清单 5 中的代码将使用XMLEventReader
接口,这是 StAX 规范包含的一个高级 API。用于获得高性能的较低级 API 是 XMLStreamReader
接口。清单 6 显示了使用此接口的 StAX 解析器。
XMLStreamReader
进行 StAX 解析
Map<String,String> map = new HashMap<String,String>(); XMLInputFactory inputFactory = XMLInputFactory.newInstance(); QName qId = new QName("id"); QName qTitle = new QName("title"); QName qPhoto = new QName("photo"); XMLStreamReader reader = inputFactory.createXMLStreamReader(input); while (reader.hasNext()){ int event = reader.next(); if (event == START_ELEMENT){ // statically included constant from XMLStreamConstants if (reader.getName().equals(qPhoto)){ String id = reader.getAttributeValue(null, qId.getLocalPart()); String title = reader.getAttributeValue(null, qTitle.getLocalPart()); map.put(id,title); } } } reader.close(); |
清单 6 中的代码类似于 清单 5 中的代码;虽然它很明显有些低级,但是您将获得很大的性能提高。
您已经了解了使用 StAX 解析器解析 XML 文档的一些优点。StAX 在 SAX 与 DOM 之间提供了很好的折衷。您可以通过使用它作为 Geronimo 2.0 的一部分立即利用 StAX。您不但将开始使用 StAX 的直观 pull API,而且将获得在 Woodstox 中使用 StAX 的高性能实现的额外优势。