XML 是由 Tim Bray 和 Michael Sperberg-McQueen 於 1996 年引入的。它的潛力已獲得廣泛公認,但是很難想象那時候會有哪個人能夠知道 XML 會成爲怎樣的一種主要技術。企業 Java 開發人員將使用 XML 用於配置、用作數據存儲以及最常見的是用作數據交換的格式。它是 Web 服務和 SOAP 的基礎,從而也是現代面向服務的架構(Service-Oriented Architecture,SOA)設計模式的基礎。但是 XML 並沒有止步在那裏。它將 X 融入 Ajax 或 Asynchronous JavaScript + XML 中,成爲現代 Web 應用程序能夠提供前所未有的豐富體驗的關鍵。
但是,XML 也不是包治百病的靈丹妙藥;它也有不足之處。XML 文檔往往都很大。XML 文檔都有通用樹結構,但是這些 XML 文檔的可擴展性意味着它的模式可以千變萬化。這些方面向高效解析 XML 提出了挑戰。克服 XML 解析挑戰的傳統方法有兩種:DOM 和 SAX。
DOM 和 SAX 是解析 XML 的兩種典型策略。它們在許多方面都是性質對立的策略。DOM 將爲 XML 文檔提供一個簡單的對象模型。DOM 解析器將把 XML 文檔轉換成表示文檔中所有數據的易於使用的對象。但是,這樣如實地表示 XML 文檔需要付出一定的代價:DOM 解析往往需要佔用很多內存。
內存對 SAX 來說不是問題。SAX 解析器將生成一系列解析事件。註冊這些事件的回調並隨後對來自這些事件的數據執行某種邏輯都由 handler 來完成。它快速且高效,但是要求有複雜的編程模型。
瞭解使用 DOM 與 SAX 之間差異的最簡單方法 —— 並且因而瞭解 StAX 的動機和優點 —— 是查看具體示例。
找到一些 XML 來解析並不難。到處都在使用 XML。現在的大多數 Web 站點都提供了某種基於 XML 的 Web 服務。Flickr 是歸 Yahoo 所有的一個流行的照片分享站點,它擁有強大而靈活的 API。讓我們來看一看訪問 Flickr 的 “有趣” 照片的一些簡單代碼(要獲得本文中使用的所有源代碼,請參閱下載,並確保把 Woodstox 放入類路徑中或者使用 JDK 1.6)。代碼如清單 1 所示:
清單 1. 使用 Flickr API
String apiKey = "c4579586f41a90372f762cb65c78be5d"; String urlStr = "http://api.flickr.com/services/rest/?" + "method=flickr.interestingness.getList&per_page=20&api_key="+apiKey; URL request = new URL(urlStr); InputStream input = request.openStream(); |
這段代碼將使用 Flickr 的代表性狀態傳輸(Representational State Transfer,REST)API(有關 Flickr 的 API 和 REST 格式的更多信息,請參閱參考資料 部分)。以上調用的一些樣例輸出如清單 2 所示:
清單 2. Flickr 的 XML
<?xml version="1.0" encoding="utf-8" ?> <rsp stat="ok"> <photos page="1" pages="25" per_page="20" total="500"> <photo id="469774979" owner="35373726@N00" secret="c8a1be2012" server="183" farm="1" title="Where will it lead me......?" ispublic="1" isfriend="0" isfamily="0" /> <photo id="470281793" owner="73955226@N00" secret="49612a2794" server="212" farm="1" title="Island Beauty" ispublic="1" isfriend="0" isfamily="0" /> <photo id="469808244" owner="43568064@N00" secret="26b71544a3" server="227" farm="1" title="" ispublic="1" isfriend="0" isfamily="0" /> </photos> </rsp> |
注意清單 2 只顯示了三張照片。API 調用實際上將返回 20(URL 字符串中的 per_page
參數)。結果十分簡單,因此來看一看如何解析這個 XML。在示例中,將解析出每張照片的標題及其 ID。該 ID 可用於創建該照片的 URL,因此不難想象 Web 應用程序(可能是 mashup)只使用此信息。首先需要使用 DOM 來提取此數據。
要使用 DOM,需要把文檔解析成文檔對象。這是表示已被解析的 XML 文檔的內存中樹結構。隨後遍歷 DOM 樹來查找每張照片的標題和 ID。將此數據放入簡單映射中。完成此過程的代碼如清單 3 所示:
清單 3. 用 DOM 進行解析
Map<String,String> map = new HashMap<String,String>(); DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document dom = builder.parse(input); Element root = dom.getDocumentElement(); NodeList childNodes = root.getChildNodes(); Node photosNode = null; for (int i=0;i<childNodes.getLength();i++){ Node node = childNodes.item(i); if (node.getNodeName().equalsIgnoreCase("photos")){ photosNode = node; break; } } childNodes = photosNode.getChildNodes(); for (int i=0;i<childNodes.getLength();i++){ Node node = childNodes.item(i); if (node.getNodeName().equalsIgnoreCase("photo")){ String title = node.getAttributes().getNamedItem("title").getTextContent(); String id = node.getAttributes().getNamedItem("id").getTextContent(); map.put(id,title); } } |
DOM 十分流行,因爲它非常易於使用。只需將輸入源傳入解析器中,然後解析器將爲您提供 document
對象。然後,您可以遍歷子節點,直至找到照片節點。每個照片節點都是照片節點的子節點,因此您將遍歷每個照片節點,然後訪問每個照片節點的title
和
id
屬性並將其存儲到映射中。
但是,DOM 也有一些明顯的效率低下之處。您要存儲大量的可能並不關心的數據,例如每張照片的所有者。您還將瀏覽所有數據兩次:第一次瀏覽用於將其讀入文檔對象,然後在遍歷文檔對象時進行第二次瀏覽。避免這些效率低下之處的傳統方法是使用 SAX。
SAX 解析器並不像 DOM 解析器一樣返回一個精密的 document
對象。相反,SAX 解析器將在遍歷 XML 文檔時給出一系列事件。必須通過實現接口或者擴展DefaultHandler
類並根據需要重寫它的方法,創建這些事件的 handler。清單 4 將演示 Flickr XML 文檔的 SAX 解析。
清單 4. 用 SAX 進行解析
SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); DefaultHandler handler = new DefaultHandler(){ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (qName.equalsIgnoreCase("photo")){ String title = attributes.getValue("title"); String id = attributes.getValue("id"); // map is static so we can access it here map.put(id, title); } } }; parser.parse(input, handler); |
很明顯,清單 4 中所示的代碼比
清單 3 中的 DOM 代碼難於理解一些。您需要使用 ContentHandler
來處理 SAX 事件,因此創建了
DefaultHandler
並重寫了它的 startElement
回調方法。查看它是不是一個照片元素,並且如果是照片元素,則訪問它的title
和
id
屬性。
代碼十分簡潔並且在運行時非常高效。它僅存儲您所關心的數據,並且只遍歷文檔一次。它是更爲複雜的代碼,要求擴展類才能註冊事件偵聽程序。如果能夠高效地解析 XML 而且可以使用更直觀的編程模型那就太好了。StAX 於是應運而生。
SAX 中的複雜度來自它實現的 Observer 設計模式。它是一個 push 模型,因爲解析器將把事件 push 到隨後作用於事件的 observer 中。StAX 模型類似於 SAX。它將來自 XML 文檔的數據和事件流線化,使其可以像 SAX 一樣快速且高效。最大的不同之處是它使用 pull 模型。這將允許應用程序代碼從解析器中 pull 事件。
這可能聽起來像是一個細微的差異,但是它將允許使用更簡單的編程模型。查閱清單 5 以查看 StAX 的運作。
清單 5. 用 StAX 進行解析
Map<String,String> map = new HashMap<String,String>(); XMLInputFactory inputFactory = XMLInputFactory.newInstance(); QName qId = new QName("id"); QName qTitle = new QName("title"); QName qPhoto = new QName("photo"); XMLEventReader reader = inputFactory.createXMLEventReader(input); while (reader.hasNext()){ XMLEvent event = reader.nextEvent(); if (event.isStartElement()){ StartElement element = event.asStartElement(); if (element.getName().equals(qPhoto)){ String id = element.getAttributeByName(qId).getValue(); String title = element.getAttributeByName(qTitle).getValue(); map.put(id,title); } } } reader.close(); |
首先,您不必擴展任何類。那是因爲您無需爲事件進行註冊。使用 StAX,您可以控制事件流,因爲將從解析器中 pull 這些事件流。您可以使用一種熟悉的類似迭代器的語法來搜索整個文檔以查找所需的數據。您仍將只存儲所需的數據,並且只需瀏覽 XML 文檔一次。您將獲得與使用 SAX 時一樣的效率,但是代碼將直觀得多。
使用 Woodstox 作爲 Geronimo 的 StAX 提供程序
現在您已經看到了 StAX 解析的優點。它被廣泛公認爲 XML 技術中的重大進步。因而,當它成爲 Java EE 5 規範的一部分時並不令人驚訝(它甚至還包含在 Java Platform, Standard Edition [Java SE] 6 中)。由於它是 Java EE 5 的一部分,因此它必須由 Geronimo 2.0 來實現。
Geronimo 團隊十分幸運,有若干個開源 StAX 實現可供選擇。團隊選取了 Woodstox 作爲 Geronimo 所附帶的 StAX 解析器。Woodstox 被視爲執行效果最佳的 StAX 實現之一(要比較各種 StAX 解析器,請參閱參考資料)。此外,Woodstox 是在 Lesser General Public License (LGPL) 和 Apache 2.0 許可下雙重授權的。因此您可以不受任何限制地將 Woodstox 及其源代碼集成到 Geronimo 中。
性能很明顯是 Woodstox 帶給 Geronimo 的優點之一。就像使用其他高性能技術一樣,瞭解如何使用 Woodstox 才能獲得最佳性能非常重要。清單 5 中的代碼將使用XMLEventReader
接口,這是 StAX 規範包含的一個高級 API。用於獲得高性能的較低級 API 是
XMLStreamReader
接口。清單 6 顯示了使用此接口的 StAX 解析器。
清單 6. 用
XMLStreamReader
進行 StAX 解析
Map<String,String> map = new HashMap<String,String>(); XMLInputFactory inputFactory = XMLInputFactory.newInstance(); QName qId = new QName("id"); QName qTitle = new QName("title"); QName qPhoto = new QName("photo"); XMLStreamReader reader = inputFactory.createXMLStreamReader(input); while (reader.hasNext()){ int event = reader.next(); if (event == START_ELEMENT){ // statically included constant from XMLStreamConstants if (reader.getName().equals(qPhoto)){ String id = reader.getAttributeValue(null, qId.getLocalPart()); String title = reader.getAttributeValue(null, qTitle.getLocalPart()); map.put(id,title); } } } reader.close(); |
清單 6 中的代碼類似於 清單 5 中的代碼;雖然它很明顯有些低級,但是您將獲得很大的性能提高。
您已經瞭解了使用 StAX 解析器解析 XML 文檔的一些優點。StAX 在 SAX 與 DOM 之間提供了很好的折衷。您可以通過使用它作爲 Geronimo 2.0 的一部分立即利用 StAX。您不但將開始使用 StAX 的直觀 pull API,而且將獲得在 Woodstox 中使用 StAX 的高性能實現的額外優勢。