streaming api_XML的Streaming API简介(StAX)

华坚成
2023-12-01

StAX概述

自成立以来,用于XML处理的Java API(JAXP)提供了两种处理XML的方法-文档对象模型(DOM)方法(使用标准对象模型表示XML文档)和XML简单API(SAX)方法,该方法使用应用程序提供的事件处理程序来处理XML。 在JSR-173:XML的流API(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风格中,这是使用Abstract Factory模式完成的。 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);
// ...

XMLStreamReaderXMLEventReader允许应用程序自行遍历基础XML流。 两种方法之间的区别在于它们如何显示已解析的XML InfoSet的片段。 XMLStreamReader充当游标,其指向刚超出最近解析的XML令牌之外的位置,并提供获取有关此令牌的更多信息的方法。 这种方法非常节省内存,因为它不会创建任何新对象。 但是,业务应用程序开发人员可能会发现XMLEventReader稍微更直观,因为它实际上是将XML转换为事件对象流的标准Java迭代器。 每个事件对象依次封装与其表示的特定XML结构有关的信息。 本系列的第2部分将详细介绍基于事件迭代器的API。

至于使用哪种API样式取决于情况。 与基于游标的API相比,基于事件迭代器的API代表了一种更加面向对象的方法。 这样,由于当前解析器的状态反映在事件对象中,因此更容易应用于模块化体系结构。 因此,应用程序组件在处理事件时不需要访问解析器/读取器。 此外,可以使用XMLInputFactorycreateXMLEventReader(XMLStreamReader)方法从XMLStreamReader创建XMLEventReader

StAX还定义了序列化API,这是Java的标准XML处理支持中非常缺少的功能。 与其解析对象一样,它也是一种流API,有两种形式:用于令牌的较低级XMLStreamWriter和用于事件对象的较高级XMLEventWriterXMLStreamWriter提供了用于编写单个XML令牌(例如,打开和关闭标签或元素属性)的方法,而无需检查其格式是否正确。 另一方面, XMLEventWriter允许应用程序将完整的XML事件对象添加到输出中。 在第3部分中,您将详细探讨StAX序列化API。

为什么要使用StAX?

在致力于学习新的XML处理API之前,您可能想知道是否值得这样做。 实际上,StAX采用的基于拉的方法相对于其他方法具有几个重要的优点。 首先,无论使用哪种API样式,都是由应用程序调用读取器(解析器),而不是相反。 通过保留对解析过程的控制,您可以简化调用代码以精确处理其期望的内容,并选择在遇到意外情况时仅停止解析。 此外,由于此方法不是基于处理程序回调,因此应用程序不需要像使用SAX时那样需要维护模拟的解析器状态。

StAX还保留了SAX通过DOM提供的优势。 通过将焦点从结果对象模型转移到已解析的流本身,应用程序可以处理理论上无限的XML流,因为事件本质上是瞬态的,不需要在内存中累积。 对于使用XML作为消息传递协议而不是表示文档内容的一类应用程序,例如Web服务或即时消息传递应用程序,这尤其重要。 例如,如果将传递给DOM的Web服务路由器servlet所做的只是将其转换为特定于应用程序的对象模型,然后简单地将其丢弃,则几乎没有用。 使用StAX直接进入应用程序模型更为有效。 对于可扩展消息和状态协议(XMPP)客户端,使用DOM是绝对不可能的-XMPP客户端/服务器流是根据用户输入的消息实时递增生成的。 等待流的结束标记(以最终完成DOM的构建)意味着等待直到对话结束。 通过将XML处理为一系列事件,应用程序可以以最合适的方式对每个事件做出React(例如,显示传入的即时消息等)。

由于其双向特性,StAX还非常好地支持链式处理,尤其是在事件级别。 接受事件(来自任何来源)的能力封装在XMLEventConsumer接口中,该接口由XMLEventWriter扩展。 因此,您可以模块化地编写应用程序,以从XMLEventReader(它也是一个普通的Iterator,可以将其视为此类)读取XML事件,对其进行处理,然后将其传递给事件使用者(如果可以,则进一步扩展处理链)需要)。 正如您将在第2部分中学习的那样,还可以通过使用应用程序提供的过滤器(实现EventFilter接口的类)或使用EventReaderDelegate装饰现有的XMLEventReader来定制XMLEventReader。

总而言之,StAX使应用程序比DOM或SAX更接近底层XML。 通过使用StAX,应用程序不仅可以建立所需的对象模型(而不必处理标准DOM),而且还可以方便地做到这一点,而不是仅在从解析器获得回调之后才可以这样做。

下一节将详细研究基于游标的API以及如何使用它来有效处理XML流。

基于游标的API

