上周周四快速了結(jié)了對于搜索引擎的集成以后,又從新回到對ISV WebService接口集成和測試的支持中。測試部發(fā)現(xiàn)了一個很棘手的問題,將WS-Security集成到了ASF(應(yīng)用服務(wù)框架)中以后,接口中如果出現(xiàn)中文,就會導(dǎo)致異常拋出。這個問題相關(guān)的同事已經(jīng)跟蹤了1,2天了,但是還是沒有頭緒,離周末還有一天,我趕緊接手,希望能夠趕在測試部下周整體測試前修復(fù)這個問題。其實前面做了那么多工作,如果一旦這個問題被卡住,那么就會前功盡棄了。
首先工作就是要確定,究竟是加了WS-Security導(dǎo)致中文問題,還是本來WS的中文返回就有問題。簡單的做了單元測試驗證了一下,確認了的卻是增加了WS-Security導(dǎo)致的中文作為參數(shù)或者作為返回值都回拋出異常。根據(jù)異常的翻譯來看,就是因為在消息體中出現(xiàn)了非法字符。前幾天一位同事讓我?guī)兔?/span>WS的一個中文問題時發(fā)現(xiàn),在Http頭上對方返回的編碼方式是utf-8而在SOAP消息體中,XML的編碼方式卻寫著gbk,所以客戶端解析器在解析SOAP消息中的xml時候采用的是Utf-8的編碼方式,解析出來的中文自然是不對的。同時翻閱了一下資料,在xml 1.0的內(nèi)部二進制嵌入必須用base64編碼一下,否則是不允許出現(xiàn)非法字符,在xml 1.1已經(jīng)對這部分作了擴展。所以這次集成了WS-Security產(chǎn)生的問題,可能就會因為這兩種原因造成的。
但是事實上并不是這兩方面的原因造成,Axis2和當(dāng)前很多Web Service框架都采用了Stax方式的Jaxp(Java API for xml processing),在帶WS-Security和不帶的兩種流程中,所用的Jaxp的讀寫解析器采用的是不同的解析器,所以才會出現(xiàn)了上面的問題,因此還是要從根本上去了解Stax模式的Jaxp框架結(jié)構(gòu)及工作模式。
Stax(Streaming API for XML)
背景
Stax不是很新的概念了,在2002年就被提出,在2004年被JCP作為編號為173的JSR正式發(fā)布。因此在我們?nèi)粘i_發(fā)中如果說到Jsr 173就是關(guān)于Streaming API for XML的內(nèi)容,同時,在lib中如果列了jsr-173.jar或者stax-api.jar也就是對于Streaming API for XML的接口定義包,同時這個包內(nèi)只是定義框架接口,并沒有具體的實現(xiàn),JSR 173是接口定義規(guī)范,各個開源組織或者廠商都可以根據(jù)規(guī)范實現(xiàn)自身的Stax,這個后面的結(jié)構(gòu)介紹中就會提到。Stax的創(chuàng)始者是BEA的兩位系統(tǒng)工程師,所以在我們?nèi)蘸蟮某鲥e中,如果類似于com.bea.xml.stream.XXX Class not found 等錯誤,多半是在所有的lib庫中沒有對應(yīng)的Stax的具體實現(xiàn),只有接口定義。當(dāng)前支持JSR的實現(xiàn)有Sun,Bea,Oracle,Breeze Factor和其他的一些開源項目(最出名的就是codehaus的woodstox)。
在Jdk 1.6種已經(jīng)把Stax集成到了內(nèi)部(javax.xml.transform.stax),順便說一句的就是,在jdk1.5中,范型和對于Collection的增強是很成功的,而在jdk1.6中雖然沒有1.5的很多新特性的融入,但是仔細觀察一下就可以發(fā)現(xiàn),其實對于xml和web service的處理和支持作了不少的增強,其實也是針對當(dāng)前SOA的大趨勢作的技術(shù)方面的增強,很多概念和實現(xiàn)其實很早就在我們的應(yīng)用開發(fā)中得到體現(xiàn),個人覺得這也是Sun在開源后的最大受益之處(利用開源來匯集更多的亮點和精華),包括對于1.7的OSGI的暢想,其實都是大勢所趨。這讓我想起了昨天看程序員增刊第一部分對于Web2.0中的特質(zhì)的一項描述,利用集體智慧,有所放棄有所收獲,放棄了獨享的知識專利(當(dāng)然核心部分可以保留),獲得的是廣泛的集體參與所帶來的智慧亮點。
Stax結(jié)構(gòu)體系和功能描述
自Stax的提出開始,其本身就只是一個框架性的規(guī)范,并沒有具體的實現(xiàn)約束,這也為各個開發(fā)商和開源組織認可這個規(guī)范提供了一個最基本的優(yōu)勢條件。這點也是當(dāng)前所有的有生命力的框架或者系統(tǒng)所具備的最基本的特點之一,開放性,制定接口,規(guī)范流程,但是不約束具體的實現(xiàn)。
功能描述:
這是我第一次去看JSR的描述,JSR是Java Specification Request的縮寫,也就是規(guī)范申請,其中所需要填的描述中就詳細的寫出了申請為JSR的目的(這比類似于國內(nèi)申請專利之類的,不過感覺更為簡潔和開放)。
Streaming API for XML 描述的是一種基于java用pull方式來處理XML的API。Streaming API通過暴露簡單的Iterator模式API提供給開發(fā)者處理XML的控制權(quán)。同時當(dāng)前兩種關(guān)于XML的規(guī)范,JAXB和JAX-RPC也十分需要通過Stax模式來處理xml。
其實早在Stax出現(xiàn)之前,jdk中的已經(jīng)有了jaxp家族,sax和dom,這兩種API分別針對不同的應(yīng)用場景作了很好的封裝。SAX是用于處理XML的事件驅(qū)動方法,由很多的回調(diào)函數(shù)組成。而DOM則是將XML解析成為內(nèi)存中的樹狀結(jié)構(gòu),然后通過API來對XML中的元素和標(biāo)簽進行分析。SAX速度快,需要的內(nèi)存小,但是無法修改XML,而DOM可以提供對于XML任意節(jié)點的訪問,同時也可以寫入內(nèi)容到XML,但是缺陷就是速度慢,消耗內(nèi)存大,需要把XML全部解析完以后才能夠繼續(xù)操作。
網(wǎng)上對于Stax的好處有很多很詳細的文檔,就我自己的學(xué)習(xí)理解來看,比較重要的幾點就是:1.pull方式替代了push。Stax從名字上就可以看出和SAX十分相像,其實很大的一點不同就是在于Pull替代了Push。Sax可以實現(xiàn)很多固定的回調(diào)函數(shù),然后在執(zhí)行XML解析的過程中不斷的被調(diào)用和處理,但是缺點很明顯,首先被動回調(diào)導(dǎo)致開發(fā)者無法根據(jù)場景來動態(tài)選擇如何裁減事件處理需求,同時無法同時處理多個XML。2.Stax比SAX可以更靈活定制事件處理條件。3.Stax可以寫入XML。4.Stax除了和SAX提供了一樣的API模式的處理方式,還封裝了Event模式的對象處理方式。5.Stax比DOM性能高,應(yīng)用場景廣泛,特別是當(dāng)前很多應(yīng)用處理xml數(shù)據(jù)流時都需要邊讀邊分析,當(dāng)流結(jié)束以后就自動關(guān)閉了,此時可能已經(jīng)將流釋放,然而此時在作DOM分析已經(jīng)無法實現(xiàn),而Stax正好滿足了這種需求。因此對于Stax適用范圍的描述如下:需要清晰,有效的pull-parsing模式分析XML,另一方面也需要靈活的對XML Stream進行讀寫操作,需要創(chuàng)建新的事件類型和擴展XML的文檔類型以及屬性的情況。
Stax體系結(jié)構(gòu):
下圖中是Stax接口部分的類圖,基本上已經(jīng)包括了Stax規(guī)范中的大部分接口定義。

