首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 网站开发 > XML SOAP >

Java6.0新特性之StAX-全面解析Java XML分析技术

2012-10-07 
Java6.0新特性之StAX--全面解析Java XML分析技术作者:臧圩人(zangweiren)网址:http://zangweiren.iteye.co

Java6.0新特性之StAX--全面解析Java XML分析技术
作者:臧圩人(zangweiren)
网址:http://zangweiren.iteye.com

>>>转载请注明出处!<<<

野马(Mustang,Java 6.0代号)相比老虎(Tiger,Java 5.0代号)来说,从性能的提升、脚本语言(Javascript、JRuby、Groovy)的支持、对java.io.File的扩展到桌面应用的增强等各个方面,本领着实大了不少。

Java 6.0对XML支持的新特性有许多方面。比如StAX、针对XML-Web服务的Java架构(JAX-WS)2.0、针对XML绑定的API(JAXB)2.0、XML数字签名API,甚至还支持SQL:2003 'XML'数据类型。在这一篇文章中我们将要介绍的是StAX技术,因为它在我们的开发中将被使用地更加频繁。

StAX是Streaming API for XML的缩写,是一种针对XML的流式拉分析API。关于对XML进行分析(或解析)的技术,大家一定都不陌生了。在Java 6.0之前,就已经有四种:

    DOM:Document Object ModelSAX:Simple API for XMLJDOM:Java-based Document Object ModelDOM4J:Document Object Model for Java

关于它们的解析原理,以及性能和优缺点,我会在本文的结尾做一个简要的介绍。这篇文章中,我们主要说说StAX这种新的解析方式。

首先我们来搞清楚两个概念:推分析拉分析

在程序中访问和操作XML文件一般有两种模型:DOM(文档对象模型)和流模型。它们的优缺点如下:

<?xml version="1.0" encoding="UTF-8"?><company><depart title="Develop Group"><user name="Tom" age="28" gender="male" >Manager</user><user name="Lily" age="26" gender="female" /></depart><depart title="Test Group"><user name="Frank" age="32" gender="male" >Team Leader</user><user name="Bob" age="45" gender="male" /><user name="Kate" age="25" gender="female" /></depart></company>

可以让我们使用基于指针的API的接口是javax.xml.stream.XMLStreamReader(很遗憾,你不能直接实例化它),要得到它的实例,我们需要借助于javax.xml.stream.XMLInputFactory类。根据JAXP的传统风格,这里使用了抽象工厂(Abstract Factory)模式。如果你对这个模式很熟悉的话,就能够在脑海中想象出我们将要编写的代码的大致框架了。

首先,获得一个XMLInputFactory的实例。方法是:

XMLInputFactory factory = XMLInputFactory.newInstance();


或者:

XMLInputFactory factory = XMLInputFactory.newFactory();


这两个方法是等价的,它们都是创建了一个新的实例,甚至实例的类型都是完全一致的。因为它们的内部实现都是:

{    return (XMLInputFactory) FactoryFinder.find("javax.xml.stream.XMLInputFactory", "com.sun.xml.internal.stream.XMLInputFactoryImpl");}


接下来我们就可以创建XMLStreamReader实例了。我们有这样一组方法可以选择:

XMLStreamReader createXMLStreamReader(java.io.Reader reader) throws XMLStreamException;XMLStreamReader createXMLStreamReader(javax.xml.tranform.Source source) throws XMLStreamException;    XMLStreamReader createXMLStreamReader(java.io.InputStream stream) throws XMLStreamException;XMLStreamReader createXMLStreamReader(java.io.InputStream stream, String encoding) throws XMLStreamException;XMLStreamReader createXMLStreamReader(String systemId, java.io.InputStream stream) throws XMLStreamException;XMLStreamReader createXMLStreamReader(String systemId, java.io.Reader reader) throws XMLStreamException;


这些方法都会根据给定的流创建一个XMLStreamReader实例,大家可以依据流的类型、是否需要指定解析XML的编码或者systemId来选择相应的方法。

在这里,我们对systemId稍作说明,并简单解释一下它与publicId的区别。

systemId和publicId是XML文档里DOCTYPE元素中经常出现的两个属性。它们都是对外部资源的引用,用以指明引用资源的地址。systemId是直接引用资源,publicId是间接定位外部资源。具体一点说是这样:

try { XMLStreamReader reader = factory.createXMLStreamReader(new FileReader("users.xml"));} catch (FileNotFoundException e) { e.printStackTrace();} catch (XMLStreamException e) { e.printStackTrace();}

要遍历XML文档,需要用到XMLStreamReader的下面几个方法:

int getEventType();boolean hasNext() throws XMLStreamException;int next() throws XMLStreamException;


getEventType()方法返回XMLStreamConstants接口中定义的一个标记常量,表示当前指针所指向标记(或事件)的类型。根据当前事件类型的不同,应用程序可以做出不同的处理。标记常量的类型和含义如下:

    START_DOCUMENT:文档的开始END_DOCUMENT:文档的结尾START_ELEMENT:元素的开始END_ELEMENT:元素的结尾PROCESSING_INSTRUCTION:处理指令CHARACTERS:字符(文本或空格)COMMENT:注释SPACE:可忽略的空格ENTITY_REFERENCE:实体的引用ATTRIBUTE:元素的属性DTD:DTDCDATA:CDATA块NAMESPACE:命名空间的声明NOTATION_DECLARATION:标记的声明ENTITY_DECLARATION:实体的声明


next()方法将指针移动到下一个标记,它同时返回这个标记(或事件)的类型。此时若接着调用getEventType()方法则返回相同的值。

hasNext()用于判断是否还有下一个标记。只有当它返回true时才可以调用next()以及其它移动指针的方法。

看了上面几个方法的介绍,大家就会发现使用XMLStreamReader遍历XML文档是非常容易的,因为它的用法和每个人都熟悉的Java迭代器(Iterator)是一样的。下面我们就用已经掌握的这几个方法对上文中给出的XML文档做一个测试。希望你还记得它的内容,如果忘记了,请翻回去重新浏览一下。

我们的测试代码如下:

