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

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