上周周四快速了结了对于搜索引擎的集成以后,又从新回到对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