使用基于游标的API时,应用程序通过在XML令牌流上推进逻辑游标来处理XML。 基于游标的解析器实质上是一种状态机,由于事件而从一种状态良好的状态转换为另一种状态。 在这种情况下,触发事件是XML令牌,当应用程序使用适当的方法沿令牌流推进解析器时,将对其进行解析。 在每种状态下,您可以使用一组方法来获取有关最新事件的信息。 通常,并非所有方法都适用于所有状态。

要使用基于游标的方法,应用程序必须首先通过调用XMLInputFactorycreateXMLStreamReader方法之一从XMLInputFactory获得XMLStreamReader 。 此方法有多个版本,每个版本支持不同类型的输入。 例如,可以创建XMLStreamReader来解析plain java.io.InputStreamjava.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_ELEMENTEND_ELEMENT 。 当解析纯元素内容时,此方法很有用。 如果在找到标签之前(注释或处理指令除外)遇到非空白文本,则会引发异常。 方法getElementText()将返回元素的开始和结束标记之间(即START_ELEMENTEND_ELEMENT之间getElementText()所有文本内容。 如果找到任何嵌套的元素,它将引发异常。

您会注意到,在此上下文中,术语“令牌”和“事件”可互换使用。 虽然基于游标的API的文档讨论事件,但将输入源视为令牌流更容易。 由于还有其他基于事件的API样式(事件是适当的对象),因此它也不太混乱。 但是, XMLStreamReader的事件本身并不是全部标记。 例如, START_DOCUMENTEND_DOCUMENT事件不需要匹配的令牌。 前一个事件在解析开始之前发生,而后一个事件在无法进行进一步解析之后发生(例如,在解析了最后一个元素的结束标记之后,读取器处于END_ELEMENT状态;但是,在尝试解析更多令牌并没有找到任何令牌之后,阅读器将转换为END_DOCUMENT状态)。

处理XML文档

在每个解析器状态下,应用程序都可以使用适用的方法来获取有关它的信息。 例如,方法getNamespaceContext()getNamespaceURI()可以分别获取当前名称空间上下文和当前有效的名称空间URI,而与当前事件类型无关。 同样, getLocation()可以获取有关当前事件位置的信息。 方法hasName()hasText()可以分别确定当前事件是否具有名称(例如元素或属性)或文本(例如字符,注释或CDATA)。 方法isStartElement()isEndElement()isCharacters()isWhiteSpace()是确定当前事件性质的便捷快捷方式。 最后,方法require( intStringString )可以声明预期的解析器状态; 除非当前事件是指定的类型,并且本地名称和名称空间(如果指定)与当前事件匹配,则它将引发异常。

清单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.notationsjavax.xml.stream.entities返回任何符号和实体声明)。

START_ELEMENTEND_ELEMENT ,可以使用与元素名称和名称空间相关的方法(例如getName()getLocalName()getPrefix()getNamespaceXXX() ); 与属性相关的方法( getAttributeXXX() )也可以在START_ELEMENT

ATTRIBUTENAMESPACE也被视为独立事件,尽管在解析典型的XML文档时不会遇到它们。 但是,由于XPath查询而返回ATTRIBUTENAMESPACE节点时,可能会遇到它们。

在基于文本的事件(例如CHARACTERSCDATACOMMENTSPACE )中,请使用各种getTextXXX()方法获取文本。 您可以分别使用getPITarget()getPIData()来检索PROCESSING_INSTRUCTION的目标和数据。 ENTITY_REFERENCEDTD也支持getText() ; ENTITY_REFERENCEgetLocalName()

解析完成后,应用程序将关闭阅读器以释放其在此过程中获取的任何资源。 请注意,这不会关闭基础输入源。

清单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();
}

XMLStreamReader高级用法

也可以通过使用基本阅读器和应用程序定义的过滤器(即,实现StreamFilter的类的实例)调用XMLInputFactorycreateFilteredReader方法来创建过滤的XMLStreamReader 。 在浏览过滤的阅读器时,只要基本阅读器前进到下一个标记,便会查询过滤器。 如果过滤器批准当前事件,则将其暴露给过滤后的阅读器。 如果不是,则跳过令牌并测试下一个令牌,依此类推。 这种方法允许开发人员创建基于游标的XML处理器,以处理解析后的内容的简化子集,并将其与各种扩展内容模型的过滤器结合使用。

要执行更复杂的流操作,请子类StreamReaderDelegate并重写适当的方法。 然后可以使用此子类的实例包装基本XMLStreamReader ,从而为应用程序提供基本XML流的修改视图。 使用此技术可以对XML流执行简单的转换,例如过滤或替换某些令牌,甚至用新的令牌扩充该流。

在清单6中,使用自定义StreamReaderDelegate包装了一个基本XMLStreamReader ,并重写了它的next()方法以跳过COMMENTPROCESSING_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。 即使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进行更深入的研究。


翻译自: https://www.ibm.com/developerworks/java/library/x-stax1/index.html

 类似资料: