去年 8月份的一份手稿,完整描述了我对j2me-kSOAP如何和服务器端的Web Service交互的经验和教训。本手稿已刊登在mingjava兄弟的新书中。
[j2me]kSOAP 的运用
编写者
|
日期
|
关键词
|
郑昀
@ultrapower
|
2006-8-24
|
J2me webservice ksoap
|
1
.概述
对于
J2ME
访问远端的
Web Service
,除了官方标准
JSR 172
,我们还有两种选择:
l
kSOAP
l
Wingfoot
Wingfoot
是由
Wingfoot Software(www.wingfoot.com)
出品的一款
J2ME(CLDC/CDC) SOAP1.1
的轻量级实现方案。
kSOAP
是
Enhydra.org
的一个开源作品,是
EnhydraME
项目的一部分。基于
Enhydra.org
出品的开源通用
XML
解析器
kXML
,
kSOAP
完成
了
J2ME/MIDP
平台上的
SOAP
解析和调用工作
。
Stefan Haustein
领导的
kSOAP
开发小组于
2001
年
5
月
17
日推出了
Alhpa
版本。之后又经过了一年的开发,
2002
年
6
月
6
日推出的
kSOAP 1.2
支持了
SOAP1.2
规范。
2003
年
8
月
25
日推出的
kSOAP2
,对
SOAP
序列化规范支持得更好了。
大多数人选择
kSOAP
的原因是,
kSOAP
虽然在
2003
年
8
月之后就不再维护了,但它是
Open Source
的,很容易加入增强特性,比如说默认情况下
kSOAP2
仅仅支持
cmnet
接入点,可以修改
kSOAP2
的
HttpTransport.java
代码增加对
cmwap
接入点的支持。
kSOAP2.0
还有一个优点是,改进了对
Microsoft dotNET
的兼容。以前有很多人抱怨
kSOAP
调用
dotNET
编写的
Web Service
时遇到了不少的困扰。
本章节我们将使用
kSOAP 2.0
的例子来讲解。
为了使用
kSOAP 2.0
,必须还要下载工具包
kXML2
。
kXML2
比
kXML
更小更快。
2
.
kSOAP2
接口
让我们先熟悉一下即将用到的
kSOAP2
的常用接口。
接口
|
org.ksoap2.
SoapEnvelope
org.ksoap2.
SoapSerializationEnvelope
org.ksoap2.
SoapObject
org.ksoap2.transport.
HttpTransport
|
SoapEnvelope
对应于
SOAP
规范中的
SOAP Envelope
,封装了
head
和
body
对象。
SoapSerializationEnvelope
是
kSOAP2
新增加的类,是对
SoapEnvelope
的扩展,对
SOAP
序列化
(Serialization)
格式规范提供了支持,能够对简单对象自动进行序列化
(simple object serialization)
。而
kSOAP1.x
则是通过
org.ksoap.ClassMap
来做序列化的,不太好操作,也不利于扩展。
SoapObject
让你自如地构造
SOAP
调用;
HttpTransport
为你屏蔽了
Internet
访问
/
请求和获取服务器
SOAP
的细节。
下面我们通过一个最简单的
webservice
调用,来看看
kSOAP
是如何做到
SOAP
解析的:
2
.
1
.
kSOAP
和
Web Service
之间传递
String
webservice
传递
String
给
MIDP
是一件很简单的事情
。首先在服务器端,不管你是用
Microsft ASP.NET
创建
webservice
,还是由
Tomcat+AXIS1.2
支撑的
webservice
,都可以这么编写主服务类:
服务器端
|
public class SimpleKSoapWS {
public SimpleKSoapWS () {
}
public String
foo(String username, String password) {
return “fooResult”;
}
}
|
kSOAP
是如何调用这个
webservice
的呢?
首先要使用
SoapObject
,这是一个高度抽象化的类,完成
SOAP
调用。可以调用它的
addProperty
方法填写要调用的
webservice
方法的参数。如下面代码所示:
SoapObject request
= new SoapObject(serviceNamespace, methodName);
SoapObject
构造函数的两个参数含义为:
serviceNamespace –
你的
webservice
的命名空间,既可以是
http://localhost:8088/flickrBuddy/services/Buddycast
这样的,也可以是
urn:PI/DevCentral/SoapService
这样的;
methodName –
你要调用方法的名字。
然后,按照
webservice
方法参数的顺序,依次调用
request.addProperty( "username", "user" );
request.addProperty( "password", "pass" );
来填充
webservice
参数。
建议
webservice
的方法传递的参数尽量用
string
类型。即使是
int
类型,
kSOAP2
与
Java
编写的
webservice
也有可能交互发生异常。
|
对于
webservice
方法返回
String
类型的情况,还用不着开发者做序列化
(Serialization)
定制工作。
kSOAP 1.X/2.0
可以自动把四种
SOAP
类型映射为
Java
类型
SOAP type
Java type
xsd:int
java.lang.Integer
xsd:long
java.lang.Long
xsd:string
java.lang.String
xsd:boolean
java.lang.Boolean
除此之外,都需要开发者自己做类型映射。
|
然后要告诉
SoapSerializationEnvelope
把构造好的
SoapObject
封装进去:
SoapSerializationEnvelope envelope =
new SoapSerializationEnvelope(SoapEnvelope.VER11);
envelope.bodyOut = request;
你可以通过
SoapSerializationEnvelope
或者
SoapEnvelope
的构造函数来指明你要用
SOAP
的哪一个规范,可以是以下几种之一:
常量
SoapEnvelope.VER10
:对应于
SOAP 1.0
规范
常量
SoapEnvelope.VER11
:对应于
SOAP 1.1
规范
常量
SoapEnvelope.VER12
:对应于
SOAP 1.2
规范
这样,无论要调用的
webservice
采用了哪一个
SOAP
规范,你都可以轻松应对。
|
接下来就要声明
HttpTransport tx = new HttpTransport(serviceURL);
ht.debug = true;
HttpTransport
构造函数的参数含义为:
serviceURL –
要投递
SOAP
数据的目标地址,譬如说
http://soap.amazon.com/onca/soap3
。
HttpTransport
是一个强大的辅助类,来完成
Http-call transport process
,它封装了网络请求的一切,你完全不用考虑序列化消息。我们通过设置它的
debug
属性为
true
来打开调试信息。
方法
HttpTransport.call()
自己
就能够发送请求给服务器、接收服务器响应并序列化
SOAP
消息,如下所示:
ht.call(null, envelope);
HttpTransport
的
call
方法的两个参数含义为:
soapAction – SOAP
规范定义了一个名为
SOAPAction
的新
HTTP
标头,所有
SOAP HTTP
请求(即使是空的)都必须包含该标头。
SOAPAction
标头旨在表明该消息的意图。通常可以置此参数为
null
,这样
HttpTransport
就会设置
HTTP
标头
SOAPAction
为空字符串。
Envelope –
就是前面我们构造好的
SoapSerializationEnvelope
或
SoapEnvelope
对象。
对于
HttpTransport
的处理上,
kSOAP2
和
kSOAP1.2
的写法不一样。
对于
kSOAP 1.2
,
HttpTransport
的构造函数是
HttpTransport (String url, String soapAction)
,第二个参数
soapAction
可以是要调用的
webservice
方法名。
而
kSOAP 2
,构造函数是
HttpTransport(String url)
。
kSOAP2
相当于把
webservice
方法名分离出去,完全交给
SoapObject
去封装,而
HttpTransport
仅仅负责把
SoapEnvelope
发送出去并接收响应,这样更合理一些。
|
调用
call
方法是一个同步过程,需要等待它返回。
返回之后,就可以调用
SoapSerializationEnvelope
的
getResult
方法来获取结果了:
Object Response = envelope.getResult();
如果
HttpTransport
的
debug
属性为
true
,那么此时就可以通过
System.out.println("Response dump>>"
+ tx.responseDump);
打印出
HttpTransport
的调试信息。尤其当前面
call
方法和
getResult
方法发生异常时,这个调试信息是非常有用的。
前面我们的
webservice
方法由于是返回
string
,所以得到这个
string
值就非常简单了:
String sResponse = (String)Response;
由于
HttpTransport
类实际上是调用了
HttpConnection
作网络连接,所以必须另起一个线程来专门做
kSOAP
工作,否则会堵塞操作。
|
综上所述,
J2ME
客户端的
MIDlet
按键事件函数这么写即可:
MIDlet codes
|
import org.ksoap2.SoapEnvelope;
import org.ksoap2.serialization.SoapObject;
import org.ksoap2.serialization.SoapSerializationEnvelope;
import org.ksoap2.transport.HttpTransport;
public void commandAction(Command c, Displayable s) {
if (c == exitCommand)
{
destroyApp(false);
notifyDestroyed();
}
if (c == connectCommand)
{
//
匿名内部
Thread
,调用
kSOAP2
访问远程服务。
Thread webserviceThread = new Thread()
{
public void run(){
try
{
String serviceNamespace =
"http://localhost:8080/SimpleWS/services/SimpleKSoapWS";
String methodName = "foo";
String serviceURL =
"http://localhost:8080/SimpleWS/services/SimpleKSoapWS";
SoapObject request
=
new SoapObject(serviceNamespace, methodName);
request.addProperty( "username", "user" );
request.addProperty( "password", "pass" );
SoapSerializationEnvelope envelope =
new SoapSerializationEnvelope(SoapEnvelope.VER11);
envelope.bodyOut = request;
HttpTransport tx = new HttpTransport(serviceURL);
ht.debug = true;
ht.call(null, envelope);
Object Response = envelope.getResult();
/*
*
必要时打印出
tx.responseDump
来观察
soap
是否正确工作
*/
System.out.println("dump>>" + tx.responseDump);
String sResponse = (String)Response;
}
catch (Exception e) {
e.printStackTrace ();
}
}
};
webserviceThread.start();
}
|
2
.
2
.
webservice
返回复杂描述的情况
kSOAP2
处理
webservice
简单的
string
类型返回值是很容易的。那么如何处理像亚马逊网上书店这种
webservice
返回的复杂描述呢?
kSOAP2
自带了一个例子来说明,下面我们就讲解一下。
关于亚马逊的查询书目的
webservice
,你可以通过
http://soap.amazon.com/schemas3/AmazonWebServices.wsdl
来获知定义。
我们要关注的是它的关键词查询请求的方法,它的定义是:
<
operation name
="
KeywordSearchRequest
">
<
soap:operation
soapAction
="
http://soap.amazon.com
" />
<
soap:body
use
="
encoded
"
encodingStyle
=
http://schemas.xmlsoap.org/soap/encoding/
namespace
="
http://soap.amazon.com
" />
</
input
>
<
output
>
<
soap:body
use
="
encoded
"
encodingStyle
=
http://schemas.xmlsoap.org/soap/encoding/
namespace
="
http://soap.amazon.com
" />
</
output
>
</
operation
>
我们提交对包含指定关键词的书目查询,如果查询成功,将会返回一系列书名节点,每一本书都提供了作者、出版社、出版日期、价格等等信息。这些书名节点都在一个“
Details
”节点下。查询结果的总数放在
TotalResults
节点。每页
10
个结果,可以通过查看
TotalPages
节点来确定需要多少页。
那么,
kSOAP2
可以很简单地通过
SoapObject
的
getProperty
方法来得到书详细信息的节点,存储入一个
Vector
对象中,如下所示:
HttpTransport ht = new HttpTransport("http://soap.amazon.com/onca/soap3");
ht.call(null, envelope);
SoapObject result = (SoapObject) envelope.getResult();
Vector resultVector = (Vector) result.getProperty("Details");
Vector
对象中实际上还是存储了一组
SoapObject
对象,这里的每一个
SoapObject
对象对应于一本书的
DOM
对象。
那么如何得到每一本书的书名、价格呢?
for(int i = 0; i < resultVector.size(); i++){
SoapObject detail = (SoapObject) resultVector.elementAt(i);
System.out.println("
书名
>>"+
(String) detail.getProperty("ProductName"));
System.out.println("
日期
>>"+
(String) detail.getProperty("ReleaseDate"));
System.out.println("
价格
>>"+
(String) detail.getProperty("ListPrice"));
}
这样就可以了。
需要注意的是,要测试这个工程,必须到亚马逊的
http://www.amazon.com/webservice
注册获取
Access Key ID
,也就是
webservice
方法中的“
devtag
”参数所需要的
Developer-Tag
。
2
.
3
.
webservice
传递自定义复杂对象
下面我们讲述如何在
MIDP
设备和
webservice
之间传递自定义类,比如这个类中不但有
String
类型成员变量,还有
Vector
之类的复杂类型
。
大致思路就是,在服务器端将类实例按照一定规格(
一个一个的成员变量写
)序列化为
byte[]
,将这个
byte[]
数组返回给
kSOAP2
。
kSOAP2
收到之后
,
再
反序列化,将
byte[]
一段一段地读入类实例。
2
.
3
.
1
.
webservice
服务器端的做法
我们先来定义要传递的
wsTeam
类:
类定义
|
public class wsTeam{
private String wsReturnCode;
private String wsPersonCount;
public StringVector
wsvPersonName;
public byte[] serialize();
public static wsTeam deserialize(byte[] data) ;
}
|
其中,
StringVector
类是另外一个自定义类,就是简单地把
String[]
封装了一下,便于操作。
StringVector
类定义在示范代码中可以找到。
服务器端主要是序列化,所以我们来讲讲
wsTeam
的
serialize()
函数。
wsTeam
的序列化函数
|
public byte[] serialize() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
try
{
dos.writeUTF(wsReturnCode);
dos.writeUTF(wsPersonCount);
wsvPersonName.writeObject(dos);
baos.close();
dos.close();
}
catch(Exception exc)
{
exc.printStackTrace();
}
return baos.toByteArray();
}
|
这样,类实例就可以把自己序列化为
byte[]
数组。
那么,
webservice
可以这么提供:
服务器端
|
public class SimpleKSoapWS {
public SimpleKSoapWS () {
}
public byte[]
foo2(String username, String password) {
wsTeam
obj= new
wsTeam
();
return obj.serialize();
}
}
|
到了
MIDP
设备上,要能够从
byte[]
恢复出
wsTeam
类实例才行。
StringVector
的序列化方法
writeObject
也很简单,先写入字符串数组的大小,然后再将每一个元素写入,如下所示:
StringVector
的序列化
|
public class StringVector
{…
public synchronized void writeObject(java.io.DataOutputStream s)
throws java.io.IOException
{
//
Write out array length
s.writeInt(count);
//
Write out all elements in the proper order.
for (int i=0; i<count; i++)
{
s.writeUTF(data[i]);
}
}
…
}
|
2
.
3
.
2
.
MIDP
设备的做法
和前面的
MIDlet
代码差不多,只不过要
kSOAP2
的
MarshalBase64
出场了
。
在
kSOAP
中,我们用
Base64
把二进制流编码为
ASCII
字符串,这样就可以通过
XML/SOAP
传输二进制数据了。
org.ksoap2.serialization.MarshalBase64
的目的就是,把
SOAP XML
中的
xsd:based64Binary
元素序列化为
Java
字节数组
(byete array)
类型。类似的,
kSOAP2
还提供了
MarshalDate
、
MarshalHashtable
类来把相应的元素序列化为
Java
的
Date
、
Hashtable
类型。
使用
MarshalBase64
|
import org.ksoap2.serialization.MarshalBase64;
SoapObject request = new SoapObject(serviceNamespace, methodName );
SoapSerializationEnvelope envelope =
new SoapSerializationEnvelope(SoapEnvelope.VER11);
envelope.bodyOut = request;
new MarshalBase64().register(envelope);
HttpTransport tx = new HttpTransport(serviceNamespace);
tx.debug = true;
tx.call(null, envelope);
Object Response = envelope.getResult();
|
将接收到的
SoapObject
强制转换为
byte[]
。
转换
|
byte[] by = (byte[])Response;
System.out.println("succ convert!");
|
然后,再调用
反序列化
|
wsTeam wc = wsTeam.deserialize(by);
|
这样,在无线设备上就得到了
wsTeam
类实例了。
wsTeam
的
deserialize
函数
是这么定义的:
wsTeam
的反序列化函数
|
public class StringVector
{…
public static wsTeam deserialize(byte[] data) {
ByteArrayInputStream bais = new ByteArrayInputStream(data);
DataInputStream dis = new DataInputStream(bais);
wsTeam wc = new wsTeam();
try
{
wc.wsReturnCode = dis.readUTF();
wc.wsPersonCount = dis.readUTF();
wc. wsvPersonName.readObject(dis);
bais.close();
dis.close();
}
catch(Exception exc)
exc.printStackTrace();
}
return wc;
}
…}
|
StringVector
的反序列化方法
readObject
也很简单,先读入字符串数组的大小,就自行新建一个同样大小的字符串数组,然后再将每一个元素写入这个数组,如下所示:
StringVector
的反序列化
|
public class StringVector
{…
public synchronized void readObject(java.io.DataInputStream s)
throws java.io.IOException, ClassNotFoundException
{
//
Read in array length and allocate array
int arrayLength = s.readInt();
data = new String[arrayLength];
//
同步
data
的大小
count = arrayLength;
//
Read in all elements in the proper order.
for (int i=0; i<arrayLength; i++)
{
data[i] = s.readUTF();
}
}…
}
|
通过上面的反序列化,我们就可以通过
for (int i=0; i<wc.wsvPersonName.size(); i++) {
System.out.println("
第
" + i +"
个人:
" +
wc.wsvPersonName.getStringAt(i));
}
来打印
MIDlet
上收到的类对象中的
StringVector
成员变量了。
3
.小结
利用
kSOAP2
提供的框架,你可以在无线设备和
Internet webservice
之间,既可以传递简单的数值,也可以传递各种各样的类对象。
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1485436