原帖地址:http://www.ibm.com/developerworks/cn/xml/x-stax1.html
2007 年 3 月 02 日
Streaming API for XML (StAX) 是用 Java™ 語言處理 XML 的最新標準。作為一種面向流的方法,無論從性能還是可用性上都優于其他方法,如 DOM 和 SAX。本系列分為 3 部分,本文是第 1 部分,簡要介紹了 StAX 及其處理 XML 的基于指針的 API。
StAX 概述
從一開始,Java API for XML Processing (JAXP) 就提供了兩種方法來處理 XML:文檔對象模型(DOM)方法是用標準的對象模型表示 XML 文檔;Simple API for XML (SAX) 方法使用應用程序提供的事件處理程序來處理 XML。JSR-173 提出了一種面向流的新方法:Streaming API for XML (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 風格,要用到抽象工廠模式;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);
// ...
|
XMLStreamReader
和 XMLEventReader
都允許應用程序迭代底層的 XML 流。兩種方法的差別在于如何公開解析后的 XML InfoSet 信息片段。XMLStreamReader
就像一個指針,指在剛剛解析過的 XML 標記的后面,并提供了方法獲得更多關于該標記的信息。這種方法節約內存,因為不用創建新的對象。但是,業務應用程序開發人員可能會發現 XMLEventReader
更直觀一些,因為它實際上就是一個標準的 Java 迭代器,將 XML 變成了事件對象流。每個事件對象都封裝了它所表示的特定 XML 結構固有的信息。本系列的第二部分將詳細討論這種基于事件迭代器的 API。
使用哪種風格的 API 取決于具體情況。和基于指針的 API 相比,基于事件迭代器的 API 具有更多的面向對象特征。因此更便于應用于模塊化的體系結構,因為當前的解析器狀態反映在事件對象中,應用程序組件在處理事件的時候不需要訪問解析器/讀取器。此外,還可以使用 XMLInputFactory
的 createXMLEventReader(XMLStreamReader)
方法從 XMLStreamReader
創建 XMLEventReader
。
StAX 還定義了一種序列化器 API,Java 標準 XML 處理支持中一直缺少的一種特性。和解析一樣,也包含兩種風格的流式 API:處理標記的底層 XMLStreamWriter
和處理事件對象的高層 XMLEventWriter
。XMLStreamWriter
提供了寫入單個 XML 記號(比如開始和關閉標記或者元素屬性)的方法,不檢查這些標記是否格式良好。另一方面,XMLEventWriter
允許應用程序向輸出中添加完整的 XML 事件。第 3 部分將詳細討論 StAX 序列化器 API。
為什么使用 StAX?
開始學習一種新的處理 XML 的 API 之前,可能要問是否值得這樣做。事實上,StAX 所采用的基于拉的方法和其他方法相比有一些突出的優點。首先,不管使用哪種 API 風格,都是應用程序調用讀取器(解析器)而不是相反。通過保留解析過程的控制權,可以簡化調用代碼來準確地處理它預期的內容。或者發生意外時停止解析。此外,由于該方法不基于處理程序回調,應用程序不需要像使用 SAX 那樣模擬解析器的狀態。
StAX 仍然保留了 SAX 相對于 DOM 的優點。通過把重心從結果對象模型轉移到解析流本身,從理論上說應用程序能夠處理無限的 XML 流,因為事件固有的臨時性,不會在內存中累積起來。對于那些使用 XML 作為消息傳遞協議而非表示文檔內容的那些應用程序尤其重要,比如 Web 服務或即時消息應用程序。比方說,如果只是將其轉換成特定于應用程序的對象模型然后就將其丟棄,那么為 Web 服務路由器 servlet 提供一個 DOM 就沒有多少用處。使用 StAX 直接轉化成應用程序模型效率更高。對于 Extensible Messaging and Presence Protocol(XMPP)客戶機,根本不能使用 DOM,因為 XMPP 客戶機/服務器流是隨著用戶輸入的消息實時生成。等待流的關閉標簽(以便最終建立 DOM)就意味著等待整個會話結束。通過把 XML 作為一系列的事件來處理,應用程序能夠以最合適的方式響應每個事件(比如顯示收到的即時消息等等)。
由于其雙向性,StAX 也支持鏈式處理,特別是在事件層上。接收事件(無論什么來源)的能力被封裝在 XMLEventConsumer(XMLEventWriter 的擴展)接口中。因此,可以模塊化地編寫應用程序從 XMLEventReader(也是一個普通的迭代器,可以按迭代器處理)讀取和處理 XML 事件、然后傳遞給事件消費者(如果需要可以進一步擴展處理鏈)。在第 2 部分將看到,也可使用應用程序提供的篩選器(實現了 EventFilter 接口的類)來定制 XMLEventReader 或者使用 EventReaderDelegate 修飾已有的 XMLEventReader。
總而言之,和 DOM 以及 SAX 相比,StAX 使應用程序更貼近底層的 XML。使用 StAX,應用程序不僅可以建立需要的對象模型(而不需要處理標準 DOM),而且可以隨時這樣做,而不必等到解析器回調。
下一節將深入討論基于指針的 API 以及如何有效地使用它處理 XML 流。
基于指針的 API
如果使用基于指針的 API,應用程序通過在 XML 標記流中移動邏輯指針來處理 XML。基于指針的解析器實質上是一個狀態機,在事件的驅動下從一個良好定義的狀態轉移到另一個狀態。這里的觸發事件是隨著應用程序使用適當的方法推動解析器在標記流中前進而解析出來的 XML 標記。在每個狀態,都可使用一組方法獲得上一個事件的信息。一般來說,并非每個狀態下都能使用所有的方法。
使用基于指針的方法,應用程序首先必須通過調用其 createXMLStreamReader
方法從 XMLInputFactory
得到 XMLStreamReader
。該方法有多個版本,支持不同類型的輸入。比方說,可以創建 XMLStreamReader
解析 plain java.io.InputStream
、java.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_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
) 可以聲明預期的解析器狀態;除非當前事件是指定的類型,并且本地名和名稱空間(如果給出的話)與當前事件匹配,否則該方法將拋出異常。
清單 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.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_INSTRUCTION
的目標和數據。ENTITY_REFERENCE
和 DTD
也支持 getText()
,ENTITY_REFERENCE
還支持 getLocalName()
。
解析完成后,應用程序關閉讀取器并釋放解析過程中獲得的資源。請注意這樣并沒有關閉底層的輸入源。
清單 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
的高級用法
通過調用 XMLInputFactory
的帶有基本讀取器的 createFilteredReader
方法和一個應用程序定義的篩選器(即實現 StreamFilter
的類實例),可以創建篩選過的 XMLStreamReader
。導航篩選過的讀取器時,讀取器每次移動到下一個標記之前都會詢問篩選器。如果篩選器認可了當前事件,就將其公開給篩選過的讀取器。否則跳過這個標記并檢查下一個,依此類推。這種方法可以讓開發人員創建一個僅處理解析內容子集的基于指針的 XML 處理程序,并與針對不同的擴展的內容模型的篩選器結合使用。
執行更復雜的流操作,可以創建 StreamReaderDelegate
的子類并重寫合適的方法。然后使用這個子類的實例包裝基本 XMLStreamReader
,從而為應用程序提供一個修改過的基本 XML 流的視圖。可通過這種技術對 XML 流執行簡單的轉換,比如篩掉或者替換特定的標記,甚至增加新的標記。
清單 6 用定制的 StreamReaderDelegate
包裝了基本 XMLStreamReader
,重寫了 next()
方法來跳過 COMMENT
和 PROCESSING_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 之外增加一種面向對象的 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。
參考資料
學習
獲得產品和技術
討論
關于作者
 |
|
 |
Peter Nehrer 是一名專長于基于 Eclipse 的企業解決方案和 Java EE 應用程序的軟件顧問。他創建了 Ecliptical Software Inc.,并且是一些和 Eclipse 有關的開放源碼項目的貢獻者。他擁有從馬薩諸塞州大學阿默斯特校區獲得的計算機科學碩士學位。
|