简要探讨 Java 中不同 XML 文档模型的工作原理 Dennis M. Sosnoski(dms@sosnoski.com) 总裁,Sosnoski Software Solutions, Inc. 2002 年 2 月
在本系列的第一篇文章中,我研究了一些用 Java 编写的主要的 XML 文档模型的性能。但是,在开始选择这种类型的技术时,性能只是问题的一部分。使用方便至少是同样重要的,并且它已是一个主要理由,来支持使用 Java 特定的模型,而不是与语言无关的 DOM 。 为切实了解哪个模型真正的作用,您需要知道它们在可用性程度上是如何排名的。本文中,我将尝试进行这个工作,从样本代码开始,来演示如何在每个模型中编码公共类型的操作。并对结果进行总结来结束本文,而且提出了促使一种表示比另一种更容易使用的一些其它因素。 请参阅以前的文章(请参阅参考资料或本文“内容”下的便捷链接)来获取这个对比中使用的各个模型的背景资料,包含实际的版本号。还可以参阅“参考资料”一节中关于源代码下载、到模型主页的链接以及其它相关信息。 代码对比 在对不同文档表示中用法技术的这些对比中,我将显示如何在每种模型中实现三种基本操作:
- 根据输入流构建文档
- 遍历元素和内容,并做一些更改:
- 从文本内容中除去前导和尾随的空白。
- 如果结果文本内容为空,就删除它。
- 否则,将它包装到父元素的名称空间中一个名为“text”的新元素中。
- 将已修改的文档写入输出流
这些示例的代码是以我在上篇文章中使用的基准程序为基础的,并进行了一些简化。基准程序的焦点是为了显示每个模型的最佳性能;对于本文,我将尝试显示在每种模型中实现操作的最简便方法。 我已经将每个模型的示例结构化为两个独立的代码段。第一段是读取文档、调用修改代码和编写已修改文档的代码。第二段是真正遍历文档表示和执行修改的递归方法。为避免分散注意力,我已在代码中忽略了异常处理。 您可以从本页底部参考资料一节链接到下载页,以获取所有样本的完整代码。样本的下载版本包括一个测试驱动程序,还有一些添加的代码用于通过计算元素、删除和添加的个数来检查不同模型的操作。 即使您不想使用 DOM 实现,但还是值得浏览下面对 DOM 用法的描述。因为 DOM 示例是第一个示例,所以与后面的模型相比,我用它来探究有关该示例的一些问题和结构的更详细信息。浏览这些内容可以补充您想知道的一些细节,如果直接阅读其它模型之一,那么将错过这些细节。 DOM DOM 规范涵盖了文档表示的所有类型的操作,但是它没有涉及例如对文档的语法分析和生成文本输出这样的问题。包括在性能测试中的两种 DOM 实现,Xerces 和 Crimson,对这些操作使用不同的技术。清单 1 显示了 Xerces 的顶级代码的一种形式。 清单 1. Xerces DOM 顶级代码
1 // parse the document from input stream ("in")
2 DOMParser parser = new DOMParser();
3 parser.setFeature("http://xml.org/sax/features/namespaces", true);
4 parser.parse(new InputSource(in));
5 Document doc = parser.getDocument();
6 // recursively walk and modify document
7 modifyElement(doc.getDocumentElement());
8 // write the document to output stream ("out")
9 OutputFormat format = new OutputFormat(doc);
10 XMLSerializer serializer = new XMLSerializer(out, format);
11 serializer.serialize(doc.getDocumentElement());
| 正如我在注释中指出的,清单 1 中的第一块代码(第 1-5 行)处理对输入流的语法分析,以构建文档表示。Xerces 定义了 DOMParser 类,以便从 Xerces 语法分析器的输出构建文档。InputSource 类是 SAX 规范的一部分,它能适应供 SAX 分析器使用的几种输入形式的任何之一。通过单一调用进行实际的语法分析和文档构造,如果成功完成了这一操作,那么应用程序就可以检索并使用已构造的 Document 。 第二个代码块(第 6-7 行)只是将文档的根元素传递给我马上要谈到的递归修改方法。这些代码与本文中所有文档模型的代码在本质上是相同的,所以在剩余的示例中我将跳过它,不再做任何讨论。 第三个代码块(第 8-11 行)处理将文档作为文本写入输出流。这里,OutputFormat 类包装文档,并为格式化生成的文本提供了多种选项。XMLSerializer 类处理输出文本的实际生成。 Xerces 的 modify 方法只使用标准 DOM 接口,所以它还与任何其它 DOM 实现兼容。清单 2 显示了代码。 清单 2. DOM Modify 方法
1 protected void modifyElement(Element element) {
2 // loop through child nodes
3 Node child;
4 Node next = (Node)element.getFirstChild();
5 while ((child = next) != null) {
6 // set next before we change anything
7 next = child.getNextSibling();
8 // handle child by node type
9 if (child.getNodeType() == Node.TEXT_NODE) {
10 // trim whitespace from content text
11 String trimmed = child.getNodeValue().trim();
12 if (trimmed.length() == 0) {
13 // delete child if nothing but whitespace
14 element.removeChild(child);
15 } else {
16 // create a "text" element matching parent namespace
17 Document doc = element.getOwnerDocument();
18 String prefix = element.getPrefix();
19 String name = (prefix == null) ? "text" : (prefix + ":text");
20 Element text =
21 doc.createElementNS(element.getNamespaceURI(), name);
22 // wrap the trimmed content with new element
23 text.appendChild(doc.createTextNode(trimmed));
24 element.replaceChild(text, child);
25 }
26 } else if (child.getNodeType() == Node.ELEMENT_NODE) {
27 // handle child elements with recursive call
28 modifyElement((Element)child);
29 }
30 }
31 }
| 清单 2 中显示的方法所使用的基本方法与所有文档表示的方法相同。 通过一个元素调用它,它就依次遍历那个元素的子元素。如果找到文本内容子元素,要么删除文本(如果它只是由空格组成的),要么通过与包含元素相同的名称空间中名为“text”的新元素来包装文本(如果有非空格的字符)。如果找到一个子元素,那么这个方法就使用这个子元素,递归地调用它本身。 对于 DOM 实现,我使用一对引用:child 和 next 来跟踪子元素排序列表中我所处的位置。在对当前子节点进行任何其它处理之前,先装入下个子节点的引用(第 7 行)。这样做使得我能够删除或替代当前的子节点,而不丢失我在列表中的踪迹。 当我创建一个新元素来包装非空白的文本内容(第 16-24 行)时,DOM 接口开始有点杂乱。用来创建元素的方法与文档关联并成为一个整体,所以我需要在所有者文档中检索当前我正在处理的元素(第 17 行)。我想将这个新元素放置在与现有的父元素相同的名称空间中,并且在 DOM 中,这意味着我需要构造元素的限定名称。根据是否有名称空间的前缀,这个操作会有所不同(第 18-19 行)。利用新元素的限定名称,以及现有元素中的名称空间 URI,我就能创建新元素(第 20-21 行)。 一旦创建了新元素,我只要创建和添加文本节点来包装内容 String ,然后用新创建的元素来替代原始文本节点(第 22-24 行)。 清单 3. Crimson DOM 顶级代码
1 // parse the document from input stream
2 System.setProperty("javax.xml.parsers.DocumentBuilderFactory",
3 "org.apache.crimson.jaxp.DocumentBuilderFactoryImpl");
4 DocumentBuilderFactory dbf = DocumentBuilderFactoryImpl.newInstance();
5 dbf.setNamespaceAware(true);
6 DocumentBuilder builder = dbf.newDocumentBuilder();
7 Document doc = builder.parse(in);
8 // recursively walk and modify document
9 modifyElement(doc.getDocumentElement());
10 // write the document to output stream
11 ((XmlDocument)doc).write(out);
| 清单 3 中的 Crimson DOM 示例代码使用了用于语法分析的 JAXP 接口。JAXP 为语法分析和转换 XML 文档提供了一个标准化的接口。本示例中的语法分析代码还可以用于 Xerces(对文档构建器类名称的特性设置有适当的更改)来替代较早给定的 Xerces 特定的示例代码。 在本示例中,我首先在第 2 行到第 3 行中设置系统特性来选择要构造的 DOM 表示的构建器工厂类(JAXP 仅直接支持构建 DOM 表示,不支持构建本文中讨论的任何其它表示)。仅当想选择一个要由 JAXP 使用的特定 DOM 时,才需要这一步;否则,它使用缺省实现。出于完整性起见,我在代码中包含了设置这个特性,但是更普遍的是将它设置成一个 JVM 命令行参数。 接着我在第 4 行到第 6 行中创建构建器工厂的实例,对使用那个工厂实例构造的构建器启用名称空间支持,并从构建器工厂创建文档构建器。最后(第 7 行),我使用文档构建器来对输入流进行语法分析并构造文档表示。 为了写出文档,我使用 Crimson 中内部定义的基本方法。不保证在 Crimson 未来版本中支持这个方法,但是使用 JAXP 转换代码来将文档作为文本输出的替代方法需要诸如 Xalan 那样的 XSL 处理器的。那超出了本文的范围,但是要获取详细信息,可以查阅 Sun 中的 JAXP 教程。 JDOM 使用 JDOM 的顶级代码比使用 DOM 实现的代码稍微简单一点。为构建文档表示(第 1-3 行),我使用带有由参数值禁止验证的 SAXBuilder 。通过使用提供的 XMLOutputter 类,将已修改的文档写入输出流同样简单(第 6-8 行)。 清单 4. JDOM 顶级代码
1 // parse the document from input stream
2 SAXBuilder builder = new SAXBuilder(false);
3 Document doc = builder.build(in);
4 // recursively walk and modify document
5 modifyElement(doc.getRootElement());
6 // write the document to output stream
7 XMLOutputter outer = new XMLOutputter();
8 outer.output(doc, out);
| 清单 5 中 JDOM 的 modify 方法也比 DOM 的同一方法简单。我获取包含元素所有内容的列表并扫描了这张列表,检查文本(象 String 对象那样的内容)和元素。这张列表是“活的”,所以我能直接对它进行更改,而不必调用父元素上的方法。 清单 5. JDOM modify 方法
1 protected void modifyElement(Element element) {
2 // loop through child nodes
3 List children = element.getContent();
4 for (int i = 0; i < children.size(); i++) {
5 // handle child by node type
6 Object child = children.get(i);
7 if (child instanceof String) {
8 // trim whitespace from content text
9 String trimmed = child.toString().trim();
10 if (trimmed.length() == 0) {
11 // delete child if only whitespace (adjusting index)
12 children.remove(i--);
13 } else {
14 // wrap the trimmed content with new element
15 Element text = new Element("text", element.getNamespace());
16 text.setText(trimmed);
17 children.set(i, text);
18 }
19 } else if (child instanceof Element) {
20 // handle child elements with recursive call
21 modifyElement((Element)child);
22 }
23 }
24 }
| 创建新元素的技术(第 14-17 行)非常简单,而且与 DOM 版本不同,它不需要访问父文档。 dom4j dom4j 的顶级代码比 JDOM 的稍微复杂些,但是它们的代码行非常类似。这里的主要区别是我保存了用来构建 dom4j 文档表示的 DocumentFactory (第 5 行),并在输出已修改的文档文本之后刷新了 writer(第 10 行)。 清单 6. dom4j 的顶级代码
1 // parse the document from input stream
2 SAXReader reader = new SAXReader(false);
3 Document doc = reader.read(in);
4 // recursively walk and modify document
5 m_factory = reader.getDocumentFactory();
6 modifyElement(doc.getRootElement());
7 // write the document to output stream
8 XMLWriter writer = new XMLWriter(out);
9 writer.write(doc);
10 writer.flush();
| 正如您在清单 6 中看到的,dom4j 使用一个工厂方法来构造文档表示(从语法分析构建)中包含的对象。根据接口来定义每个组件对象,所以实现其中一个接口的任何类型的对象都能包含在表示中(与 JDOM 相反,它使用具体类:这些类在某些情况中可以划分子类和被继承,但是在文档表示中使用的任何类都需要以原始 JDOM 类为基础)。通过使用不同工厂进行 dom4j 文档构建,您能获取不同系列的组件中构造的文档。 在样本代码(第 5 行)中,我检索了用于构建文档的(缺省)文档工厂,并将它存储在一个实例变量(m_factory )中以供 modify 方法使用。并不严格需要这一步 — 可以在一个文档中同时使用来自不同工厂的组件,或者可以绕过工厂而直接创建组件的实例 — 但在该例中,我只想创建与在文档其余部分中使用的同一类型的组件,并且使用相同的工厂来确保完成这个步骤。 清单 7. dom4j modify 方法
1 protected void modifyElement(Element element) {
2 // loop through child nodes
3 List children = element.content();
4 for (int i = 0; i < children.size(); i++) {
5 // handle child by node type
6 Node child = (Node)children.get(i);
7 if (child.getNodeType() == Node.TEXT_NODE) {
8 // trim whitespace from content text
9 String trimmed = child.getText().trim();
10 if (trimmed.length() == 0) {
11 // delete child if only whitespace (adjusting index)
12 children.remove(i--);
13 } else {
14 // wrap the trimmed content with new element
15 Element text = m_factory.createElement
16 (QName.get("text", element.getNamespace()));
17 text.addText(trimmed);
18 children.set(i, text);
19 }
20 } else if (child.getNodeType() == Node.ELEMENT_NODE) {
21 // handle child elements with recursive call
22 modifyElement((Element)child);
23 }
24 }
25 }
| 清单 7 中 dom4j modify 方法与 JDOM 中使用的方法非常类似。不通过使用 instanceof 运算符来检查内容项的类型,我可以通过 Node 接口方法 getNodeType 来获取类型代码(也可以使用 instanceof ,但类型代码方法看起来更清晰)。通过使用 QName 对象来表示元素名称和通过调用已保存的工厂的方法来构建元素可以区别新元素的创建技术(第 15-16 行)。 Electric XML 清单 8 中 Electric XML(EXML)的顶级代码是任何这些示例中最简单的一个,通过单一方法调用就可以读取和编写文档。 清单 8. EXML 顶级代码
1 // parse the document from input stream
2 Document doc = new Document(in);
3 // recursively walk and modify document
4 modifyElement(doc.getRoot());
5 // write the document to output stream
6 doc.write(out);
| 清单 9 中 EXML modify 方法尽管与 JDOM 一样,需要使用 instanceof 检查,但它与 DOM 方法最相似。在 EXML 中,无法创建一个带名称空间限定的名称的元素,所以取而代之,我创建新元素,然后设置其名称来达到相同的效果。 清单 9. EXML modify 方法
1 protected void modifyElement(Element element) {
2 // loop through child nodes
3 Child child;
4 Child next = element.getChildren().first();
5 while ((child = next) != null) {
6 // set next before we change anything
7 next = child.getNextSibling();
8 // handle child by node type
9 if (child instanceof Text) {
10 // trim whitespace from content text
11 String trimmed = ((Text)child).getString().trim();
12 if (trimmed.length() == 0) {
13 // delete child if only whitespace
14 child.remove();
15 } else {
16 // wrap the trimmed content with new element
17 Element text = new Element();
18 text.addText(trimmed);
19 child.replaceWith(text);
20 text.setName(element.getPrefix(), "text");
21 }
22 } else if (child instanceof Element) {
23 // handle child elements with recursive call
24 modifyElement((Element)child);
25 }
26 }
27 }
| XPP XPP 的顶级代码(在清单 10 中)是所有示例中最长的一个,与其它模型相比,它需要相当多的设置。 清单 10. XPP 顶级代码
1 // parse the document from input stream
2 m_parserFactory = XmlPullParserFactory.newInstance();
3 m_parserFactory.setNamespaceAware(true);
4 XmlPullParser parser = m_parserFactory.newPullParser();
5 parser.setInput(new BufferedReader(new InputStreamReader(in)));
6 parser.next();
7 XmlNode doc = m_parserFactory.newNode();
8 parser.readNode(doc);
9 // recursively walk and modify document
10 modifyElement(doc);
11 // write the document to output stream
12 XmlRecorder recorder = m_parserFactory.newRecorder();
13 Writer writer = new OutputStreamWriter(out);
14 recorder.setOutput(writer);
15 recorder.writeNode(doc);
16 writer.close();
| 因为使用 JAXP 接口,所以我必须首先创建分析器工厂的实例并在创建分析器实例之前启用名称空间处理(第 2-4 行)。一旦获取了分析器实例,我就能将输入设置到分析器中,并真正构建文档表示(第 5-8 行),但是这涉及比其它模型更多的步骤。 输出处理(第 11-16 行)也涉及比其它模型更多的步骤,主要因为 XPP 需要 Writer 而不是直接将 Stream 作为输出目标接受。 清单 11 中 XPP modify 方法尽管需要更多代码来创建新元素(第 13-21 行),但它与 JDOM 方法最类似。名称空间处理在这里有点麻烦。我首先必须创建元素的限定名称(第 15-16 行),然后创建元素,最后在稍后设置名称和名称空间 URI(第 18-21 行)。 清单 11. XPP modify 方法
1 protected void modifyElement(XmlNode element) throws Exception {
2 // loop through child nodes
3 for (int i = 0; i < element.getChildrenCount(); i++) {
4 // handle child by node type
5 Object child = element.getChildAt(i);
6 if (child instanceof String) {
7 // trim whitespace from content text
8 String trimmed = child.toString().trim();
9 if (trimmed.length() == 0) {
10 // delete child if only whitespace (adjusting index)
11 element.removeChildAt(i--);
12 } else {
13 // construct qualified name for wrapper element
15 String prefix = element.getPrefix();
16 String name = (prefix == null) ? "text" : (prefix + ":text");
17 // wrap the trimmed content with new element
18 XmlNode text = m_parserFactory.newNode();
19 text.appendChild(trimmed);
20 element.replaceChildAt(i, text);
21 text.modifyTag(element.getNamespaceUri(), "text", name);
22 }
23 } else if (child instanceof XmlNode) {
24 // handle child elements with recursive call
25 modifyElement((XmlNode)child);
26 }
27 }
28 }
| 结束语 DOM、dom4j 和 Electric XML 都得到这些几乎同样易于使用的代码样本,其中 EXML 可能最简单,而 dom4j 受一些小条件限制而较困难。DOM 提供了与语言无关的非常实在的好处,但是如果你只使用 Java 代码,那么通过与 Java 特定的模型相比较,它看上去有点麻烦。我认为这表明 Java 特定的模型通常成功地实现简化 Java 代码中的 XML 文档处理这个目标。
超越基础:真实世界可用性 代码样本显示 JDOM 和 EXML 为基本文档操作(使用元素、属性和文本)提供了简单和清晰的接口。根据我的经验,它们的方法并不能很好地完成处理整个文档表示的编程任务。要完成这些类型的任务,DOM 和 dom4j 使用的组件方法 — 其中从属性到名称空间的所有文档组件实现一些公共接口 — 工作得更好。 相关的例子是最近我为 JDOM 和 dom4j 实现的 XML 流型(XML Streaming (XMLS) )编码。这个代码遍历整个文档并编码每个组件。JDOM 实现比 dom4j 实现复杂得多,主要是因为 JDOM 使用一些没有公共接口的独特类来表示每个组件。 因为 JDOM 缺少公共接口,所以即使处理 Document 对象的代码与处理 Element 对象的代码都有一些诸如子组件那样相同类型的组件,但是它们必须有所不同。还需要特殊方法来检索与其它类型的子组件相对的 Namespace 组件。甚至当处理被认为是内容的子组件类型时,您需要在组件类型上使用多个带 instanceof 检查的 if 语句,而不是使用一条更清晰更快速的 switch 语句。 具有讽刺意味的可能是 JDOM 的最初目标之一是利用 Java Collection 类,这些类本身在很大程度上以接口为基础。库中接口的使用增加了许多灵活性,而这是以增加了一些复杂性为代价的,并且这对于为重用而设计的代码来说,通常是一个很好的折衷。这可能还主要归功于 dom4j,它达到一个成熟并且稳定的状态,比 JDOM 要快得多。 | 尽管如此,对于使用多种语言的开发人员来说,DOM 仍是一个非常好的选择。DOM 实现广泛应用于多种编程语言。它还是许多其它与 XML 相关的标准的基础,所以即使您使用 Java 特定的模型,也还有一个您逐步熟悉 DOM 所需要的好机会。因为它正式获得 W3C 推荐(与基于非标准的 Java 模型相对),所以在某些类型的项目中可能也需要它。 就使用方便这一范畴而言,在 JDOM、dom4j 和 Electric XML 这三个主要竞争者中,dom4j 与其它两个的区别在于它使用带有多个继承层的基于接口的方法。这会使得遵循 API JavaDocs 更为困难些。例如,您正在寻找的一个方法(例如 content() ,在我们 dom4j 的 modify 方法示例的第 3 行中使用的)可能是 Element 扩展的 Branch 接口的一部分,而不是 Element 接口本身的一部分。尽管如此,这种基于接口的设计添加了许多灵活性(请参阅侧栏超越基础:真实世界可用性)。考虑到 dom4j 的性能、稳定性和特性设置的优点,您应把它当作多数项目中的一个有力的候选者。 在任一 Java 特定的文档模型之中,JDOM 可能拥有最广泛的用户基础,并且它的确是使用起来最简单的模型之一。尽管如此,作为项目开发的一个选择,它还是必须容忍 API 的不固定性和从一个版本到下一个版本的更新,在性能对比中它也表现得很糟糕。基于当前实现,我愿为着手新项目的人们推荐 dom4j,而不是 JDOM。 除了 XPP 以外,EXML 比其它任何模型占用的资源都要少得多,并且考虑到 EXML 易于使用的优点,您应肯定会认为它适用于 jar 文件大小很重要的应用程序。但是,EXML 的 XML 支持的局限性和受限的许可证,以及在较大文件上所表现出的相对拙劣的性能,不得不在许多应用程序中放弃使用它。 XPP 在语法分析和编写文本文档时需要更多步骤,并且在处理名称空间时也需要更多步骤。如果 XPP 打算添加一些便利的方法来处理其中一些常见情况,那么在对比中它可能会更胜一筹。正如它现在所表现的,上篇文章中性能方面的领先者却成了本文中的可用性方面的失败者。尽管如此,因为 XPP 性能方面的优势,所以对于需要较小的 jar 文件大小的应用程序还是值得将它作为 EXML 的替代方法。 下一次... 到目前为止在我写的两篇文章中,涉及到用 Java 编写的 XML 文档模型的性能和可用性。在本系列的后两篇文章中,我将讨论用 Java 技术进行 XML 数据绑定的方法。这些方法与文档模型的方法有许多相似处,但是它们更进一步将 XML 文档映射到实际应用程序数据结构中。我们将看到这一操作在使用的简便性和提高性能方面是如何做得如此好的。 回到 developerWorks,检查 Java 代码的 XML 数据绑定的实质。同时,您可以通过下面链接的论坛,给出您对本文的评论和问题。 参考资料
|