野馬(Mustang,Java 6.0代號)相比老虎(Tiger,Java 5.0代號)來說,從性能的提升、腳本語言(Javascript、JRuby、Groovy)的支持、對java.io.File的擴展到桌面應用的增強等各個方面,本領着實大了不少。
|
首先我們來搞清楚兩個概念:推分析和拉分析。
在程序中訪問和操作XML文件一般有兩種模型:DOM(文檔對象模型)和流模型。它們的優缺點如下:
是一種從XML文檔中搜索節點的查詢語言)查詢。
DOM缺點:需要一次性加載整個文檔到內存中,對於大型文檔,會造成性能問題。
流模型缺點:是隻讀的,並且只能向前,不能在文檔中執行向後導航操作。
關於什麼是DOM,文章結尾處會有介紹。這裏我們簡單說一下流:它是一個連續的字節序列,可以理解爲不停地從源頭向目標搬運着字節的特殊對象。
讓我們回到主題。流模型每次迭代XML文檔中的一個節點,適合於處理較大的文檔,所耗內存空間小。它有兩種變體--“推”模型和“拉”模型。
到此,我們就弄明白了“推分析”和“拉分析”的概念:
StAX就是一種拉分析式的XML解析技術。它也支持對XML文件的生成操作,但是這篇文章裏我們只介紹有關解析的知識。
從一開始,JAXP(Java API for XML Processing)就提供了兩種方法來處理XML:DOM和SAX。
StAX是一種面向流的新方法,最終版本於2004年3月發佈,併成爲JAXP 1.4(包含在Java 6.0中)的一部分。
StAX的實現使用了JWSDP(Java Web Services Development Pack)1.6,並結合了SJSXP
(Sun Java System XML Streaming Parser,位於javax.xml.stream.*包中)。
JWSDP是用來開發Web Services、Web應用程序以及Java應用(主要是XML處理)的開發包。它包含的Java API有:
- JAXP:Java API for XML Processing
- JAXB:Java Architecture for XML Binding
- JAX-RPC:Java API for XML-based Remote Procedure Calls
- JAX-WS:Java API for XML Web Services
- SAAJ:SOAP with Attachments API for Java
- JAXR:Java API for XML Registries
- Web Services Registry
JWSDP的早期版本中還包括:
- Java Servlet
- JSP:JavaServer Pages
- JSF:JavaServer Faces
現在,JWSDP已經被GlassFish所替代。
StAX包括兩套處理XML的API,分別提供了不同程度的抽象。它們是:基於指針的API和基於迭代器的API。
我們先來了解基於指針的API。它把XML作爲一個標記(或事件)流來處理,應用程序可以檢查解析器的狀態,
獲得解析的上一個標記的信息,然後再處理下一個標記,依次類推。
在開始API探索之前,我們首先創建一個名爲users.xml的XML文檔用於測試,它的內容如下:
<?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是間接定位外部資源。具體一點說是這樣:
好了,我們接着用以上列出的第一個接口來創建一個XMLStreamReader實例:
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:DTD
- CDATA:CDATA塊
- NAMESPACE:命名空間的聲明
- NOTATION_DECLARATION:標記的聲明
- ENTITY_DECLARATION:實體的聲明
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();
}
}
運行結果: Name:Lily
Name:Frank
Name:Bob
Name:Kate
在上面的示例代碼中,我們用到了XMLStreamReader的兩個新方法:
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_ELEMENT
int 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();//獲得當前有效的命名空間的URI
- javax.xml.stream.Location getLocation();//獲得當前標記的位置信息,包含行號、列號等
- boolean hasName();//判斷當前標記是否有名稱,比如元素或屬性
- boolean hasText();//判斷當前標記是否有文本,比如註釋、字符或CDATA
- boolean 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_ELEMENT
int 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();
}
運行結果: 2.name=Lily;age=26;gender=female;
3.name=Frank;age=32;gender=male;
4.name=Bob;age=45;gender=male;
5.name=Kate;age=25;gender=female;
相信你看到這裏,已經可以順利地使用XMLStreamReader來完成XML文檔的解析了。
上面我們介紹了基於指針的StAX API。這種方式儘管效率高,但是沒有提供XML結構的抽象,因此是一種低層API。
較爲高級的基於迭代器的API允許應用程序把XML作爲一系列事件對象來處理,每個對象和應用程序交換XML結構的一部分。應用程序只需要確定解析事件的類型,將其轉換成對應的具體類型,然後利用其方法獲得屬於該事件對象的信息。
StAX中基於迭代器的API是一種面向對象的方式,這也是它與基於指針的API的最大區別。它通過將事件轉變爲對象,讓應用程序可以用面向對象的方式處理它們,這有利於模塊化和不同組件之間的代碼重用。
事件迭代器API的主要接口是javax.xml.stream.XMLEventReader和javax.xml.stream.events.XMLEvent。XMLEventReader和XMLStreamReader相比要簡單的多,這是因爲關於解析事件的所有信息都封裝在了事件對象(XMLEvent)中。
創建XMLEvent對象前同樣需要一個XMLInputFactory實例。它有如下這些創建XMLEvent實例的方法:
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();//判斷該事件對象是否是處理指令
- Characters asCharacters();//轉換爲字符事件對象
- StartElement asStartElement();//轉換爲標籤開始事件對象
- EndElement asEndElement();//轉換爲標籤結尾事件對象
- javax.xml.stream.Location getLocation();//獲得事件對象的位置信息,類似於XMLStreamReader的getLocation()方法
- int getEventType();//獲得事件對象的類型,類似於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();
}
運行後得到如下結果: depart:title=Develop Group
user:age=28:name=Tom:gender=male
user:age=26:name=Lily:gender=female
depart:title=Test Group
user:age=32:name=Frank:gender=male
user:age=45:name=Bob:gender=male
user:age=25:name=Kate:gender=female
這個例子中,我們利用基於迭代器的StAX API打印出了所有元素的本地名稱以及它們的全部屬性信息。大家可以看到,它的用法與基於指針的StAX API的用法十分相似。但是由於使用了面向對象的思想,更加容易理解。
我們用到了兩個新的接口:StartElement和Attribute。它們都是XMLEvent接口的子接口,且都在javax.xml.stream.events.*包中。它們是更具體的事件對象類型。實際上在javax.xml.stream.events中,除了XMLEvent接口自身外,其餘接口都是它的子接口。它們的名稱和代表的具體事件對象類型如下:
- Attribute:元素的屬性
- Characters:字符
- Comment:註釋
- DTD:DTD
- StartDocument:文檔的開始
- EndDocument:文檔的結束
- StartElement:元素的開始
- EndElement:元素的結束
- EntityDeclaration:實體聲明
- EntityReference:實體的引用
- Namespace:命名空間聲明
- NotationDeclaration:標記的聲明
- ProcessingInstruction:處理指令
這些事件對象接口不僅代表了一種事件類型,還包含對應事件對象的信息。至於它們所具有的方法大多是獲取事件對象信息的訪問器,其含義及具體用法,都很容易理解和使用,因此不再詳細介紹。
大家可能注意到,XMLEvent只提供了三個asXXX()形式的方法將它轉換到具體的子類型,如果你想要處理的事件對象類型在這三種類型之外,直接使用強制類型轉換就可以了。
現在我們掌握了StAX的基於指針的拉分析API和基於迭代器的拉分析API的基本應用。我們再來看一種稍微高級的用法,它可以幫助我們更好地完成XML文檔的解析工作。
XMLInputFactory還有兩個創建流讀取器的方法:
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();
}
@Override
public 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();
}
}
}
測試結果: Name=Lily
Name=Frank
Name=Bob
Name=Kate
大家可能已經發現,這裏有一個與之前處理不同的地方,就是我們先打印了用戶的信息,再調用next()方法;這與java.util.Iterator的先調用next()方法,再獲取對象信息不同。而之前我們一直採用的是與Iterator一樣的處理代碼。這裏,就有一個問題需要說明。
對於XMLStreamReader的next()方法來說,第一次被調用的時候返回的是第二個標記(或事件)。要獲得第一個標記,就需要在調用next()方法之前調用getEventType()方法。這是需要注意的地方。我們以上的代碼之所以採用Java迭代器一樣的處理方式,是因爲第一個標記總是START_DOCUMENT,而我們不需要對它進行操作,因此就採用了一種熟悉的編碼方式,方便大家理解。XMLEventReader的nextEvent()方法就不存在這樣的問題。
EventFilter的用法與StreamFilter相同,不再舉例說明。
StAX還爲我們提供了另外一種隔離標記或事件對象過濾邏輯的方法,那就是StreamReaderDelegate和EventReaderDelegate這兩個類,它們都位於javax.xml.stream.util.*包中。StAX API中大部分都是接口,這兩個是確確實實的類。它們都做了同樣的工作,就是分別包裝了XMLStreamReader和XMLEventReader,並把所有的方法都委託(Delegate)給它們處理,既沒有增加任何的方法或邏輯,也沒有改變或刪除任何方法,因此這裏使用的是策略(Strategy)模式。我們可以採用裝飾(Decorator)模式,給StreamReaderDelegate或EventReaderDelegate增加新的功能。請看下面的例子:
/**
* 測試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()方法,增加過濾邏輯
@Override
public 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();
}
}
}
測試結果:
Name=Lily;age=26
Name=Frank;age=32
Name=Bob;age=45
Name=Kate;age=25
EventReaderDelegate的用法與StreamReaderDelegate相同。
現在我們介紹完了StAX的兩種解析XML文檔的方式,大家也可能對它的使用有了自己的認識。我們最後總結一下:XMLStreamReader和XMLEventReader都允許應用程序迭代底層的XML流,區別在於它們如何對外提供解析後的XML信息片段。前者像個指針,指在剛剛解析過的XML標記的後面,並提供獲得關於該標記更多信息的方法。因爲不用創建新的對象,所以更節約內存。後者具有更多的面向對象特徵,就是個標準的Java迭代器,解析器的當前狀態反映在事件對象中,應用程序在處理事件對象的時候不需要訪問解析器/讀取器。
關於各種XML解析技術的優劣
除了我們剛剛介紹過的StAX這種Java 6.0新支持的XML文檔解析技術之外,還有四種廣爲應用的解析方式,我們將對它們做一個簡要介紹,並比較五種技術的優缺點以及性能表現,以供大家在開發中選擇何種解析技術做參考。
一、DOM(Document Object Model)
文檔對象模型分析方式。以層次結構(類似於樹型)來組織節點和信息片段,映射XML文檔的結構,允許獲取和操作文檔的任意部分。是W3C的官方標準。
1、允許應用程序對數據和結構做出更改。
2、訪問是雙向的,可以在任何時候在樹中上下導航,獲取和操作任意部分的數據。
1、通常需要加載整個XML文檔來構造層次結構,消耗資源大。
二、SAX(Simple API for XML)
流模型中的推模型分析方式。通過事件驅動,每發現一個節點就引發一個事件,通過回調方法完成解析工作,解析XML文檔的邏輯需要應用程序完成。
1、不需要等待所有數據都被處理,分析就能立即開始。
2、只在讀取數據時檢查數據,不需要保存在內存中。
3、可以在某個條件得到滿足時停止解析,不必解析整個文檔。
4、效率和性能較高,能解析大於系統內存的文檔。
1、需要應用程序自己負責TAG的處理邏輯(例如維護父/子關係等),使用麻煩。
2、單向導航,很難同時訪問同一文檔的不同部分數據,不支持XPath。
三、JDOM(Java-based Document Object Model)
Java特定的文檔對象模型。自身不包含解析器,使用SAX。
1、使用具體類而不是接口,簡化了DOM的API。
2、大量使用了Java集合類,方便了Java開發人員。
1、沒有較好的靈活性。
2、性能較差。
四、DOM4J(Document Object Model for Java)
簡單易用,採用Java集合框架,並完全支持DOM、SAX和JAXP。
1、大量使用了Java集合類,方便Java開發人員,同時提供一些提高性能的替代方法。
2、支持XPath。
3、有很好的性能。
1、大量使用了接口,API較爲複雜。
五、StAX(Streaming API for XML)
流模型中的拉模型分析方式。提供基於指針和基於迭代器兩種方式的支持。
1、接口簡單,使用方便。
2、採用流模型分析方式,有較好的性能。
1、單向導航,不支持XPath,很難同時訪問同一文檔的不同部分。
爲了比較這五種方式在解析XML文檔時的性能表現,我們來創建三個不同大小的XML文檔:smallusers.xml(100KB)、middleusers.xml(1MB)、bigusers.xml(10MB)。我們分別用以上五種解析方式對這三個XML進行解析,然後打印出所有的用戶信息,並分別計算它們所用的時間。測試代碼會在文章後面的附件中給出,這裏只比較它們的耗時。
單位:s(秒)
100KB | 1MB | 10MB | |
DOM | 0.146s | 0.469s | 5.876s |
SAX | 0.110s | 0.328s | 3.547s |
JDOM | 0.172s | 0.756s | 45.447s |
DOM4J | 0.161s | 0.422s | 5.103s |
StAX Stream | 0.093s | 0.334s | 3.553s |
StAX Event | 0.131s | 0.359s | 3.641s |
由上面的測試結果可以看出,性能表現最好的是SAX,其次是StAX Stream和StAX Event,DOM和DOM4J也有着不錯的表現。性能最差的是JDOM。
所以,如果你的應用程序對性能的要求很高,SAX當然是首選。如果你需要訪問和控制任意數據的功能,DOM是個很好的選擇,而對Java開發人員來講,DOM4J是更好的選擇。
原文地址:
http://www.java3z.com/cwbwebhome/article/article8/81132.html
如果只需要做XML文檔解析的話,綜合性能、易用性、面向對象特徵等各方面來衡量,StAX Event無疑是最好的選擇。
附錄:
附件中包含該文章中用到的全部示例代碼,分爲兩個Eclipse工程:GreatTestProject和XMLTest,均可編譯執行。GreatTestProject是對StAX API的示例代碼;而XMLTest所有五種解析方式的使用示例,並可以針對它們做性能測試。其中,XMLTest工程的jar包默認是用maven來管理的,你可以根據需要修改。