2007-10-08

使用StAX解析XML: 拉式解析和事件

关键字: XML StAX

2007 年 7 月 05 日

Streaming API for XML (StAX) 的基于事件迭代器 API 无论在性能还是在可用性上都有其他 XML 处理方法所不及的独到之处。第 1 部分介绍了 StAX 并详细讨论了它的基于指针的 API。本文进一步讨论基于事件迭代器 API 及其为 Java™ 开发人员带来的好处。


第 1 部分(请参阅 参考资料) 提到,StAX 提供了两种风格的处理 XML 的 API。基于指针的 API 是解析 XML 的低层方法。使用这种方法,应用程序沿着 XML 标记流移动指针,在每一步中检查解析器的状态来了解解析内容的更多信息。这种方法效率很高,特别适用于资源受限的环境。但是,基于指针的 API 不是面向对象的,因而不适合 Java 应用程序,尤其是在代码的可扩展性和可维护性与性能同样重要的企业领域中就更是如此。比方说,多层 Web 服务使用一般组件处理消息信封,而把特定于消息的内容处理(如参数绑定)委托给其他组件完成,这种情况下就能从面向对象的方法中获益。

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


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

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



                
String uri = "http://www.atomenabled.org/atom.xml";
URL url = new URL(uri);
InputStream input = url.openStream();
XMLInputFactory factory = XMLInputFactory.newInstance();
XMLEventReader reader = factory.createXMLEventReader(uri, input);
...

XMLInputFactory 支持各种可用于创建 XMLEventReader 的输入源。除了 Java I/O 包中的 InputStreamReader 之外,还支持 JAXP Source(来自 TrAX),后者有助于集成 StAX 和 JAXP 的转换 API(TrAX)。最后,还可以从 XMLStreamReader 创建 XMLEventReader。这种用法可以很好地说明基于事件迭代器的 API 如何堆叠于基于指针的 API 之上。事实上,实现通常要使用其他输入源创建一个 XMLStreamReader,然后再用它创建 XMLEventReader


创建 XMLEventReader 之后,应用程序可用它迭代表示底层 XML 流的 InfoSet 片段的事件。由于接口 XMLEventReader 扩展了 java.util.Iterator,可以使用标准迭代器方法如 hasNext()next()。但是请注意,不支持 remove() 方法,如果调用该方法会抛出异常。

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

  • nextEvent() 本质上是一种等同于 Iterator 的 next() 方法的强类型方法,它返回一个 XMLEvent,它是所有事件对象的基本接口。
  • nextTag() 能够跳过所有无关紧要的空白直到下一个开始或结束标记。因此返回值将是 StartElementEndElement 事件(参见后述)。该方法在处理纯元素(即文档类型声明 DTD 中声明为 EMPTY 的元素)内容时尤其有用。
  • getElementText() 可以访问纯文本元素的文本内容(开始标签到结束标签之间)。从 StartElement 作为下一个预期事件开始,该方法在遇到 EndElement 之前将所有字符连接起来并返回结果字符串。
  • peek() 可以得到迭代器将返回的下一个事件(如果有)但是不移动迭代器。

清单 2 示范了 XMLEventReader 方法如何用于迭代 Atom 提要。Atom 是用于 Web 发布的一种连锁格式。该例首先获得 XMLInputFactory 的默认实例然后用它创建 XMLEventReader 来解析给定 URL 的 Atom 提要。迭代事件的过程中,peek() 方法判定下一个事件是否从 icon 元素开始,该元素包含提要的图标 URL。如果是,则用 getElementText() 方法获取元素的文本内容(即图标 URL),然后停止迭代。



                
final QName ICON = new QName("http://www.w3.org/2005/Atom", "icon");
URL url = new URL(uri);
InputStream input = url.openStream();

XMLInputFactory factory = XMLInputFactory.newInstance();
XMLEventReader reader = factory.createXMLEventReader(uri, input);
try {
while (reader.hasNext()) {
XMLEvent event = reader.peek();
if (event.isStartElement()) {
StartElement start = event.asStartElement();
if (ICON.equals(start.getName())) {
System.out.println(reader.getElementText());
break;
}
}

reader.nextEvent();
}
} finally {
reader.close();
}

input.close();

