Java6.0新特性StAX--全面解析

 野馬(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之前,就已經有四種:

  1. DOM:Document Object Model
  2. SAX:Simple API for XML
  3. JDOM:Java-based Document Object Model
  4. DOM4J:Document Object Model for Java
   關於它們的解析原理,以及性能和優缺點,我會在本文的結尾做一個簡要的介紹。這篇文章中,我們主要說說StAX這種新的解析方式。

首先我們來搞清楚兩個概念:推分析拉分析

在程序中訪問和操作XML文件一般有兩種模型:DOM(文檔對象模型)和流模型。它們的優缺點如下:

引用
DOM優點:允許編輯和更新XML文檔,可以隨機訪問文檔中的數據,可以使用XPath(XML Path Language,
是一種從XML文檔中搜索節點的查詢語言)查詢。
DOM缺點:需要一次性加載整個文檔到內存中,對於大型文檔,會造成性能問題。


引用
流模型優點:對XML文件的訪問採用流的概念,在任何時候內存中只有當前節點,解決了DOM的性能問題。
流模型缺點:是隻讀的,並且只能向前,不能在文檔中執行向後導航操作。


關於什麼是DOM,文章結尾處會有介紹。這裏我們簡單說一下流:它是一個連續的字節序列,可以理解爲不停地從源頭向目標搬運着字節的特殊對象。

讓我們回到主題。流模型每次迭代XML文檔中的一個節點,適合於處理較大的文檔,所耗內存空間小。它有兩種變體--“推”模型和“拉”模型。



推模型:就是我們常說的SAX,它是一種靠事件驅動的模型。當它每發現一個節點就引發一個事件,而我們需要編寫這些事件的處理程序。這樣的做法很麻煩,且不靈活。


拉模型:在遍歷文檔時,會把感興趣的部分從讀取器中拉出,不需要引發事件,允許我們選擇性地處理節點。這大大提高了靈活性,以及整體效率。


到此,我們就弄明白了“推分析”和“拉分析”的概念:

引用
基於流模型中推模型的分析方式稱爲推分析;基於流模型中拉模型的分析方式就稱爲拉分析。


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是間接定位外部資源。具體一點說是這樣:

systemId:外部資源(大多是DTD文件)的URI。比如本地文件file:///user/dtd/users.dtd或者網絡某個地址的文件http://www.w3.org/dtd/users.dtd。


publicId:相當於一個名字,這個名字代表了一個外部資源。比如,我們規定"W3C HTML 4.0.1"這個字符串對應"http://www.w3.org/dtd/users.dtd"這個資源。那麼,publicId="W3C HTML 4.0.1"和systemId="http://www.w3.org/dtd/users.dtd"的作用就是一樣的。


好了,我們接着用以上列出的第一個接口來創建一個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接口中定義的一個標記常量,表示當前指針所指向標記(或事件)的類型。根據當前事件類型的不同,應用程序可以做出不同的處理。標記常量的類型和含義如下:

  1. START_DOCUMENT:文檔的開始
  2. END_DOCUMENT:文檔的結尾
  3. START_ELEMENT:元素的開始
  4. END_ELEMENT:元素的結尾
  5. PROCESSING_INSTRUCTION:處理指令
  6. CHARACTERS:字符(文本或空格)
  7. COMMENT:註釋
  8. SPACE:可忽略的空格
  9. ENTITY_REFERENCE:實體的引用
  10. ATTRIBUTE:元素的屬性
  11. DTD:DTD
  12. CDATA:CDATA塊
  13. NAMESPACE:命名空間的聲明
  14. NOTATION_DECLARATION:標記的聲明
  15. 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();
	}
}
運行結果:
引用
Name:Tom
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();
	}
運行結果:
1.name=Tom;age=28;gender=male;
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();//判斷該事件對象是否是處理指令
第二類是將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();
	}
運行後得到如下結果:
company
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接口自身外,其餘接口都是它的子接口。它們的名稱和代表的具體事件對象類型如下:

  1. Attribute:元素的屬性
  2. Characters:字符
  3. Comment:註釋
  4. DTD:DTD
  5. StartDocument:文檔的開始
  6. EndDocument:文檔的結束
  7. StartElement:元素的開始
  8. EndElement:元素的結束
  9. EntityDeclaration:實體聲明
  10. EntityReference:實體的引用
  11. Namespace:命名空間聲明
  12. NotationDeclaration:標記的聲明
  13. ProcessingInstruction:處理指令
   你可能覺得這些類看着很眼熟,因爲它們在XMLStreamReader的getEventType()方法的返回值,也就是XMLStreamConstants中定義的常量中,都能找到一一的對應。唯獨缺少了SAPCE(可忽略的空白)和CDATA(CDATA塊)。也就是說,在基於指針的StAX API中定義事件類型,在基於迭代器的StAX API中都是以對象的形式提供給應用程序的,這就是爲什麼說後者是一種更具有面向對象思想的高層API的原因。

這些事件對象接口不僅代表了一種事件類型,還包含對應事件對象的信息。至於它們所具有的方法大多是獲取事件對象信息的訪問器,其含義及具體用法,都很容易理解和使用,因此不再詳細介紹。

大家可能注意到,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=Tom
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=Tom;age=28
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來管理的,你可以根據需要修改。
發佈了51 篇原創文章 · 獲贊 6 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章