圖 Stax接口類圖
兩類API設(shè)計接口:Cursor API,Iterator API
Cursor API的兩個接口定義為:XMLStreamReader和XMLStreamWriter。這部分接口提供了類似于游標(biāo)方式的方法定義,能夠在XML解析過程中,從XML文檔流獲取或者寫入信息,同樣也類似于游標(biāo)讀取信息一樣,只能前進不能后退,在任意一個時刻能夠返回XML中的部分信息片斷。
Iterator API的兩個接口定義為:XMLEventReader和XMLEventWriter。Iterator API將XML輸入流看作了一組離散的事件對象,這些XML流讀入并被Parser分析,然后被解析成為這類事件對象,在開發(fā)者的應(yīng)用程序中,可以主動的Pull(拉)出需要處理的事件對象。
對于這兩種API的選擇基于下面幾點:
1.內(nèi)存有限類似于J2ME可以選擇使用cursor API。
2.性能是第一優(yōu)先級,創(chuàng)建底層的庫或者框架結(jié)構(gòu)時使用cursor API更有效。
3.如果需要類似于管道處理XML,使用Iterator API。
4.如果想要修改事件流,使用Iterator API。
5.如果需要應(yīng)用能夠處理可插入的處理流程,使用Iterator API。
6.總的來說,如果你對性能要求不是很高,建議使用Iterator API,因為它更靈活和易擴展。
工廠類
Stax采用的是抽象工廠模式來動態(tài)的根據(jù)環(huán)境配置加載不同的Stax的實現(xiàn)。在我原先查找問題的時候看來也是產(chǎn)生WS-Security中文問題的根源,當(dāng)帶WS-Security的時候?qū)τ?/span>XML流分析和讀寫采用了codehaus的woodstox包中針對Stax的Cursor API實現(xiàn),而不帶WS-Security時對于XML流分析和讀寫采用的是axis2中實現(xiàn)的Cursor API。
工廠類都是抽象類,因此都需要實例來繼承實現(xiàn),如何選取工廠類的實現(xiàn),并且通過工廠類來生成兩套API的實現(xiàn),按照以下的規(guī)則來載入:(以XMLInputFactory為例)
1. 讀取系統(tǒng)屬性,看配置中是否有javax.xml.Stream.XMLInputFactory等配置的描述。
2. 讀取Jre的lib/xml.stream.properties文件來讀取配置。
3. 從可讀取的Jar中讀取在META-INF/services/javax.xml.stream.XMLInputFactory文件來判斷載入哪一個的工廠類實現(xiàn)。
4. 使用默認的XMLInputFactory實例。
其他接口的說明
XMLResolver接口可以在分析XML的過程中對于某些資源解析定位到定義和實現(xiàn)的方法上。
XMLReporter接口用于報告所有的非致命的錯誤,致命錯誤通過XMLStreamException來報告。
對于接口使用細節(jié)可以參看sun公司的webservice tutorial。
WS-Security中文問題解決
在對新一代的Jaxp做了基本學(xué)習(xí)以后,那么對于axis2如何處理SOAP消息有了基本的了解,在跟蹤了代碼調(diào)試以后,發(fā)現(xiàn)問題主要是出在axis2的rampart模塊的Axis2Util類,其中的兩個方法getDocumentFromSOAPEnvelope(SOAPEnvelope env, boolean useDoom)和getSOAPEnvelopeFromDOMDocument(SOAPEnvelope env, boolean useDoom)。在有WS-Security和沒有的不同情況下,傳入的參數(shù)useDoom為true和false,導(dǎo)致走了兩個不同的解析流程。當(dāng)useDoom為true的時候,axis2通過SOAPEnvelope對象和axis2的Streaming parser來解析和構(gòu)建Dom Document。當(dāng)useDoom為false的時候,首先將SOAPEnvelope對象讀入字節(jié)數(shù)組流,然后在根據(jù)Stax工廠生成實例,并且構(gòu)造出StAXSOAPModelBuilder,然后返回通過StAXSOAPModelBuilder獲得的Dom Document對象。
察看了一下調(diào)用者傳入?yún)?shù)的地方,其實是通過MsgContext的參數(shù)配置來確定采取什么策略,因此只需要將axis2.xml中配置增加一個parameter,設(shè)置useDoom為true即可?;蛘呔褪亲鲆粋€handle或者phase在Inflow和outflow中配置這個參數(shù)即可。
搞了那么久也就是修改一個配置,如果光從結(jié)果看,花了兩天時間真是比較浪費,但是如果從過程來看,那么這兩天時間所學(xué)到的那還是比較值的。
由于第二天是周日,問題解決了也就沒有再繼續(xù)細究。但是周日早晨晨跑的時候,給自己列了三個疑問,首先為什么走系統(tǒng)獲取的Stax會有問題,再則如果我用sun的jaxp實現(xiàn)來替換是否能夠解決此問題而不需要配置useDoom。useDoom兩種處理模式究竟有什么區(qū)別。
問題的再次細究
周一上班的時候還是記得周日早晨提出的三個問題,因此就仔細的再分析了一下這三個問題。
首先是采取sun的jaxp替換,這個實現(xiàn)在sun的jwsdp中已經(jīng)包含了,替換以后然后強制在jre的配置文件中指定使用,出現(xiàn)了異常,看來直接使用還不行,需要針對一些參數(shù)作配置,特別是對namingspace的解析,同時也沒有花更多時間去細研究。
再則,仔細回想了一下我在定位這個問題的時候做的實驗,我曾經(jīng)試圖將中文先用Base64編碼,然后服務(wù)端接收到以后回傳,客戶端再用Base64解碼,沒有任何問題。有時候換一個中文或者中文前后有字母數(shù)字,也可以正常處理,同時在跟蹤代碼過程中看過SOAP消息中的內(nèi)容,內(nèi)容是亂碼。這讓我有點啟發(fā),例如我輸入?yún)?shù)為“岑文初”每次始終都會出錯,如果輸入為“岑文”,就沒有問題,看了看內(nèi)存內(nèi)的變量,發(fā)現(xiàn),原來如果是“岑文初”的時候SOAP消息中的標(biāo)簽封閉被破壞,如果是“岑文”,雖然是亂碼,但是沒有破壞標(biāo)簽的封閉。
仔細看了看上次提到的兩個流程,其實兩個流程除了parser不同以外,對于SOAPEnvelope的處理也是不一樣的,走UseDoom的是直接將分析好的Dom對象返回,不做附加的處理,只是根據(jù)Envelope生成了SOAP的解析器以及配置了Stax的Cursor的兩個接口實現(xiàn)類。不走UseDoom的情況則是完全將SOAPEnvelope再次序列化并且通過外部的Stax實現(xiàn)來解析和處理,但是問題就出在對象到字節(jié)流的序列化過程,默認的是使用了SOAP規(guī)定的utf-8編碼方式,因此在這個過程中有些中文的內(nèi)容就破壞了SOAP的消息包XML的標(biāo)簽合法性,導(dǎo)致外部解析器分析出現(xiàn)問題。如果將傳入和傳出的中文都編碼成utf-8沒有任何問題。
問題總結(jié)就是其實根源在于對于內(nèi)容中的中文字符編碼時采用Utf-8破壞了xml的封閉性,而我開始采用的useDoom正好規(guī)避了這個過程,也就自然通過了。但是就其設(shè)計本身來說,rampart應(yīng)該是贊同使用useDoom為false的方式,這才是真正的Stax的模式,同時有很好擴展性。另一方面?zhèn)€人覺得類似于這種抽象工廠機制來說,最好不要在系統(tǒng)變量或者jre中強制指定,這樣會導(dǎo)致一些意想不到的問題,雖然是規(guī)范,但是細節(jié)實現(xiàn)畢竟有差異,因此一些特殊的開源框架的一些莫名其妙的xml解析問題也常常由于這些引起。
幾點感悟
靈活的SPI(Service Provider Interface)模式是當(dāng)前框架設(shè)計以及底層設(shè)計的必要特質(zhì),開放才會發(fā)展得更好。
靈活是把雙刃劍,在遇到一些靈活的框架設(shè)計時,首先必須了解其原理和結(jié)構(gòu),然后根據(jù)實踐來驗證問題的緣由。
抽象工廠還是有適用場景的,類似于Jaxp和SCA等框架的實現(xiàn),抽象工廠以及利用Jar的META-INF/services來載入SPI的實現(xiàn)是IOC的一種很好的補充。
更多內(nèi)容請訪問我的blog:http://blog.csdn.net/cenwenchu79