Streaming API for XML (StAX) 是用 Java™ 语言处理 XML 的最新标准。作为一种面向流的方法,无论从性能还是可用性上都优于其他方法,如 DOM 和 SAX。本系列分为 3 部分,本文是第 1 部分,简要介绍了 StAX 及其处理 XML 的基于指针的 API。
从 一开始,Java API for XML Processing (JAXP) 就提供了两种方法来处理 XML:文档对象模型(DOM)方法是用标准的对象模型表示 XML 文档;Simple API for XML (SAX) 方法使用应用程序提供的事件处理程序来处理 XML。JSR-173 提出了一种面向流的新方法:Streaming API for XML (StAX)。其最终版本于 2004 年 3 月发布,并成为了 JAXP 1.4(将包含在即将发布的 Java 6 中)的一部分。
如其名称所暗示的那样,StAX 把重点放在流 上。 实际上,StAX 与其他方法的区别就在于应用程序能够把 XML 作为一个事件流来处理。将 XML 作为一组事件来处理的想法并不新颖(事实上 SAX 已经提出来了),但不同之处在于 StAX 允许应用程序代码把这些事件逐个拉出来,而不用提供在解析器方便时从解析器中接收事件的处理程序。
StAX 实际上包括两套处理 XML 的 API,分别提供了不同程度的抽象。基于指针的 API 允许应用程序把 XML 作为一个标记(或事件)流来处理;应用程序可以检查解析器的状态,获得解析的上一个标记的信息,然后再处理下一个标记,依此类推。这是一种低层 API,尽管效率高,但是没有提供底层 XML 结构的抽象。较为高级的基于迭代器的 API 允许应用程序把 XML 作为一系列事件对象来处理,每个对象和应用程序交换 XML 结构的一部分。应用程序只需要确定解析事件的类型,将其转换成对应的具体类型,然后利用其方法获得属于该事件的信息。
为了使用这两类 API,应用程序首先必须获得一个具体的 XMLInputFactory
。根据传统的 JAXP 风格,要用到抽象工厂模式;XMLInputFactory
类提供了静态的 newInstance
方法,它负责定位和实例化具体的工厂。配置该实例可设置定制或者预先定义好的属性(其名称在类 XMLInputFactory 中定义)。最后,为了使用基于指针的 API,应用程序还要通过调用某个 createXMLStreamReader
方法获得一个 XMLStreamReader
。如果要使用基于事件迭代器的 API,应用程序就要调用 createXMLEventReader
方法获得一个 XMLEventReader
(如清单 1 所示)。
清单 1. 获取和配置默认的 XMLInputFactory
// get the default factory instance XMLInputFactory factory = XMLInputFactory.newInstance(); // configure it to create readers that coalesce adjacent character sections factory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE); XMLStreamReader r = factory.createXMLStreamReader(input); // ... |
XMLStreamReader
和 XMLEventReader
都允许应用程序迭代底层的 XML 流。两种方法的差别在于如何公开解析后的 XML InfoSet 信息片段。XMLStreamReader
就像一个指针,指在刚刚解析过的 XML 标记的后面,并提供了方法获得更多关于该标记的信息。这种方法节约内存,因为不用创建新的对象。但是,业务应用程序开发人员可能会发现 XMLEventReader
更直观一些,因为它实际上就是一个标准的 Java 迭代器,将 XML 变成了事件对象流。每个事件对象都封装了它所表示的特定 XML 结构固有的信息。本系列的第二部分将详细讨论这种基于事件迭代器的 API。
使 用哪种风格的 API 取决于具体情况。和基于指针的 API 相比,基于事件迭代器的 API 具有更多的面向对象特征。因此更便于应用于模块化的体系结构,因为当前的解析器状态反映在事件对象中,应用程序组件在处理事件的时候不需要访问解析器/读 取器。此外,还可以使用 XMLInputFactory
的 createXMLEventReader(XMLStreamReader)
方法从 XMLStreamReader
创建 XMLEventReader
。
StAX 还定义了一种序列化器 API,Java 标准 XML 处理支持中一直缺少的一种特性。和解析一样,也包含两种风格的流式 API:处理标记的底层 XMLStreamWriter
和处理事件对象的高层 XMLEventWriter
。XMLStreamWriter
提供了写入单个 XML 记号(比如开始和关闭标记或者元素属性)的方法,不检查这些标记是否格式良好。另一方面,XMLEventWriter
允许应用程序向输出中添加完整的 XML 事件。第 3 部分将详细讨论 StAX 序列化器 API。
开 始学习一种新的处理 XML 的 API 之前,可能要问是否值得这样做。事实上,StAX 所采用的基于拉的方法和其他方法相比有一些突出的优点。首先,不管使用哪种 API 风格,都是应用程序调用读取器(解析器)而不是相反。通过保留解析过程的控制权,可以简化调用代码来准确地处理它预期的内容。或者发生意外时停止解析。此 外,由于该方法不基于处理程序回调,应用程序不需要像使用 SAX 那样模拟解析器的状态。
StAX 仍然保留了 SAX 相对于 DOM 的优点。通过把重心从结果对象模型转移到解析流本身,从理论上说应用程序能够处理无限的 XML 流,因为事件固有的临时性,不会在内存中累积起来。对于那些使用 XML 作为消息传递协议而非表示文档内容的那些应用程序尤其重要,比如 Web 服务或即时消息应用程序。比方说,如果只是将其转换成特定于应用程序的对象模型然后就将其丢弃,那么为 Web 服务路由器 servlet 提供一个 DOM 就没有多少用处。使用 StAX 直接转化成应用程序模型效率更高。对于 Extensible Messaging and Presence Protocol(XMPP)客户机,根本不能使用 DOM,因为 XMPP 客户机/服务器流是随着用户输入的消息实时生成。等待流的关闭标签(以便最终建立 DOM)就意味着等待整个会话结束。通过把 XML 作为一系列的事件来处理,应用程序能够以最合适的方式响应每个事件(比如显示收到的即时消息等等)。
由于其双 向性,StAX 也支持链式处理,特别是在事件层上。接收事件(无论什么来源)的能力被封装在 XMLEventConsumer(XMLEventWriter 的扩展)接口中。因此,可以模块化地编写应用程序从 XMLEventReader(也是一个普通的迭代器,可以按迭代器处理)读取和处理 XML 事件、然后传递给事件消费者(如果需要可以进一步扩展处理链)。在第 2 部分将看到,也可使用应用程序提供的筛选器(实现了 EventFilter 接口的类)来定制 XMLEventReader 或者使用 EventReaderDelegate 修饰已有的 XMLEventReader。
总而言之,和 DOM 以及 SAX 相比,StAX 使应用程序更贴近底层的 XML。使用 StAX,应用程序不仅可以建立需要的对象模型(而不需要处理标准 DOM),而且可以随时这样做,而不必等到解析器回调。
下一节将深入讨论基于指针的 API 以及如何有效地使用它处理 XML 流。
如 果使用基于指针的 API,应用程序通过在 XML 标记流中移动逻辑指针来处理 XML。基于指针的解析器实质上是一个状态机,在事件的驱动下从一个良好定义的状态转移到另一个状态。这里的触发事件是随着应用程序使用适当的方法推动解 析器在标记流中前进而解析出来的 XML 标记。在每个状态,都可使用一组方法获得上一个事件的信息。一般来说,并非每个状态下都能使用所有的方法。
使用基于指针的方法,应用程序首先必须通过调用其 createXMLStreamReader
方法从 XMLInputFactory
得到 XMLStreamReader
。该方法有多个版本,支持不同类型的输入。比方说,可以创建 XMLStreamReader
解析 plain java.io.InputStream
、java.io.Reader
或者 JAXP Source(javax.xml.transform.Source
)。从理论上说,后一种办法很容易和其他 JAXP 技术交互,比如 SAX 和 DOM。
清单 2. 创建 XMLStreamReader 解析 InputStream
URL url = new URL(uri); InputStream input = url.openStream(); XMLInputFactory factory = XMLInputFactory.newInstance(); XMLStreamReader r = factory.createXMLStreamReader(uri, input); // process the stream // ... r.close(); input.close(); |
XMLStreamReader
接口基本上定义了基于指针的 API(虽然标记常量在其超类型 XMLStreamConstants
接口中定义)。之所以称为基于指针,是因为读取器就像是底层标记流上的指针。应用程序可以沿着标记流向前推进指针并分析当前指针所在位置的标记。
XMLStreamReader
提供了多种方法导航标记流。为了确定当前指针所指向的标记(或事件)的类型,应用程序可以调用 getEventType()
。该方法返回接口 XMLStreamConstants
中定义的一个标记常量。移动到下一个标记,应用程序可以调用 next()
。该方法也返回解析的标记的类型,如果接着调用 getEventType()
则返回的值相同。只有当方法 hasNext()
返回 true 时(就是说还有其他标记需要解析)才能调用该方法(以及其他移动读取器的方法)。
清单 3. 使用 XMLStreamReader 处理 XML 的常用模式
// create an XMLStreamReader XMLStreamReader r = ...; try { int event = r.getEventType(); while (true) { switch (event) { case XMLStreamConstants.START_DOCUMENT: // add cases for each event of interest // ... } if (!r.hasNext()) break; event = r.next(); } } finally { r.close(); } |
还与其他几种方法可以移动 reader
。 nextTag()
方法将跳过所有的空白、注释或处理指令,直到遇到 START_ELEMENT
或 END_ELEMENT
。该方法在解析只含元素的内容时很有用,如果在发现标记之前遇到非空白文本(不包括注释或处理指令),就会抛出异常。getElementText()
方法返回元素的开始和关闭标签(即 START_ELEMENT
和 END_ELEMENT
)之间的所有文本内容。如果遇到嵌套的元素就会抛出异常。
请注意,这里的 “标记” 和 “事件” 可以互换使用。虽然基于指针的 API 的文档说的是事件,但把输入源看成标记流很方便。而且不容易造成混乱,因为还有一整套基于事件的 API(那里的事件是真正的对象)。不过,XMLStreamReader
的事件本质上并非都是标记。比方说,START_DOCUMENT
和 END_DOCUMENT
事件不需要对应的标记。前一个事件是解析开始之前发生,后者则在没有更多解析工作要做的时候发生(比如解析完成最后一个元素的关闭标签之后,读取器处于 END_ELEMENT
状态,但是如果没有发现更多的标记需要解析,读取器就会切换到 END_DOCUMENT
状态)。
在每个解析器状态,应用程序都可通过可用的方法获得相关信息。比如,无论当前是什么类型的事件,getNamespaceContext()
和 getNamespaceURI()
方法可以获得当前有效的名称空间上下文和名称空间 URI。类似的,getLocation()
可以获得当前事件的位置信息。方法 hasName()
和 hasText()
可以分别判断当前事件是否有名称(比如元素或属性)或文本(比如字符、注释或 CDATA)。方法 isStartElement()
、isEndElement()
、isCharacters()
和 isWhiteSpace()
可以方便地确定当前事件的性质。最后,方法 require(int
, String
, String
) 可以声明预期的解析器状态;除非当前事件是指定的类型,并且本地名和名称空间(如果给出的话)与当前事件匹配,否则该方法将抛出异常。
清单 4. 如果当前事件是 START_ELEMENT 使用有关的属性方法
if (reader.getEventType() == XMLStreamConstants.START_ELEMENT) { System.out.println("Start Element: " + reader.getName()); for(int i = 0, n = reader.getAttributeCount(); i < n; ++i) { QName name = reader.getAttributeName(i); String value = reader.getAttributeValue(i); System.out.println("Attribute: " + name + "=" + value); } } |
创建之后,XMLStreamReader
将从 START_DOCUMENT
状态开始(即 getEventType()
返回 START_DOCUMENT
)。处理标记的时候应考虑到这一点。和迭代器不同,不需要先移动指针(使用 next()
)来进入合法的状态。同样地,当读取器转换到最终状态 END_DOCUMENT
之后,应用程序也不应再移动它。在这种状态下,hasNext()
方法将返回 false。
START_DOCUMENT
事件提供了获取关于文档本身信息的方法,如 getEncoding()
、getVersion()
和 isStandalone()
。应用程序也可调用 getProperty(String)
获得命名属性的值,不过一些属性仅在特定状态做了定义(比方说,如果当前事件是 DTD,则属性 javax.xml.stream.notations
和 javax.xml.stream.entities
分别返回所有的符号和实体声明)。
在 START_ELEMENT
和 END_ELEMENT
事件中,可以使用和元素名称以及名称空间有关的方法(如 getName()
、getLocalName()
、getPrefix()
和 getNamespaceXXX()
),在 START_ELEMENT
事件中还可使用与属性有关的方法(getAttributeXXX()
)。
ATTRIBUTE
和 NAMESPACE
也被识别为独立的事件,虽然在解析 典型的 XML 文档时不会用到。但是当 ATTRIBUTE
或 NAMESPACE
节点作为 XPath 查询结果返回时可以使用。
和基于文本的事件(如 CHARACTERS
、CDATA
、COMMENT
和 SPACE
),可使用各种 getTextXXX()
方法取得文本。可以分别使用 getPITarget()
和 getPIData()
检索 PROCESSING_INSTRUCTION
的目标和数据。ENTITY_REFERENCE
和 DTD
也支持 getText()
,ENTITY_REFERENCE
还支持 getLocalName()
。
解析完成后,应用程序关闭读取器并释放解析过程中获得的资源。请注意这样并没有关闭底层的输入源。
清单 5 提供了一个完整的例子,使用基于指针的 API 处理 XML 文档。首先取得 XMLInputFactory
的默认实例并创建一个 XMLStreamReader
解析给定的输入流。然后不断检查读取器的状态,根据当前事件的类型报告某些信息(比如在 START_ELEMENT
状态下报告元素名及元素属性)。最后,遇到 END_DOCUMENT
时关闭读取器。
清单 5. 使用 XMLStreamReader 解析 XML 文档的完整例子
XMLInputFactory factory = XMLInputFactory.newInstance(); XMLStreamReader r = factory.createXMLStreamReader(input); try { int event = r.getEventType(); while (true) { switch (event) { case XMLStreamConstants.START_DOCUMENT: out.println("Start Document."); break; case XMLStreamConstants.START_ELEMENT: out.println("Start Element: " + r.getName()); for(int i = 0, n = r.getAttributeCount(); i < n; ++i) out.println("Attribute: " + r.getAttributeName(i) + "=" + r.getAttributeValue(i)); break; case XMLStreamConstants.CHARACTERS: if (r.isWhiteSpace()) break; out.println("Text: " + r.getText()); break; case XMLStreamConstants.END_ELEMENT: out.println("End Element:" + r.getName()); break; case XMLStreamConstants.END_DOCUMENT: out.println("End Document."); break; } if (!r.hasNext()) break; event = r.next(); } } finally { r.close(); } |
通过调用 XMLInputFactory
的带有基本读取器的 createFilteredReader
方法和一个应用程序定义的筛选器(即实现 StreamFilter
的类实例),可以创建筛选过的 XMLStreamReader
。 导航筛选过的读取器时,读取器每次移动到下一个标记之前都会询问筛选器。如果筛选器认可了当前事件,就将其公开给筛选过的读取器。否则跳过这个标记并检查 下一个,依此类推。这种方法可以让开发人员创建一个仅处理解析内容子集的基于指针的 XML 处理程序,并与针对不同的扩展的内容模型的筛选器结合使用。
执行更复杂的流操作,可以创建 StreamReaderDelegate
的子类并重写合适的方法。然后使用这个子类的实例包装基本 XMLStreamReader
,从而为应用程序提供一个修改过的基本 XML 流的视图。可通过这种技术对 XML 流执行简单的转换,比如筛掉或者替换特定的标记,甚至增加新的标记。
清单 6 用定制的 StreamReaderDelegate
包装了基本 XMLStreamReader
,重写了 next()
方法来跳过 COMMENT
和 PROCESSING_INSTRUCTION
事件。使用该读取器时,应用程序不用担心会遇到这种类型的标记。
清单 6. 使用定制的 StreamReaderDelegate 筛选注释和处理指令
URL url = new URL(uri); InputStream input = url.openStream(); XMLInputFactory f = XMLInputFactory.newInstance(); XMLStreamReader r = f.createXMLStreamReader(uri, input); XMLStreamReader fr = new StreamReaderDelegate(r) { public int next() throws XMLStreamException { while (true) { int event = super.next(); switch (event) { case XMLStreamConstants.COMMENT: case XMLStreamConstants.PROCESSING_INSTRUCTION: continue; default: return event; } } } }; try { int event = fr.getEventType(); while (true) { switch (event) { case XMLStreamConstants.COMMENT: case XMLStreamConstants.PROCESSING_INSTRUCTION: // this should never happen throw new IllegalStateException("Filter failed!"); default: // process XML normally } if (!fr.hasNext()) break; event = fr.next(); } } finally { fr.close(); } input.close(); |
可以看到,基于指针的 API 主要是为了提高效率。所有的状态信息可以直接从流读取器获得,不需要创建额外的对象。非常适用于性能和低内存占用至关重要的应用程序。
人们早就认识到了拉式 XML 解析的好处。事实上,StAX 本身源于一种称为 XML Pull Parsing 的方法。XML Pull Parser API 类似于 StAX 所提供的基于指针的 API,可以通过分析解析器的状态获得上一个解析事件的信息,然后移动到下一个,依此类推。但没有提供基于事件迭代器的 API。这是一种非常轻型的方法,特别适合资源受限的环境,比如 J2ME。但是,很少有实现提供企业级特性如验证,因此 XML Pull 一直未受到企业 Java 开发人员的关注。
基于以往拉式解析器实现的经验,StAX 的创建者选择了在基于指针的 API 之外增加一种面向对象的 API。虽然 XMLEventReader
接口看起来似乎很简单,但是基于事件迭代器的方法具有一个基于指针的方法不具备的重要优点。通过将解析器事件变成一级对象,从而让应用程序可以采用面向对象的方式处理它们。这样做有助于模块化和不同应用程序组件之间的代码重用。
清单 7. 使用 StAX XMLEventReader 解析 XML
XMLInputFactory inputFactory = XMLInputFactory.newInstance(); XMLEventReader reader = inputFactory.createXMLEventReader(input); try { while (reader.hasNext()) { XMLEvent e = reader.nextEvent(); if (e.isCharacters() && ((Characters) e).isWhiteSpace()) continue; out.println(e); } } finally { reader.close(); } |
本文介绍了 StAX 及其基于指针的 API。第 2 部分将深入讨论事件迭代器 API。