三.平台跨的不容易
本来这部分内容应该作为很后面的内容,但是由于工作已经作了,也总结了,那么就先写下来贴一下,也算是个分享吧,这部分内容在网上找了很久都没有,所以也算是不错的一个实践。
ISV有几家接了上来,有用PHP的,有.net的,这时候ASF框架的WebService继功能测试,性能测试,安全性测试进入了一个新的测试阶段,兼容性测试。由于ISV的技术力量参差不齐,所以我们需要包办实现所有语言的客户端调用Demo的工作,因此对我这个做ASF的人来说,又要懂得各个语言的客户端调用以及配置,幸好还有一个ISV Support部门也做一些这样的工作,但是由于都是新手,也没有太多的指望。
WebService之所以能够被认为是SOA最行之有效的技术手段,主要还是因为其通过wsdl规范以xml作为数据和操作请求描述的载体,基于SOAP协议在http或者smtp上传输,实现业务逻辑交互与实现语言及平台的无关性,达到跨平台交互的效果。然而作为协议,往往来说是制定了规范性的框架,但是框架内的细节实现,不同的厂商,平台,开发语言,开源框架都会有不同的实现方式,因此也造成了WebService客户端解析Soap数据包兼容性的问题。这个问题在普通的接口中不容易出现,只是在调用接口返回数据类型为对象数组的时候出现。
首先出现在Java平台的两个比较通用的开源WebService框架上:axis2,xfire。(cxf暂时还没有去做测试)。现象:axis2和xfire的两种客户端都无法正常解析ASF返回的数组对象。例如返回的是Account对象,Account有id,name,value三个属性。模拟返回2个Account对象,结果axis2客户端获得一个数组,内部有一个Account对象,不过三个属性都是没有被初始化。xfire客户端获得一个数组,内部有两个Account对象,同样属性都没有被初始化。跟踪两个客户端源码并结合返回的Soap消息分析,得到了问题的原因。
SOAP返回的包体如下:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<_ns_:getUserAccountArr2Response xmlns:_ns_="http://webservice.asf.xplatform.alisoft.com">
<return xmlns="http://webservice.asf.xplatform.alisoft.com">
<Account xmlns=””>
<accountId>11</accountId>
<isDeleted>false</isDeleted>
<accountBalance>100.23</accountBalance>
</Account>
<Account xmlns=””>
<accountId>111</accountId>
<isDeleted>false</isDeleted>
<accountBalance>111.23</accountBalance>
</Account>
</return>
</_ns_:getUserAccountArr2Response>
</soapenv:Body>
</soapenv:Envelope>
先来解释Axis2的问题,Axis2客户端在解析此包体的时候,首先检查return标签,然后根据wsdl中的描述确认内部数组对象类型为Account,然后循环获取结果集构造对象,但是按照axis2的内部逻辑处理正常的情况,应该没有Account这层标签,直接是多个结构体组装而成,由于多了Account这层外围标签,导致解析第一个对象就出现问题,因此,就出现了上面描述的结果。此时有些怀疑是否是ASF框架在返回SOAP的时候没有遵循WSDL的规范,但是没有检验过xfire也不能确定是否是没有符合规范而造成的。
在来解释一下XFire客户端调用问题的原因。同样跟踪了XFire的客户端代码,发现问题主要是出在最后给对象获取属性值的操作上。首先XFire客户端启动时会根据本地的接口包或者对象包路径来反转成为namingspace然后和属性名称一起生成QName缓存在本地,作为属性对象。然后当获得了返回SOAP消息包体的时候,根据这些QName去获取属性的内容,但是可以从上面描述的SOAP返回的内容来看,Account的namingspace丢失了,导致后面各个属性的namingspace也都丢失了。看了一下ASF在返回SOAP的代码,的却在构造SOAP返回包的时候无法获得对象的namingspace,只有它的上级return类型有namingspace,那么如何解决呢,转念一想,其实这也是一种规范,wsdl的生成工具大部分都遵循这种包反转作为namingspace的策略,因此在构造返回包体的时候采取了这个策略来填充SOAP包,XFire客户端正常。(后话,万一遇到一些和我一样自己喜欢修改wsdl的人,那么xfire就未必能够正常解析这类服务了)。从这儿也验证了ASF对于WSDL的消息包返回规范是正确的,也就也证明了axis2客户端的一个缺陷,因此在java平台暂时不建议客户使用axis2,同时axis2的客户端友好度远远低于xfire,不过axis2的优势在于配置灵活以及可插入性(这也是ASF为什么集成axis2作为默认的webservice发布框架的原因,后续blog会回顾其他几个测试的历程)
这还是开始,由于都是开源框架,所以调试和检测相对来说还比较方便。接着测试部就提出在用.net客户端调用返回对象数组出现问题,问题和XFire最早一样。当时我就很肯定地就是应该问题出在解析那些属性上。说实话,第一次接触.net,什么都不会,装了个vs 2005就开始捣鼓,不过.net真是傻瓜工具,调用webservice相当简单,就只需要建立一个web reference,其中web reference就指向一个wsdl地址,那么.net就自动替你动态生成好client了,然后就像普通的对象调用一样,直接可以操作此服务(不过ASF的webservice的发布和引用也已经做的这么傻瓜了^_^)。简单是把双刃剑,容易上手,但是容易养成不求甚解的习惯,工作到现在,要不是开发框架,我根本不会去管wsdl中哪个元素是什么用处,工具生成好了,用就罢了,只要不出错。懒倒还是一方面,最痛苦的莫过于没有办法看到源码,只能黑盒测试以及猜测,这时候我觉得java真是好。还问了一个以前的高手朋友,他做了6,7年的java然后转到.net上,我说怎么跟踪.net的源码,他和我说:“据说.net快要开放源码了”。#_#|| 我回了一句:“我基本上等不到那天了。”言归正传,下面是如何分析.net问题的报告。
Java&.Net WebService
兼容问题
Java发布的webservice 在.net客户端调用的时,数组对象类型返回兼容问题。
问题描述:
Java发布的WebService在Java客户端调用下都是正常的,但是在.net的客户端调用下,如果返回的类型是数组对象类型,那么就会发现得到了数组,并且数组内部对象生成,但是对象内部的属性值无法获得。
问题分析:
在wsdl中定义数组对象类型返回有两种方式:
1.
<xs:complexType name="Account">
<xs:sequence>
<xs:element minOccurs="0" name="accountBalance" type="xs:double"/>
<xs:element minOccurs="0" name="accountId" nillable="true" type="xs:string"/>
<xs:element minOccurs="0" name="isDeleted" nillable="true" type="xs:string"/>
</xs:sequence>
</xs:complexType>
<xs:element name="getUserAccountArrResponse">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded" minOccurs="0" name="return" nillable="true" type="xsd:Account"/>
</xs:sequence>
</xs:complexType>
</xs:element>
2.
<xs:element name="getUserAccountArr2Response">
<xs:complexType>
<xs:sequence>
<xs:element minOccurs="0" name="return" nillable="true" type="xsd:ArrayOfAccount"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="Account">
<xs:sequence>
<xs:element minOccurs="0" name="accountBalance" type="xs:double"/>
<xs:element minOccurs="0" name="accountId" nillable="true" type="xs:string"/>
<xs:element minOccurs="0" name="isDeleted" nillable="true" type="xs:string"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="ArrayOfAccount">
<xs:complexContent>
<xs:restriction base="soapenc:Array">
<xs:attribute ref="soapenc:arrayType" wsdl:arrayType="xsd:Account[]"></xs:attribute>
</xs:restriction>
</xs:complexContent>
</xs:complexType>
配置一的情况:
有两种场景出现:
场景一:
public interface IAccountService2
{
public Account checkUserAccount(String accountId);
public Account[] getUserAccountList(String accountIdBeg,String accountIdEnd);
public Account[] getUserAccountArr(String accountIdBeg);
public Account[] getUserAccountArr2(String accountIdBeg);
public double payForAppOrder(Account account,double fee);
public void delAccount(Account account,String name);
public int checkUser(String accountId,String accountId1);
}
其中接口中所有的返回或者参数对象都和接口定义在同一个包体内,这样生成wsdl的时候xsd的schema就只有一份,那么.net的客户端数组对象返回问题不存在。
场景二:
public interface IAccountService
{
public AccountBean checkUserAccount(String accountId) throws InvocationTargetException;
public AccountBean[] getUserAccountList(String accountIdBeg,String accountIdEnd);
public AccountBean[] getUserAccountArr(String accountIdBeg);
public Account[] getUserAccountArr2(String accountIdBeg);
public double payForAppOrder(AccountBean account,double fee);
public void delAccount(AccountBean account,String name);
public int checkUser(String accountId,String accountId1);
}
接口中的返回对象和接口不在一个包内,那么生成的xsd的schema就有多个,那么.net的客户端调用java发布的webservice就存在前面描述的问题。
因此用同样的wsdl分别用.net和java发布,通过.net客户端去调用,前者不存在问题,后者有问题,截获soap相应报文如下:
java 返回的soap包:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<_ns_:getUserAccountArr2Response xmlns:_ns_="http://webservice.asf.xplatform.alisoft.com">
<return xmlns="http://webservice.asf.xplatform.alisoft.com">
<Account>
<accountId>11</accountId>
<isDeleted>false</isDeleted>
<accountBalance>100.23</accountBalance>
</Account>
<Account>
<accountId>111</accountId>
<isDeleted>false</isDeleted>
<accountBalance>111.23</accountBalance>
</Account>
</return>
</_ns_:getUserAccountArr2Response>
</soapenv:Body>
</soapenv:Envelope>
.net返回的soap包:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body>
<getUserAccountArr2Response xmlns="http://webservice.asf.xplatform.alisoft.com">
<return>
<accountBalance>12.12</accountBalance>
<accountId>11</accountId>
<isDeleted xsi:nil="true"/>
</return>
<return>
<accountBalance>12.12</accountBalance>
<accountId>11</accountId>
<isDeleted xsi:nil="true"/>
</return>
</getUserAccountArr2Response>
</soap:Body>
</soap:Envelope>
但就作为wsdl中定义的话,return只有一个内容就是Account数组,java的定义应该比较符合定义内容。
部分结论:
也就是说在第一种配置情况下,wsdl中包含一个xsd的schema,.net客户端不存在任何问题。wsdl中存在多个schema的情况下,数组对象无法构造成功,
但是对于单个对象返回可以正常解析。
解决方案:
1.修改服务框架服务端代码适应.net客户端(不可行,会导致java的出现问题)
2.将这种特殊接口的schema中定义的类都放在一个包里(觉得不是很合适)
3.把对象都序列化然后作为结果返回,个人感觉性能比较低,不过可以真的减小跨平台的问题。
配置二的情况:
不存在客户端调用的构造问题,不过需要改造客户端代码(其实就是获得了xml的数据片断,自己去解析xml的数据来构造客户端对象)。此类方法在网上也很通用,可以参看 www.salesforce.com提供给第三方的API接口介绍,就是类似的。
C# Example
private void querySample()
{
QueryResult qr = null;
binding.QueryOptionsValue = new sforce.QueryOptions();
binding.QueryOptionsValue.batchSize = 250;
binding.QueryOptionsValue.batchSizeSpecified = true;
qr = binding.query("select FirstName, LastName from Contact");
bool bContinue = true;
int loopCounter = 0;
while (bContinue)
{
Console.WriteLine("/nResults Set " + Convert.ToString(loopCounter++) + " - ");
//process the query results
for (int i=0;i<qr.records.Length;i++)
{
sforce.sObject con = qr.records[i];
string fName = con.Any[0].InnerText;
string lName = con.Any[1].InnerText;
if (fName == null)
Console.WriteLine("Contact " + (i + 1) + ": " + lName);
else
Console.WriteLine("Contact " + (i + 1) + ": " + fName + " " + lName);
}
//handle the loop + 1 problem by checking to see if the most recent queryResult
if (qr.done)
bContinue = false;
else
qr = binding.queryMore(qr.queryLocator);
}
Console.WriteLine("/nQuery succesfully executed.");
Console.Write("/nHit return to continue...");
Console.ReadLine();
}
}
此时,我们的客户端代码修改成为:
原来的代码:
jdk2Service.AccountService service5 = new jdk2Service.AccountService();
jdk2Service.Account[] re = service5.getUserAccountArr("demo");
jdk2Service.Account re2 = service5.checkUserAccount("test");
现在的代码:
jdkService.AccountService service3 = new jdkService.AccountService();
jdkService.ArrayOfAccountBean res = service3.getUserAccountArr("tea");
string
name = res.Any[0].FirstChild.InnerText;//
获取了第一个返回对象的第一个属性值。
这种模式比较通用在现在的跨平台的客户端调用webservice。
因此考虑AEP接口改造成为这种方式,同时可以给客户封装类似的构造函数库提供给客户使用。
结束语:
这个报告发给了我们的架构师们以及相关人员,晚上下班到家,收到了老大的邮件,让我们总架构师向微软提出这个问题,看是否真的是这样的情况,能否有好的方法解决。这让我想起了前一阵子谁说的一句话:“有多少人打过微软的客户服务电话反映过情况”。赫赫,我们这就算是反映了,效果么……,觉得求人不如求己,开源好啊^_^