课程:XML 的流式 API
本课程专注于 XML 的流式 API(StAX),这是一种基于 Java 技术的流式、事件驱动、拉取解析的 API,用于读取和写入 XML 文档。StAX 使您能够创建快速、相对易于编程且具有轻量级内存占用的双向 XML 解析器。
为什么选择 StAX?
StAX 项目由 BEA 主导,得到了 Sun Microsystems 的支持,JSR 173 规范于 2004 年 3 月通过了 Java 社区流程的最终批准投票。StAX API 的主要目标是通过公开一个简单的基于迭代器的 API,将“解析控制权交给程序员。这允许程序员请求下一个事件(拉取事件),并允许以过程化方式存储状态。” StAX 的创建是为了解决两种最常见解析 API,SAX 和 DOM,的限制。
流式处理与 DOM
一般来说,处理 XML 信息集有两种编程模型:流式处理和文档对象模型(DOM)。
DOM 模型涉及创建代表整个文档树和 XML 文档的完整信息集状态的内存对象。一旦在内存中,DOM 树可以自由导航和任意解析,因此为开发人员提供了最大的灵活性。然而,这种灵活性的代价是潜在的大内存占用和显著的处理器需求,因为整个文档的表示必须作为对象在内存中保持,以便在文档处理期间使用。在处理小型文档时,这可能不是问题,但随着文档大小的增加,内存和处理器需求可能会迅速升高。
流式处理是指一种编程模型,在应用程序运行时串行传输和解析 XML 信息集,通常是实时的,并且通常来自动态来源,其内容事先并不完全知晓。此外,基于流的解析器可以立即开始生成输出,并且信息集元素在使用后可以立即丢弃和进行垃圾回收。虽然提供了较小的内存占用、降低的处理器需求和在某些情况下更高的性能,但流处理的主要折衷是您只能在文档中的一个位置看到信息集状态。您基本上受限于文档的“纸板筒”视图,这意味着您需要在阅读 XML 文档之前知道要进行哪些处理。
在处理 XML 时,流式处理模型特别适用于应用程序具有严格的内存限制,比如在运行 Java 平台微版(Java ME 平台)的手机上,或者当应用程序需要同时处理多个请求时,比如在应用服务器上。实际上,可以说大多数 XML 业务逻辑都可以从流式处理中受益,并且不需要在内存中维护整个 DOM 树。
拉取解析与推送解析
流拉取解析是一种编程模型,其中客户端应用程序在需要与 XML 信息集交互时调用 XML 解析库的方法,即客户端只有在明确请求时才会获取(拉取)XML 数据。
流推送解析是一种编程模型,其中 XML 解析器在遇到 XML 信息集中的元素时向客户端发送(推送)XML 数据,即使客户端此时还没有准备好使用它。
在处理 XML 流时,拉取解析相比于推送解析提供了几个优势:
- 在拉取解析中,客户端控制应用程序线程,并且可以在需要时调用解析器的方法。相比之下,在推送处理中,解析器控制应用程序线程,客户端只能接受解析器的调用。
- 拉取解析库可以比推送库更小,与这些库交互的客户端代码也更简单,即使对于更复杂的文档。
- 拉取客户端可以使用单个线程同时读取多个文档。
- StAX 拉取解析器可以过滤 XML 文档,使客户端不需要的元素被忽略,并且可以支持非 XML 数据的 XML 视图。
StAX 使用案例
StAX 规范定义了 API 的许多用例:
- 数据绑定
- 反编组 XML 文档
- 将 XML 文档编组
- 并行文档处理
- 无线通信
- 简单对象访问协议(SOAP)消息处理
- 解析简单可预测的结构
- 解析具有前向引用的图形表示
- 解析 Web 服务描述语言(WSDL)
- 虚拟数据源
- 查看存储在数据库中的 XML 数据
- 查看由 XML 数据绑定创建的 Java 对象中的数据
- 将 DOM 树作为事件流导航
- 解析特定的 XML 词汇
- 管道化 XML 处理
对所有这些用例的完整讨论超出了本课程的范围。请参考 StAX 规范以获取更多信息。
将 StAX 与其他 JAXP API 进行比较
作为 JAXP 家族中的一个 API,StAX 可以与 SAX、TrAX 和 JDOM 等其他 API 进行比较。在后两者中,StAX 不像 TrAX 或 JDOM 那样强大或灵活,但也不需要太多内存或处理器负载才能发挥作用,并且在许多情况下,StAX 可以胜过基于 DOM 的 API。上面概述的相同论点,权衡 DOM 模型与流模型的成本/效益,在这里同样适用。
有鉴于此,最接近的比较可以在 StAX 和 SAX 之间进行,正是在这里 StAX 提供了许多情况下有益的功能;其中一些包括:
- 使用 StAX 的客户端通常比使用 SAX 的客户端更容易编码。虽然可以说 SAX 解析器稍微更容易编写,但 StAX 解析器的代码可能更小,客户端与解析器交互所需的代码更简单。
- StAX 是一个双向 API,意味着它既可以读取又可以写入 XML 文档。SAX 只能读取,所以如果你想要写入 XML 文档,就需要另一个 API。
- SAX 是一个推送 API,而 StAX 是一个拉取 API。上面概述的推送和拉取 API 之间的权衡在这里也适用。
以下表格总结了 StAX、SAX、DOM 和 TrAX 的比较特性。(表格改编自 Jeff Ryan 的文章Does StAX Belong in Your XML Toolbox?)。
XML 解析器 API 特性摘要
特性 | StAX | SAX | DOM | TrAX |
API 类型 | 拉取,流式 | 推送,流式 | 内存树 | XSLT 规则 |
使用便捷性 | 高 | 中 | 高 | 中 |
XPath 能力 | 否 | 否 | 是 | 是 |
CPU 和内存效率 | 良好 | 良好 | 各异 | 各异 |
仅向前 | 是 | 是 | 否 | 否 |
读取 XML | 是 | 是 | 是 | 是 |
写入 XML | 是 | 否 | 是 | 是 |
创建,读取,更新,删除 | 否 | 否 | 是 | 否 |
StAX API
StAX API 公开了用于 XML 文档的迭代式、基于事件的处理的方法。XML 文档被视为一系列经过过滤的事件,并且信息集状态可以以过程化方式存储。此外,与 SAX 不同,StAX API 是双向的,可以实现对 XML 文档的读取和写入。
StAX API 实际上是两个不同的 API 集:一个光标 API 和一个迭代器 API。这两个 API 集将在本课程的后面更详细地解释,但它们的主要特点如下所述。
光标 API
如其名称所示,StAX 光标 API 表示一个光标,您可以使用它从头到尾遍历 XML 文档。这个光标一次只能指向一件事,并且总是向前移动,从不后退,通常一次移动一个信息集元素。
两个主要的光标接口是XMLStreamReader
和XMLStreamWriter
。XMLStreamReader
包括了从 XML 信息模型中检索所有可能信息的访问方法,包括文档编码、元素名称、属性、命名空间、文本节点、起始标记、注释、处理指令、文档边界等等;例如:
public interface XMLStreamReader { public int next() throws XMLStreamException; public boolean hasNext() throws XMLStreamException; public String getText(); public String getLocalName(); public String getNamespaceURI(); // ... other methods not shown }
您可以在XMLStreamReader
上调用诸如getText
和getName
之类的方法,以获取当前光标位置的数据。XMLStreamWriter
提供了与StartElement
和EndElement
事件类型对应的方法;例如:
public interface XMLStreamWriter { public void writeStartElement(String localName) throws XMLStreamException; public void writeEndElement() throws XMLStreamException; public void writeCharacters(String text) throws XMLStreamException; // ... other methods not shown }
光标 API 与 SAX 在许多方面相似。例如,可以直接访问字符串和字符信息的方法可用,并且可以使用整数索引访问属性和命名空间信息。与 SAX 一样,光标 API 方法将 XML 信息作为字符串返回,这减少了对象分配的需求。
迭代器 API
StAX 迭代器 API 将 XML 文档流表示为一组离散的事件对象。这些事件由应用程序拉取,并由解析器按照它们在源 XML 文档中读取的顺序提供。
基本的迭代器接口称为XMLEvent
,并且为事件迭代器 API 表中列出的每种事件类型都有子接口。用于读取迭代器事件的主要解析器接口是XMLEventReader
,用于写入迭代器事件的主要接口是XMLEventWriter
。XMLEventReader
接口包含五种方法,其中最重要的是nextEvent
,它返回 XML 流中的下一个事件。XMLEventReader
实现了java.util.Iterator
,这意味着从XMLEventReader
返回的内容可以被缓存或传递给可以与标准 Java 迭代器一起工作的程序;例如:
public interface XMLEventReader extends Iterator { public XMLEvent nextEvent() throws XMLStreamException; public boolean hasNext(); public XMLEvent peek() throws XMLStreamException; // ... }
类似地,在迭代器 API 的输出端,你有:
public interface XMLEventWriter { public void flush() throws XMLStreamException; public void close() throws XMLStreamException; public void add(XMLEvent e) throws XMLStreamException; public void add(Attribute attribute) throws XMLStreamException; // ... }
迭代器事件类型
XMLEvent
在事件迭代器 API 中定义的类型
事件类型 | 描述 |
StartDocument |
报告一组 XML 事件的开始,包括编码、XML 版本和独立属性。 |
StartElement |
报告元素的开始,包括任何属性和命名空间声明;还提供了开始标记的前缀、命名空间 URI 和本地名称的访问。 |
EndElement |
报告元素的结束标记。如果已在相应的 StartElement 上显式设置了命名空间,则在此处可以调用已经超出范围的命名空间。 |
Characters |
对应于 XML CData 部分和 CharacterData 实体。请注意,可忽略的空格和重要的空格也被报告为 Character 事件。 |
EntityReference |
字符实体可以作为独立事件报告,应用程序开发人员可以选择解析或传递未解析的实体。默认情况下,实体会被解析。或者,如果不想将实体报告为事件,则可以替换文本并报告为 Characters 。 |
ProcessingInstruction |
报告底层处理指令的目标和数据。 |
Comment |
返回注释的文本。 |
EndDocument |
报告一组 XML 事件的结束。 |
DTD |
报告与流相关联的(如果有的话)DTD 的信息,并提供一种返回在 DTD 中找到的自定义对象的方法。 |
Attribute |
属性通常作为 StartElement 事件的一部分报告。然而,有时希望将属性作为独立的 Attribute 事件返回;例如,当命名空间作为 XQuery 或 XPath 表达式的结果返回时。 |
Namespace |
与属性一样,命名空间通常作为 StartElement 的一部分报告,但有时希望将命名空间作为独立的 Namespace 事件报告。 |
请注意,只有在处理的文档包含 DTD 时,才会创建 DTD
、EntityDeclaration
、EntityReference
、NotationDeclaration
和 ProcessingInstruction
事件。
事件映射示例
作为事件迭代器 API 如何映射 XML 流的示例,请考虑以下 XML 文档:
<?xml version="1.0"?> <BookCatalogue > <Book> <Title>Yogasana Vijnana: the Science of Yoga</Title> <ISBN>81-40-34319-4</ISBN> <Cost currency="INR">11.50</Cost> </Book> </BookCatalogue>
此文档将被解析为十八个主要和次要事件,如下表所示。请注意,通常从主要事件而不是直接访问,可以访问用大括号({}
)显示的次要事件。
迭代器 API 事件映射示例
# | 元素/属性 | 事件 |
1 | version="1.0" |
StartDocument |
2 | isCData = false data = "\n" IsWhiteSpace = true |
Characters |
3 | qname = BookCatalogue:http://www.publishing.org 属性 = null 命名空间 = {BookCatalogue" -> http://www.publishing.org"} |
StartElement |
4 | qname = 书 属性 = null 命名空间 = null |
StartElement |
5 | qname = 标题 属性 = null 命名空间 = null |
StartElement |
6 | isCData = false data = "Yogasana Vijnana: the Science of Yoga\n\t" IsWhiteSpace = false |
Characters |
7 | qname = Title namespaces = null |
EndElement |
8 | qname = ISBN attributes = null namespaces = null |
StartElement |
9 | isCData = false data = "81-40-34319-4\n\t" IsWhiteSpace = false |
Characters |
10 | qname = ISBN namespaces = null |
EndElement |
11 | qname = Cost attributes = {"currency" -> INR} namespaces = null |
StartElement |
12 | isCData = false data = "11.50\n\t" IsWhiteSpace = false |
Characters |
13 | qname = Cost namespaces = null |
EndElement |
14 | isCData = false data = "\n" IsWhiteSpace = true |
Characters |
15 | qname = Book namespaces = null |
EndElement |
16 | isCData = false data = "\n" IsWhiteSpace = true |
Characters |
17 | qname = BookCatalogue:http://www.publishing.org namespaces = {BookCatalogue" -> http://www.publishing.org"} |
EndElement |
18 | EndDocument |
在这个例子中有几个重要的事项需要注意:
- 事件按照文档中遇到相应的 XML 元素的顺序创建,包括元素的嵌套、打开和关闭元素、属性顺序、文档开始和文档结束等。
- 与正确的 XML 语法一样,所有容器元素都有相应的开始和结束事件;例如,每个
StartElement
都有一个对应的EndElement
,即使是空元素也是如此。 Attribute
事件被视为次要事件,并且可以从其对应的StartElement
事件中访问。- 与
Attribute
事件类似,Namespace
事件被视为次要事件,但在事件流中出现两次,并且可以从它们对应的StartElement
和EndElement
中分别访问两次。 - 所有元素都指定了
Character
事件,即使这些元素没有字符数据。同样,Character
事件可以跨事件分割。 - StAX 解析器维护一个命名空间堆栈,其中保存了当前元素及其祖先元素定义的所有 XML 命名空间信息。通过
javax.xml.namespace.NamespaceContext
接口暴露的命名空间堆栈可以通过命名空间前缀或 URI 访问。
在游标和迭代器 API 之间进行选择
此时合理地问一下,“我应该选择哪个 API?我应该创建 XMLStreamReader
还是 XMLEventReader
的实例?为什么会有两种类型的 API?”
开发目标
StAX 规范的作者针对三种类型的开发者:
- 图书馆和基础设施开发者:创建应用服务器、JAXM、JAXB、JAX-RPC 等实现;需要高效、低级别的 API,并且具有最小的可扩展性要求。
- Java ME 开发者:需要小型、简单的拉取解析库,并且具有最小的可扩展性需求。
- Java 平台企业版(Java EE)和 Java 平台标准版(Java SE)开发人员:需要干净、高效的拉取解析库,同时需要灵活性来读取和写入 XML 流,创建新的事件类型,并扩展 XML 文档元素和属性。
鉴于这些广泛的开发类别,StAX 的作者认为定义两个小型、高效的 API 比过载一个更大、必然更复杂的 API 更有用。
比较游标和迭代器 API
在选择游标和迭代器 API 之间之前,你应该注意一些你可以使用迭代器 API 而不能使用游标 API 的事项:
- 从
XMLEvent
子类创建的对象是不可变的,可以在数组、列表和映射中使用,并且可以在解析器继续处理后传递到你的应用程序中。 - 你可以创建
XMLEvent
的子类型,这些子类型可以是全新的信息项,也可以是现有项目的扩展,但具有额外的方法。 - 你可以以比游标 API 更简单的方式向 XML 事件流中添加和删除事件。
同样,在做出选择时,请记住一些一般性建议:
- 如果你正在为特别受内存限制的环境编程,比如 Java ME,你可以使用游标 API 创建更小、更高效的代码。
- 如果性能是你的最高优先级——例如,在创建低级库或基础设施时——游标 API 更有效率。
- 如果你想创建 XML 处理管道,请使用迭代器 API。
- 如果你想修改事件流,请使用迭代器 API。
- 如果你希望你的应用程序能够处理事件流的可插拔处理,请使用迭代器 API。
- 一般来说,如果你没有明确偏好,建议使用迭代器 API,因为它更灵活、可扩展,从而“未雨绸缪”你的应用程序。
使用 StAX
一般来说,StAX 程序员通过使用 XMLInputFactory
、XMLOutputFactory
和 XMLEventFactory
类来创建 XML 流读取器、写入器和事件。通过在工厂上设置属性来进行配置,可以通过在工厂上使用 setProperty
方法将特定于实现的设置传递给底层实现。类似地,可以使用 getProperty
工厂方法查询特定于实现的设置。
下面描述了 XMLInputFactory
、XMLOutputFactory
和 XMLEventFactory
类,然后讨论了资源分配、命名空间和属性管理、错误处理,最后使用游标和迭代器 API 读取和写入流。
StAX 工厂类
StAX 工厂类。XMLInputFactory
、XMLOutputFactory
和 XMLEventFactory
,让您定义和配置 XML 流读取器、流写入器和事件类的实现实例。
XMLInputFactory
XMLInputFactory
类允许您配置由工厂创建的 XML 流读取器处理器的实现实例。通过在类上调用 newInstance
方法来创建抽象类 XMLInputFactory
的新实例。然后使用静态方法 XMLInputFactory.newInstance
来创建新的工厂实例。
派生自 JAXP,XMLInputFactory.newInstance
方法通过以下查找过程确定要加载的特定 XMLInputFactory
实现类:
- 使用
javax.xml.stream.XMLInputFactory
系统属性。 - 使用 Java SE 平台的 Java Runtime Environment (JRE) 目录中的
lib/xml.stream.properties
文件。 - 如果可用,使用 Services API 通过查找 JRE 中可用的 JAR 文件中的
META-INF/services/javax.xml.stream.XMLInputFactory
文件确定类名。 - 使用平台默认的
XMLInputFactory
实例。
在获取适当的 XMLInputFactory
引用之后,应用程序可以使用工厂来配置和创建流实例。以下表格列出了 XMLInputFactory
支持的属性。详细列表请参阅 StAX 规范。
javax.xml.stream.XMLInputFactory
属性
属性 | 描述 |
isValidating |
打开实现特定的验证。 |
isCoalescing |
(必需) 要求处理器合并相邻的字符数据。 |
isNamespaceAware |
关闭命名空间支持。所有实现必须支持命名空间。对非命名空间感知文档的支持是可选的。 |
isReplacingEntityReferences |
(必需) 要求处理器用其替换值替换内部实体引用,并将其报告为字符或描述实体的事件集。 |
isSupportingExternalEntities |
(必需) 要求处理器解析外部解析实体。 |
reporter |
(必需) 设置并获取XMLReporter 接口的实现。 |
resolver |
(必需) 设置并获取XMLResolver 接口的实现。 |
allocator |
(必需) 设置并获取XMLEventAllocator 接口的实现。 |
XMLOutputFactory
通过在类上调用newInstance
方法来创建抽象类XMLOutputFactory
的新实例。然后使用静态方法XMLOutputFactory.newInstance
来创建一个新的工厂实例。用于获取实例的算法与XMLInputFactory
相同,但引用javax.xml.stream.XMLOutputFactory
系统属性。
XMLOutputFactory
只支持一个属性,即javax.xml.stream.isRepairingNamespaces
。此属性是必需的,其目的是创建默认前缀并将其与命名空间 URI 关联起来。有关更多信息,请参阅 StAX 规范。
XMLEventFactory
通过在类上调用newInstance
方法来创建抽象类XMLEventFactory
的新实例。然后使用静态方法XMLEventFactory.newInstance
来创建一个新的工厂实例。此工厂引用javax.xml.stream.XMLEventFactory
属性来实例化工厂。用于获取实例的算法与XMLInputFactory
和XMLOutputFactory
相同,但引用javax.xml.stream.XMLEventFactory
系统属性。
XMLEventFactory
没有默认属性。
Java 中文官方教程 2022 版(四十)(2)https://developer.aliyun.com/article/1488166