返回的事件对象是不变的,应用程序可以保存到解析过程之后。但是应用程序也可定义不同的事件保留(或重用)策略,请参阅第 3 部分。

应用程序也可用 getProperty(String) 从底层实现中获得一个定制的或预先定义的属性值。完成之后,应用程序应该调用读取器的 close() 方法关闭它,以便释放处理所占用的资源。

使用 XMLEventReader 迭代事件流非常简单。处理这些事件需要知道和熟悉 StAX XMLEvent 的层次结构,下面我们来讨论它。



前面已经强调过,XMLEventReader 在解析过程的每一步之后通过事件对象和应用程序通信自己的状态。整个 API 中使用的事件对象的标准类型定义在 javax.xml.stream.events 包中。接口 XMLEvent 表示类型层次结构的根,所有类型的事件必须扩展该接口。表示各种指针层事件类型(在基于指针的 API 中)定义在接口 XMLStreamConstants 中。不过,在第 3 部分将看到也可使用定制的接口(只要扩展了 XMLEvent)。


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

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

清单 3 中首先使用 isStartElement()asStartElement() 方法确定检索的事件是否是 StartElement,然后将其向下转换成 StartElement 类型,从而访问元素名。


                
// get an event from the reader
...
if (event.isStartElement()) {
StartElement start = event.asStartElement();
// use methods provided by StartElement
...

除了和类型层次结构有关的方法外,XMLEventType 还提供了 getLocation()getSchemaType()writeAsEncodedUnicode(Writer) 方法。getLocation() 返回的 Location 对象提供了关于事件在底层输入源中的位置(比如该事件结束的行列号)的可选信息。getSchemaType() 用于检索和给定事件有关的 XML Schema 信息(如果实现支持该功能)。writeAsEncodedUnicode(Writer) 方法以标准的方式定义了将事件对象写入 java.io.Writer 的契约。这些方法对于定义定制的事件(将在下一期讨论)特别有用,因为可以让序列化器委托 XMLEvent 派生类的序列化而不需要应用程序使用定制的序列化器。



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

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


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

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

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

清单 4 示范了如何处理非解析外部实体引用。该例中,虚构的 catalog 文档包含了对内容放在 PDF 或 HTML 文件(两者都不是有效的 XML)的出版物的引用。迭代这些事件的过程中,从 DTD 事件中提取符号声明并按照名字缓存。遇到实体引用时,取得实体声明并根据名称检索缓存的符号声明。实际的应用程序可能会使用符号标识符来定位适当的内容处 理程序并使用实体的系统标识符作为其输入。



                
final String xml = "<?xml version=\"1.0\" standalone=\"no\" ?>" +
"<!DOCTYPE catalog [" +
"<!ELEMENT catalog (publication+) >" +
"<!ELEMENT publication (#PCDATA) >" +
"<!ATTLIST publication title CDATA #REQUIRED >" +
"<!NOTATION pdf SYSTEM \"application/pdf\" >" +
"<!NOTATION html SYSTEM \"text/html\" >" +
"<!ENTITY overview SYSTEM \"resources/overview.pdf\" NDATA pdf
>" +
"<!ENTITY chapter1 SYSTEM \"resources/chapter_1.html\" NDATA html
>" +
"]>" +
"<catalog>" +
"<ext title=\"Overview\">&overview;</ext>" +
"<ext title=\"Chapter 1\">&chapter1;</ext>" +
"</catalog>";
Map notations = new HashMap();
StringReader input = new StringReader(xml);
XMLInputFactory f = XMLInputFactory.newInstance();
XMLEventReader r = f.createXMLEventReader("http://example.com/catalog.xml",
input);
PrintWriter out = new PrintWriter(System.out);
try {
while (r.hasNext()) {
XMLEvent event = r.nextEvent();
switch (event.getEventType()) {
case XMLStreamConstants.ENTITY_REFERENCE:
EntityReference ref = (EntityReference) event;
EntityDeclaration decl = ref.getDeclaration();
NotationDeclaration n = (NotationDeclaration)
notations.get(decl.getNotationName());

out.print("Object of type ");
out.print(n.getSystemId());
out.print(" located at ");
out.print(decl.getSystemId());
out.print(" would be placed here.");
break;
case XMLStreamConstants.DTD:
DTD dtd = (DTD) event;
for (Iterator i = dtd.getNotations().iterator(); i.hasNext();)
{
n = (NotationDeclaration) i.next();
notations.put(n.getName(), n);
}
default:
event.writeAsEncodedUnicode(out);
out.println();
}
}
} finally {
r.close();
}

input.close();
out.flush();


对每个元素,XMLEventReader 都返回 StartElement 事件表示其开始标记,最后还有对应的 EndElement 事件表示结束标记。即使没有单独的开始和结束标记的空元素(比如 ),读取器也会在 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() 确定那些名称空间超出了作用域。

清单 5 报告所有的 Atom 扩展元素和属性(即不属于 Atom 名称空间或 XML 名称空间的元素和属性)。对每个 StartElement 事件都检查其名称空间 URI 是否是 Atom 名称空间 URI。然后迭代所有的属性,使用 Attribute 接口获得属性名。最后报告不属于 Atom 或 XML 名称空间的属性。



                
final String ATOM_NS = "http://www.w3.org/2005/Atom";

URL url = new URL(uri);
InputStream input = url.openStream();
XMLInputFactory f = XMLInputFactory.newInstance();
XMLEventReader r = f.createXMLEventReader(uri, input);
try {
while (r.hasNext()) {
XMLEvent event = r.nextEvent();
if (event.isStartElement()) {
StartElement start = event.asStartElement();
boolean isExtension = false;
boolean elementPrinted = false;
if (!ATOM_NS.equals(start.getName().getNamespaceURI())) {
System.out.println(start.getName());
isExtension = true;
elementPrinted = true;
}

for (Iterator i = start.getAttributes(); i.hasNext();) {
Attribute attr = (Attribute) i.next();
String ns = attr.getName().getNamespaceURI();
if (ATOM_NS.equals(ns))
continue;

if ("".equals(ns) && !isExtension)
continue;

if ("xml".equals(attr.getName().getPrefix()))
continue;

if (!elementPrinted) {
elementPrinted = true;
System.out.println(start.getName());
}

System.out.print("\t");
System.out.println(attr);
}
}
}
} finally {
r.close();
}

input.close();


Characters 事件实际上用于表示三类文本事件:实际内容的文本(CHARACTERS)、CDATA 部分以及可忽略的空白(SPACE)。它提供了区分这三种文本类型的方法,如果是 CDATA 事件则 isCData() 返回 true,如果是 SAPCE 事件则 isIgnorableWhitespace() 返回 true。getData() 方法返回该事件的文本。此外,isWhiteSpace() 说明文本是否全部由空白字符组成(不一定是可忽略的空白)。

未解析的一般实体引用由 EntityReference 事件报告。只有当读取器的 javax.xml.stream.isReplacingEntityReferences 属性设为 false 时才会报告解析实体。否则,就要求解析器用替换文本(在声明中做了指定)代替内部实体引用并作为一般字符事件报告,如果是解析的外部实体则作为正常标记报告。接口 EntityReference 提供了获取实体名及其声明的方法(作为 EntityDeclaration 事件),即通过分别调用 getName()getDeclaration()

事件 PROCESSING_INSTRUCTIONCOMMENTS 分别用 ProcessingInstructionComment 表示。ProcessingInstruction 提供了 getTarget()getData() 方法,用来检索指令的目标和数据。接口 Comment 定义的 getText() 方法可以检索注释文本。

清单 6 中的例子说明了如何使用 Characters 事件报告各种类型的文本内容。同时也说明了接口 Comment 和 ProcessingInstruction 的用法。



                
final String ATOM_NS = "http://www.w3.org/2005/Atom";

URL url = new URL(uri);
InputStream input = url.openStream();

XMLInputFactory f = XMLInputFactory.newInstance();
XMLEventReader r = f.createXMLEventReader(uri, input);
try {
while (r.hasNext()) {
XMLEvent event = r.nextEvent();
if (event.isCharacters()) {
Characters c = event.asCharacters();
System.out.print("Characters");
if (c.isCData()) {
System.out.print(" (CDATA):");
System.out.println(c.getData());
} else if (c.isIgnorableWhiteSpace()) {
System.out.println(" (IGNORABLE SPACE)");
} else if (c.isWhiteSpace()) {
System.out.println(" (EMPTY SPACE)");
} else {
System.out.print(": ");
System.out.println(c.getData());
}
} else if (event.isProcessingInstruction()) {
ProcessingInstruction pi = (ProcessingInstruction) event;
System.out.print("PI(");
System.out.print(pi.getTarget());
System.out.print(", ");
System.out.print(pi.getData());
System.out.println(")");
} else if (event.getEventType() == XMLStreamConstants.COMMENT) {
System.out.print("Comment: ");
System.out.println(((Comment) event).getText());
}
}
} finally {
r.close();
}

input.close();

XMLEventReader 提交的最后一个事件是 EndDocument,它没有定义新方法。



可以看到,使用 XMLEventReaderXMLEvent 及其子类型解析 XML 非常简单。通过控制解析过程,应用程序可以决定对每个事件做什么。但是如果应用程序(或某个组件)需要特定内容类型的事件流,也可以创建专门的事件读取器。比方说,很容易创建筛选过的 XMLEventStream 只允许特定的事件传递给调用者。只需要对 XMLInputFactory 实例调用 createXMLEventReader(XMLEventReader, EventFilter) 方法,并传递基本事件读取器和接受/拒绝从基本读取器获得的事件的简单筛选器。清单 7 给出了一个筛选器的例子(只接受处理指令事件,应用程序可以定义任何接受条件)。



                
URL url = new URL(uri);
InputStream input = url.openStream();

XMLInputFactory factory = XMLInputFactory.newInstance();
XMLEventReader r = factory.createXMLEventReader(uri, input);
XMLEventReader fr = factory.createFilteredReader(r, new EventFilter() {
public boolean accept(XMLEvent e) {
return e.getEventType() == PROCESSING_INSTRUCTION;
}
});

try {
while (fr.hasNext()) {
XMLEvent e = fr.nextEvent();
if (e.getEventType() == PROCESSING_INSTRUCTION) {
ProcessingInstruction pi = (ProcessingInstruction) e;
System.out.println(pi.getTarget() + ": " + pi.getData());
}
}
} finally {
fr.close();
r.close();
}

input.close();

要执行更复杂的流操作,可扩展 EventReaderDelegate,这是 javax.xml.stream.util 包中定义的一个工具类。该类允许开发人员包装已有的 XMLEventStream,把所有调用都默认委托给它。然后让子类改写某些特殊的方法从而改变基本读取器的行为。比如,可通过该方法在事件流中插入合成事件或者转换它。迭代修改过的流的应用程序不需要知道它处理的是什么。清单 8 给出了使用这种技术的一个例子。



                
URL url = new URL(uri);
InputStream input = url.openStream();

XMLInputFactory factory = XMLInputFactory.newInstance();
XMLEventReader r = factory.createXMLEventReader(uri, input);

XMLEventReader fr = new EventReaderDelegate(r) {

private Comment comment;

public XMLEvent nextEvent() throws XMLStreamException {
XMLEvent event = null;
if (comment != null) {
event = comment;
comment = null;
return event;
}

event = super.nextEvent();
if (event.isStartDocument()) {
XMLEventFactory ef = XMLEventFactory.newInstance();
comment = ef.createComment("Generated " + new Date());
}

return event;
}
};

OutputStreamWriter writer = new OutputStreamWriter(System.out);
try {
while (fr.hasNext()) {
XMLEvent event = fr.nextEvent();
event.writeAsEncodedUnicode(writer);
}
} finally {
fr.close();
r.close();
}

input.close();
writer.flush();

请注意,要实现该例应用程序必须能够创建标准事件的实例(这里是 Comment)。这种功能要用到类 XMLEventFactory,它定义了各种标准事件类型的创建方法(事实上每个方法都有数个重载版本,根据事件的类型具有不同的参数设置)。和 XMLInputFactory 类似,该类也实现了抽象工厂模式:调用 getInstance() 获得它的具体实例。


本文仅介绍了用 StAX 基于事件迭代器的 API 所能完成的一部分功能,见证了它的灵活性以及易用性。第 3 部分中将介绍如何创建和使用定制事件。还将探究 StAX 序列化器 API。
评论
发表评论

您还没有登录,请登录后发表评论

Eastsun
搜索本博客
我的相册
1b680e5a-efae-3ec3-8ccd-970a4a72a056-thumb
6.5beta.PNG
共 61 张
最近加入圈子
存档
最新评论