/** * 列出所有用户 *  * @author zangweiren 2010-4-17 *  */public class ListUsers {// 获得解析器public static XMLStreamReader getStreamReader() {String xmlFile = ListUsers.class.getResource("/").getFile()+ "users.xml";XMLInputFactory factory = XMLInputFactory.newFactory();try {XMLStreamReader reader = factory.createXMLStreamReader(new FileReader(xmlFile));return reader;} catch (FileNotFoundException e) {e.printStackTrace();} catch (XMLStreamException e) {e.printStackTrace();}return null;}// 列出所有用户名称public static void listNames() {XMLStreamReader reader = ListUsers.getStreamReader();// 遍历XML文档try {while (reader.hasNext()) {int event = reader.next();// 如果是元素的开始if (event == XMLStreamConstants.START_ELEMENT) {// 列出所有用户名称if ("user".equalsIgnoreCase(reader.getLocalName())) {System.out.println("Name:"+ reader.getAttributeValue(null, "name"));}}}reader.close();} catch (XMLStreamException e) {e.printStackTrace();}}public static void main(String[] args) {ListUsers.listNames();}}


运行结果:
String getLocalName();String getAttributeValue(String namespaceURI, String localName);

与此相关的还有一个方法:

QName getName();


这三个方法牵扯到XML的namespace(命名空间)、localName(本地名称)、QName(Qualified Name,限定名称)三个概念,我们顺便解释一下:

命名空间是为了支持相同名称不同含义的XML标签而产生的,它可以这么定义:
<com:company xmlns:com="http://www.zangweiren.com/company">    <!-- here is other tags --></com:company>

其中,com是命名空间的前缀,company是命名空间的标签,http://www.zangweiren.com/company是命名空间的标识,相同的标识被认为是同一个命名空间。标识又叫URI,是唯一的,有URL(统一资源定位器)和URN(统一资源名称)两种。前缀是命名空间的简写,目的是为了使用方便。命名空间被声明后就可以被使用:

<com:company xmlns:com="http://www.zangweiren.com/company">    <com:depart name="Develop Group" /></com:company>


在上例的<com:depart />标签中,前缀com是命名空间,depart是localName,这两个合起来就是QName。

在明白了这三个XML基本概念之后,也就明白了getLocalName()和getAttributeValue(String namespaceURI, String localName)方法的含义。

现在,我们已经学会了使用XMLStreamReader遍历XML文档,并对特定标签进行解析了。

我们再来看看下面两个方法:

String getElementText() throws XMLStreamException;int nextTag() throws XMLStreamException;


getElementText()方法返回元素的开始标签(START_ELEMENT)和关闭标签(END_ELEMENT)之间的所有文本内容,若遇到嵌套的元素就会抛出异常。

nextTag()方法将跳过所有空白、注释或处理指令,直到遇到START_ELEMENT或END_ELEMENT。它在解析只含元素内容的XML文档时很有用。否则,在发现标记之前遇到非空白文本(不包括注释和处理指令),就会抛出异常。

比如我们修改上一个测试程序,增加一个新方法:

// 列出所有用户的名称和年龄public static void listNamesAndAges() {XMLStreamReader reader = ListUsers.getStreamReader();try {while (reader.hasNext()) {// 跳过所有空白、注释或处理指令,到下一个START_ELEMENTint event = reader.nextTag();if (event == XMLStreamConstants.START_ELEMENT) {if ("user".equalsIgnoreCase(reader.getLocalName())) {System.out.println("Name:"+ reader.getAttributeValue(null, "name")+ ";Age:"+ reader.getAttributeValue(null, "age"));}}}reader.close();} catch (XMLStreamException e) {e.printStackTrace();}}


然后把它添加到主方法中:

public static void main(String[] args) {ListUsers.listNames();ListUsers.listNamesAndAges();}


运行它试试看,在解析到<user name="Tom" age="28" gender="male" >Manager</user>的时候会报错,因此你会得到一个类似这样的错误信息:

javax.xml.stream.XMLStreamException: ParseError at [row,col]:[4,53]
Message: found: CHARACTERS, expected START_ELEMENT or END_ELEMENT

对于基于指针的XMLStreamReader来说,虽然API文档说的是“事件”,但是我们把它看成“标记”更易于理解,而且不会与另一套基于事件的API相混淆。

XMLStreamReader的某些方法,无论当前标记(或事件)是什么类型的,都可以被调用。它们的定义和作用如下:

String getVersion();//获得XML文档中的版本信息String getEncoding();//获得XML文档中的指定编码javax.xml.namespace.NamespaceContext getNamespaceContext();//获得当前有效的命名空间上下文,包含前缀、URI等信息String getNamespaceURI();//获得当前有效的命名空间的URIjavax.xml.stream.Location getLocation();//获得当前标记的位置信息,包含行号、列号等boolean hasName();//判断当前标记是否有名称,比如元素或属性boolean hasText();//判断当前标记是否有文本,比如注释、字符或CDATAboolean isStartElement();//判断当前标记是否是标签开始boolean isEndElement();//判断当前标记是否是标签结尾boolean isCharacters();//判断当前标记是否是字符boolean isWhiteSpace();//判断当前标记是否是空白

对于以上方法都很容易理解和记忆,我们不再编写代码展示它们的效果。

让我们看看有关属性操作方法。还是首先熟悉一下它们的定义:

int getAttributeCount();String getAttributeLocalName(int index);QName getAttributeName(int index);String getAttributeNamespace(int index);String getAttributePrefix(int index);String getAttributeType(int index);String getAttributeValue(int index);String getAttributeValue(String namespaceURI, String localName);


这些方法都十分容易理解,基本上看方法的名称和参数就知道它的用途了。而且最后一个方法在上面的示例中我们已经用过了。让我们再用一个简单的示例程序进一步加深对这些方法的认识。

// 列出所有用户的名称和年龄public static void listNamesAndAges() {XMLStreamReader reader = ListUsers.getStreamReader();try {while (reader.hasNext()) {// 跳过所有空白、注释或处理指令,到下一个START_ELEMENTint event = reader.nextTag();if (event == XMLStreamConstants.START_ELEMENT) {if ("user".equalsIgnoreCase(reader.getLocalName())) {System.out.println("Name:"+ reader.getAttributeValue(null, "name")+ ";Age:"+ reader.getAttributeValue(null, "age"));}}}reader.close();} catch (XMLStreamException e) {e.printStackTrace();}}


把它加入到主方法中:

public static void main(String[] args) {ListUsers.listNames();// ListUsers.listNamesAndAges();ListUsers.listAllAttrs();}


运行结果:
XMLEventReader createXMLEventReader(java.io.InputStream stream) throws XMLStreamException; XMLEventReader createXMLEventReader(java.io.InputStream stream, String encoding) throws XMLStreamException; XMLEventReader createXMLEventReader(java.io.Reader reader) throws XMLStreamException; XMLEventReader createXMLEventReader(String systemId, java.io.InputStream stream) throws XMLStreamException; XMLEventReader createXMLEventReader(String systemId, java.io.Reader reader) throws XMLStreamException; XMLEventReader createXMLEventReader(Source source) throws XMLStreamException; XMLEventReader createXMLEventReader(XMLStreamReader reader) throws XMLStreamException;

最后一个方法不同与其它的,它是将一个XMLStreamReader对象转换成一个XMLEventReader对象。值得注意的是,XMLInputFactory没有提供将XMLEventreader对象转换成XMLStreamreader对象的方法。我想,在我们的开发过程中,应该不会出现这种需要将高层API转换成低层API来使用的情况。

XMLEventReader接口扩展了java.util.Iterator接口,它定义了以下几个方法:

String getElementText() throws XMLStreamException;boolean hasNext();XMLEvent nextEvent() throws XMLStreamException;XMLEvent nextTag() throws XMLStreamException;XMLEvent peek() throws XMLStreamException;


其中,getElementText()、hasNext()、nextTag()三个方法的含义及用法类似于XMLStreamReader,而nextEvent()方法类似于XMLStreamReader的next()方法。所以,这里只对peed()方法做一下说明。

调用peek()方法,你将得到下一个事件对象。它与nextEvent()方法的不同是,当你连续两次或两次以上调用它时,你得到的都是同一个事件对象。

我们再看看XMLEvent接口中定义的方法。这些方法大体可以分为三种类别。第一类是用于事件类型判断的:

boolean isAttribute();//判断该事件对象是否是元素的属性boolean isCharacters();//判断该事件对象是否是字符boolean isStartDocument();//判断该事件对象是否是文档开始boolean isEndDocument();//判断该事件对象是否是文档结尾boolean isStartElement();//判断该事件对象是否是元素开始boolean isEndElement();//判断该事件对象是否是元素结尾boolean isEntityReference();//判断该事件对象是否是实体的引用boolean isNamespace();//判断该事件对象是否是命名空间boolean isProcessingInstruction();//判断该事件对象是否是处理指令

第二类是将XMLEvent转换为具体的子类对象的:

Characters asCharacters();//转换为字符事件对象StartElement asStartElement();//转换为标签开始事件对象EndElement asEndElement();//转换为标签结尾事件对象

第三类是获取事件对象通用信息的:

javax.xml.stream.Location getLocation();//获得事件对象的位置信息,类似于XMLStreamReader的getLocation()方法int getEventType();//获得事件对象的类型,类似于XMLStreamReader的getEventType()方法

其中,getEventType()方法的返回值也是XMLStreamConstants中定义的常量,其类型和含义与XMLStreamReader的getEventType()方法的返回值完全相同。

下面让我们用一段示例代码来熟悉基于迭代器的StAX API的使用方法,进而引出XMLEvent接口的子接口类型。我们仍然使用users.xml作为测试文件:

// 列出所有信息@SuppressWarnings("unchecked")public static void listAllByXMLEventReader() {String xmlFile = ListUsers.class.getResource("/").getFile()+ "users.xml";XMLInputFactory factory = XMLInputFactory.newInstance();try {// 创建基于迭代器的事件读取器对象XMLEventReader reader = factory.createXMLEventReader(new FileReader(xmlFile));// 遍历XML文档while (reader.hasNext()) {XMLEvent event = reader.nextEvent();// 如果事件对象是元素的开始if (event.isStartElement()) {// 转换成开始元素事件对象StartElement start = event.asStartElement();// 打印元素标签的本地名称System.out.print(start.getName().getLocalPart());// 取得所有属性Iterator attrs = start.getAttributes();while (attrs.hasNext()) {// 打印所有属性信息Attribute attr = (Attribute) attrs.next();System.out.print(":" + attr.getName().getLocalPart()+ "=" + attr.getValue());}System.out.println();}}reader.close();} catch (FileNotFoundException e) {e.printStackTrace();} catch (XMLStreamException e) {e.printStackTrace();}}


把它加到主程序中:

public static void main(String[] args) {ListUsers.listNames();// ListUsers.listNamesAndAges();ListUsers.listAllAttrs();ListUsers.listAllByXMLEventReader();}


运行后得到如下结果:
XMLStreamReader createFilteredReader(XMLStreamReader reader, StreamFilter filter) throws XMLStreamException; XMLEventReader createFilteredReader(XMLEventReader reader, EventFilter filter) throws XMLStreamException;

它们分别为XMLStreamReader和XMLEventReader增加一个过滤器,过滤掉不需要解析的内容,只留下应用程序关心的信息用于解析。虽然我们可以在应用程序中做同样的过滤工作,就像之前示例程序中所写的那样,但是把过滤工作交给过滤器的好处是,让应用程序可以更加专注于解析工作,并且对于通用的过滤(比如注释),将它放到过滤器中可以实现过滤逻辑部分代码的重用。这符合软件设计原则。

如果你编写过文件过滤器java.io.FileFilter的话,那么编写StreamFilter和EventFilter就更加容易。我们先来看看这两个接口的定义:

public interface StreamFilter {  public boolean accept(XMLStreamReader reader);}public interface EventFilter {  public boolean accept(XMLEvent event);}


我们就以StreamFilter为例来演示过滤器的用法。为此,我们使用users.xml为测试文档编写一段新的程序:

/** * StreamFilter示例程序 *  * @author zangweiren 2010-4-19 *  */public class TestStreamFilter implements StreamFilter {public static void main(String[] args) {TestStreamFilter t = new TestStreamFilter();t.listUsers();}@Overridepublic boolean accept(XMLStreamReader reader) {try {while (reader.hasNext()) {int event = reader.next();// 只接受元素的开始if (event == XMLStreamConstants.START_ELEMENT) {// 只保留user元素if ("user".equalsIgnoreCase(reader.getLocalName())) {return true;}}if (event == XMLStreamConstants.END_DOCUMENT) {return true;}}} catch (XMLStreamException e) {e.printStackTrace();}return false;}public XMLStreamReader getFilteredReader() {String xmlFile = TestStreamFilter.class.getResource("/").getFile()+ "users.xml";XMLInputFactory factory = XMLInputFactory.newFactory();XMLStreamReader reader;try {reader = factory.createXMLStreamReader(new FileReader(xmlFile));// 创建带有过滤器的读取器实例XMLStreamReader freader = factory.createFilteredReader(reader, this);return freader;} catch (FileNotFoundException e) {e.printStackTrace();} catch (XMLStreamException e) {e.printStackTrace();}return null;}public void listUsers() {XMLStreamReader reader = getFilteredReader();try {// 列出所有用户的名称while (reader.hasNext()) {// 过滤工作已交由过滤器完成,这里不需要再做System.out.println("Name="+ reader.getAttributeValue(null, "name"));if (reader.getEventType() != XMLStreamConstants.END_DOCUMENT) {reader.next();}}reader.close();} catch (XMLStreamException e) {e.printStackTrace();}}}


测试结果:
/** * 测试StreamReaderDelegate * * @author zangweiren 2010-4-19 * */public class TestStreamDelegate {public static void main(String[] args) {TestStreamDelegate t = new TestStreamDelegate();t.listUsers();}public XMLStreamReader getDelegateReader() {String xmlFile = TestStreamFilter.class.getResource("/").getFile()+ "users.xml";XMLInputFactory factory = XMLInputFactory.newFactory();XMLStreamReader reader;try {reader = new StreamReaderDelegate(factory.createXMLStreamReader(new FileReader(xmlFile))) {// 重写(Override)next()方法,增加过滤逻辑@Overridepublic int next() throws XMLStreamException {while (true) {int event = super.next();// 保留用户元素的开始if (event == XMLStreamConstants.START_ELEMENT&& "user".equalsIgnoreCase(getLocalName())) {return event;} else if (event == XMLStreamConstants.END_DOCUMENT) {return event;} else {continue;}}}};return reader;} catch (FileNotFoundException e) {e.printStackTrace();} catch (XMLStreamException e) {e.printStackTrace();}return null;}public void listUsers() {XMLStreamReader reader = this.getDelegateReader();try {while (reader.hasNext()) {reader.next();if (reader.getEventType() != XMLStreamConstants.END_DOCUMENT) {// 列出用户的名称和年龄System.out.println("Name="+ reader.getAttributeValue(null, "name") + ";age="+ reader.getAttributeValue(null, "age"));}}reader.close();} catch (XMLStreamException e) {e.printStackTrace();}}}

测试结果:
这个提议不错,当初没有考虑到。想写一篇包含全部解析方式的文章,就放一起了。

热点排行