理解StAX

来源:互联网 发布:奥巴马外星人知乎 编辑:程序博客网 时间:2024/06/05 11:31

 
StAX是用 Java?语言处理 XML 的最新标准。作为一种面向流的方法,无论从性能还是可用性上都优于其他方法,如 DOM 和 SAX。从一开始,Java API for XML Processing (JAXP) 就提供了两种方法来处理 XML:文档对象模型(DOM)方法是用标准的对象模型表示 XML 文档;Simple API for XML (SAX) 方法使用应用程序提供的事件处理程序来处理 XML。JSR-173 提出了一种面向流的新方法:StAX。其最终版本于 2004 年 3 月发布,并成为了 JAXP 1.4(将包含在即将发布的 Java 6 中)的一部分。如其名称所暗示的那样,StAX 把重点放在流上。实际上,StAX 与其他方法的区别就在于应用程序能够把 XML 作为一个事件流来处理。将 XML 作为一组事件来处理的想法并不新颖(事实上 SAX 已经提出来了),但不同之处在于StAX 允许应用程序代码把这些事件逐个拉出来,而不用提供在解析器方便时从解析器中接收事件的处理程序。

StAX是用 Java?语言处理 XML 的最新标准。作为一种面向流的方法,无论从性能还是可用性上都优于其他方法,如 DOM 和 SAX。从一开始,Java API for XML Processing (JAXP) 就提供了两种方法来处理 XML:文档对象模型(DOM)方法是用标准的对象模型表示 XML 文档;Simple API for XML (SAX) 方法使用应用程序提供的事件处理程序来处理 XML。JSR-173 提出了一种面向流的新方法:StAX。其最终版本于 2004 年 3 月发布,并成为了 JAXP 1.4(将包含在即将发布的 Java 6 中)的一部分。如其名称所暗示的那样,StAX 把重点放在流上。实际上,StAX 与其他方法的区别就在于应用程序能够把 XML 作为一个事件流来处理。将 XML 作为一组事件来处理的想法并不新颖(事实上 SAX 已经提出来了),但不同之处在于StAX 允许应用程序代码把这些事件逐个拉出来,而不用提供在解析器方便时从解析器中接收事件的处理程序。

为了使用这两类 API,应用程序首先必须获得一个具体的 XMLInputFactory。根据传统的 JAXP 风格,要用到抽象工厂模式;XMLInputFactory 类提供了静态的 newInstance 方法,它负责定位和实例化具体的工厂。配置该实例可设置定制或者预先定义好的属性(其名称在类 XMLInputFactory 中定义)。最后,为了使用基于指针的 API,应用程序还要通过调用某个createXMLStreamReader 方法获得一个 XMLStreamReader。如果要使用基于事件迭代器的 API,应用程序就要调用createXMLEventReader 方法获得一个 XMLEventReader。
XMLStreamReader 和 XMLEventReader 都允许应用程序迭代底层的 XML 流。两种方法的差别在于:XMLStreamReader 就像一个指针,指在刚刚解析过的 XML 标记的后面,并提供了方法获得更多关于该标记的信息。这种方法节约内存,因为不用创建新的对象。但是业务应用程序开发人员可能会发现 XMLEventReader 更直观一些,因为它实际上就是一个标准的 Java 迭代器,将 XML 变成了事件对象流。每个事件对象都封装了它所表示的特定XML 结构固有的信息。

使用哪种风格的 API 取决于具体情况。和基于指针的 API 相比,基于事件迭代器的 API 具有更多的面向对象特征。因此更便于应用于模块化的体系结构,因为当前的解析器状态反映在事件对象中,应用程序组件在处理事件的时候不需要访问解析器/读取器。

基于指针的 API
如果使用基于指针的 API,应用程序通过在 XML 标记流中移动逻辑指针来处理 XML。基于指针的解析器实质上是一个状态机,在事件的驱动下从一个良好定义的状态转移到另一个状态。这里的触发事件是随着应用程序使用适当的方法推动解析器在标记流中前进而解析出来的 XML 标记。在每个状态,都可使用一组方法获得上一个事件的信息。一般来说,并非每个状态下都能使用所有的方法。

使用基于指针的方法,应用程序首先必须通过调用其createXMLStreamReader 方法从 XMLInputFactory 得到XMLStreamReader。该方法有多个版本,支持不同类型的输入。比方说,可以创建 XMLStreamReader 解析java.io.InputStream、java.io.Reader 等。

XMLStreamReader 接口基本上定义了基于指针的 API(虽然标记常量在其超类型 XMLStreamConstants 接口中定义)。之所以称为基于指针,是因为读取器就像是底层标记流上的指针。应用程序可以沿着标记流向前推进指针并分析当前指针所在位置的标记。

XMLStreamReader 提供了多种方法导航标记流。为了确定当前指针所指向的标记(或事件)的类型,应用程序可以调用 getEventType()。该方法返回接口 XMLStreamConstants 中定义的一个标记常量。移动到下一个标记,应用程序可以调用 next()。该方法也返回解析的标记的类型,如果接着调用getEventType() 则返回的值相同。只有当方法 hasNext() 返回 true 时(就是说还有其他标记需要解析)才能调用该方法(以及其他移动读取器的方法)。

还与其他几种方法可以移动 reader。 nextTag() 方法将跳过所有的空白、注释或处理指令,直到遇到 START_ELEMENT 或 END_ELEMENT。该方法在解析只含元素的内容时很有用,如果在发现标记之前遇到非空白文本(不包括注释或处理指令),就会抛出异常。getElementText() 方法返回元素的开始和关闭标签(即 START_ELEMENT 和 END_ELEMENT)之间的所有文本内容。如果遇到嵌套的元素就会抛出异常。

请注意,这里的“标记”和“事件”可以互换使用。虽然基于指针的 API 的文档说的是事件,但把输入源看成标记流很方便。而且不容易造成混乱,因为还有一整套基于事件的 API(那里的事件是真正的对象)。不过,XMLStreamReader 的事件本质上并非都是标记。比方说,START_DOCUMENT 和 END_DOCUMENT 事件不需要对应的标记。前一个事件是解析开始之前发生,后者则在没有更多解析工作要做的时候发生(比如解析完成最后一个元素的关闭标签之后,读取器处于END_ELEMENT 状态,但是如果没有发现更多的标记需要解析,读取器就会切换到 END_DOCUMENT 状态)。

处理 XML 文档

在每个解析器状态,应用程序都可通过可用的方法获得相关信息。比如,无论当前是什么类型的事件,getNamespaceContext() 和 getNamespaceURI() 方法可以获得当前有效的名称空间上下文和名称空间 URI。类似的,getLocation() 可以获得当前事件的位置信息。方法 hasName() 和 hasText() 可以分别判断当前事件是否有名称(比如元素或属性)或文本(比如字符、注释或 CDATA)。方法 isStartElement()、isEndElement()、isCharacters() 和 isWhiteSpace() 可以方便地确定当前事件的性质。最后,方法 require(int, String, String) 可以声明预期的解析器状态;除非当前事件是指定的类型,并且本地名和名称空间(如果给出的话)与当前事件匹配,否则该方法将抛出异常。

创建之后,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_INSTRUCTIO 的目标和数据。ENTITY_REFERENCE 和 DTD 也支持 getText(),ENTITY_REFERENCE 还支持 getLocalName()。

解析完成后,应用程序关闭读取器并释放解析过程中获得的资源。请注意这样并没有关闭底层的输入源。

清单 5 提供了一个完整的例子,使用基于指针的 API 处理 XML 文档。首先取得XMLInputFactory 的默认实例并创建一个 XMLStreamReader 解析给定的输入流。然后不断检查读取器的状态,根据当前事件的类型报告某些信息(比如在START_ELEMENT 状态下报告元素名及元素属性)。最后,遇到END_DOCUMENT 时关闭读取器。


publicclass Test {

publicstaticvoid main(String[] args) throws XMLStreamExceptio {

new Test().parse("person.xml");
}

publicvoid parse(String fileName) throws XMLStreamExceptio {
XMLInputFactory factory = XMLInputFactory.newInstance();

XMLStreamReader r = factory.createXMLStreamReader(Test.class .getResourceAsStream(fileName));

StringBuffer parsingResult = new StringBuffer();

try {

int event = r.getEventType();

while (true) {

switch (event) {

case XMLStreamConstants.START_DOCUMENT:

break;

case XMLStreamConstants.START_ELEMENT:

parsingResult.append("<");

parsingResult.append(r.getName());

for (int i = 0,  = r.getAttributeCount(); i < n; ++i) {

parsingResult.append(" ");

parsingResult.append(r.getAttributeName(i));

parsingResult.append("=");

parsingResult.append("/"");

parsingResult.append(r.getAttributeValue(i));

parsingResult.append("/"");

}

parsingResult.append(">");

parsingResult.append("/n");

break;

case XMLStreamConstants.CHARACTERS:

if (r.isWhiteSpace())

break;

parsingResult.append(r.getText());

parsingResult.append("/n");

break;

case XMLStreamConstants.END_ELEMENT:

parsingResult.append("<");

parsingResult.append(r.getName());

parsingResult.append(">");

parsingResult.append("/n");

break;

case XMLStreamConstants.END_DOCUMENT:

break;

}

if (!r.hasNext())

break;

event = r.next();

}

} finally {

r.close();

}

System.out.println(parsingResult);

}

}

基于事件迭代器(Iterator)的API


StAX 提供的另一种风格的 API 以事件对象为中心。和基于指针的 API 一样,这也是一种基于拉的 XML 解析方法:应用程序使用提供的方法从解析器中拉出每个事件,按照需要处理该事件,依此类推,直到流解析完成(或者应用程序决定停止解析)。

XMLEventReader 接口简介


事件迭代器 API 的主要接口是 XMLEventReader。和 XMLStreamReader 相比它的方法要少很多。这是因为 XMLEventReader 用于迭代事件对象流(事实上 XMLEventReader 扩展了 java.util.Iterator)。关于解析事件的所有信息都封装在事件对象而不是读取器中。

要使用基于事件迭代器的 API,应用程序首先必须从 XMLInputFactory 获得XMLEventReader 的实例。工厂本身可用标准 JAXP 方法获得,它依靠抽象工厂模式支持可插入的服务提供者。这就使得获取默认的XMLInputFactory 实现的实例和调用 XMLInputFactory.getInstance() 一样简单。

使用 XMLEventReader

由于接口 XMLEventReader 扩展了 java.util.Iterator,可以使用标准迭代器方法如 hasNext() 和 next()。但是请注意,不支持 remove() 方法,如果调用该方法会抛出异常。

XMLEventReader 还提供了一些方便的方法来简化 XML 处理:


1.nextEvent() 本质上是一种等同于 Iterator 的 next() 方法的强类型方法,它返回一个 XMLEvent,它是所有事件对象的基本接口。

2.nextTag() 能够跳过所有无关紧要的空白直到下一个开始或结束标记。因此返回值将是 StartElement 或 EndElement 事件(参见后述)。该方法在处理纯元素(即文档类型声明 DTD 中声明为 EMPTY 的元素)内容时尤其有用。

3.getElementText() 可以访问纯文本元素的文本内容(开始标签到结束标签之间)。从 StartElement 作为下一个预期事件开始,该方法在遇到EndElement 之前将所有字符连接起来并返回结果字符串。

4.peek() 可以得到迭代器将返回的下一个事件(如果有)但是不移动迭代器。

导航 XMLEvent 层次结构

从解析器中检索到事件之后,应用程序通常需要将其向下转换成 XMLEvent 的子类型以便访问该特定类型的信息。有多种方法,除了蛮力的 instanceof 检查(即通过一系列的 if/the 语句检查返回的事件是否实现了指定接口)以外,XMLEvent 还提供了 getEventType() 方法返回XMLStreamConstants 中定义的事件常量。可基于该信息对事件进行向下类型转换。比方说,如果事件的 getEventType() 返回 START_ELEMENT,它就可以安全地转换成 StartElement。

确定事件具体类型的另一种方法是使用为此提供的布尔查询方法。比如,如果事件是一个 Attribute 则 isAttribute() 返回 true,如果是 StartElement 则isStartElement() 返回 true,等等。此外还有几种方便的方法可用于向下类型转换。asStartElement()、asEndElement() 和 asCharacters() 分别将相应的事件转换成 StartElement、EndElement 和 Characters。

StAX并不像SAX一般有众多的事件,而只有StartElement、Characters、EndElement共3种事件。开发人员只需判断目前处于何种事件状态,然后执行相应的业务逻辑。

StAX允许从事件中反向得到xml节点对象,这为开发人员提供了巨大的方便,开发人员可以可以基于这一点来进一步操作节点属性等对象。从事件中反向得到xml节点的方法如下:

Javax.xml.stream.events.XMLEvent.asStartElement()

Javax.xml.stream.events.XMLEvent.asEndElement()

处理 XML 文档

解析表示完整 XML 文档的流时,XMLEventReader 返回的第一个事件是 StartDocument。该接口提供了获得文档本身信息的方法。比如,getSystemId() 方法可以返回文档的系统 ID(如果知道的话)。getVersion() 返回该文档使用的 XML 版本。默认的版本是 1.0,除非在文档的 XML 声明中指定了其他值。

getCharacterEncodingScheme() 返回文档的字符编码,不论在 XML 声明中显式指定还是解析器自动检测。默认值为 UTF-8,除非给出了外部标记声明或者在文档 XML 声明中显式指定了该值,否则 isStandalone() 返回 true。

访问 DTD

如果 XMLEventReader 遇到 DTD 则返回 DTD 事件。如果应用程序不关心 DTD,可以通过将解析器的 javax.xml.stream.supportDTD 属性设置为 false 来关闭该特性。事件的 getDocumentTypeDeclaration() 方法可以将整个 DTD 作为一个字符串检索,包括内部子集。这个实现实际上可将 DTD 处理成更加结构化的形式(特定于提供的)并通过调用 getProcessedDTD() 方法使其可用。getEntities() 方法返回 EntityDeclaratio 事件列表(参见后述),这些事件表示一般外部实体声明,包括内部和外部实体。此外,getNotations() 方法返回 NotationDeclaratio 事件列表(同样将在后面说明),用于表示声明的符号。

EntityDeclaratio 事件表示在文档的 DTD 中声明的非解析的一般实体。该事件不被单独报告,而是作为 DTD 事件的一部分。它提供了用于获取实体的名称、公共和系统 ID 以及相关的符号名的方法(分别使用 getName()、getPublicId()、getSystemId() 和 getNotationName())。如果是内部实体,getReplacementText() 方法可检索其替换文本。

类似的,NotationDeclaratio 事件也只能通过 DTD 事件访问。它表示符号声明。除了名称以外(getName() 方法),该接口还提供了检索符号的公共和系统 ID 的方法(分别是 getPublicId() 和 getSystemId())。两者至少要有一个可用。

处理元素、属性和名称空间声明

对每个元素,XMLEventReader 都返回 StartElement 事件表示其开始标记,最后还有对应的 EndElement 事件表示结束标记。即使没有单独的开始和结束标记的空元素(比如 <empty-element/>),读取器也会在StartElement 之后接着返回 EndElement 事件。

和其他事件相比可能会经常处理 StartElement,因为它通常用于表示 XML 文档的大部分信息。检索元素的限定名可调用 getName()。类 QName 表示XML 限定名,它将限定名中的所有成分(如名称空间 URI、前缀和本地名)封装起来。getNamespaceContext() 方法可以检索当前的名称空间上下文,包括当前所有名称空间的信息。检索元素的属性使用 getAttributes() 或者用getAttributeByName(QName) 按属性名逐个检索(如果事先知道的话)。类似的,可以调用 getNamespaces() 获得元素上声明的任何名称空间。getNamespaceURI(String) 返回捆绑到当前上下文中特定前缀的名称空间。

虽然被建模为事件并用接口 Attribute 表示,但元素的属性一般不作为单独的事件报告。而是通过 StartElement 事件访问。getName() 方法返回属性的限定名,getValue() 用字符串返回属性值。调用 isSpecified() 确定元素中是否指定了该属性,或者文档模式提供了默认值。方法 getDTDType() 返回属性的声明类型(如 CDATA、IDREF 或 NMTOKEN)。

类似的,元素中声明的所有名称空间都可通过 StartElement 事件访问而不需要单独报告。接口 Namespace 实际上是扩展了 Attribute,因为名称空间事实上被指定为元素的属性(包括特定的前缀)。方法 getPrefix() 是获得名称空间属性的本地名的快捷方式(除非是默认名称空间声明,这种情况下前缀是一个空字符串而非“xmlns”)。与此类似,getNamespaceURI() 方法返回属性值(即名称空间 URI)。为了判断该名称空间是否是默认名称空间(具有空前缀),可调用 isDefaultNamespaceDeclaration()。

EndElement 表示元素的结束标记(或元素标记的结束,如果是空元素的话)。可使用 getName() 方法获取元素的限定名,使用 getNamespaces() 确定那些名称空间超出了作用域。

import javax.xml.stream.XMLInputFactory;

import javax.xml.stream.XMLEventReader;

import javax.xml.stream.XMLStreamException;

import javax.xml.stream.events.XMLEvent;

import javax.xml.stream.events.StartElement;

import javax.xml.stream.events.EndElement;

import javax.xml.namespace.QName;

public class TestStAX {

public static void main(String[] args) throws XMLStreamExceptio {

new TestStAX().parse("books.xml");

}

private void parse(String fileName) throws XMLStreamExceptio {

XMLInputFactory xmlif = XMLInputFactory.newInstance();

XMLEventReader xmler = xmlif.createXMLEventReader(TestStAX.class .getResourceAsStream(fileName));

XMLEvent event;

StringBuffer parsingResult = new StringBuffer();

while (xmler.hasNext()) {

event = xmler.nextEvent();

if (event.isStartElement()) {

// 如果解析的是起始标记

StartElement se = event.asStartElement();

parsingResult.append("<");

parsingResult.append(se.getName());

for (Iterator i = se.getAttributes(); i.hasNext();) {

Attribute attr = (Attribute) i.next();

parsingResult.append(" ");

parsingResult.append(attr);

}

parsingResult.append(">");

}

elseif (event.isCharacters()) {

// 如果解析的是文本内容

parsingResult.append(event.asCharacters().getData());

} elseif (event.isEndElement()) {

// 如果解析的是结束标记

EndElement ee = event.asEndElement();

parsingResult.append("</");

parsingResult.append(ee.getName());

parsingResult.append(">");

}

}

System.out.println(parsingResult);

}

}

<?xml version="1.0" encoding="UTF-8"?>

<books>

 <book id="1">上下五千年</book>

 <book id="2">十万个为什么</book>

</books>