从何时选择移动 Web 服务到总体设计指导原则再到用于移动 Web 服务的值类型,本文提出了在设计用于移动设备的 Web 服务时需要考虑的许多设计事项。文中还介绍了许多设计移动 Web 服务方面的最佳实践。从本文中,您可以了解如何决定何时使用 Web 服务、在设计 Web 服务时需要考虑什么事项,以及在规划移动 Web 服务时必须谨记哪些问题。
Web 服务是一种集成技术。在集成异构系统时 Web 服务的价值可以得到最好的证明,因为其支持许多类型的编程语言、运行时环境和网络。当需要从不兼容的环境连接应用程序时,Web 服务就有了用武之地。通过 Web 服务,您可以将业务应用程序从 java™ 2 Platform EnterPRise Edition (J2EE) 连接到 .NET。您还可以使用某个运行在 linux™ 中的应用程序将一个应用程序集成在 Windows™ 操作环境中。在本文中,我提供了一些针对移动 Web 服务的重要设计考虑事项,并且向您介绍了一些与之有关的最佳实践。
首先,我将讨论在开始之前 需要考虑哪些事项。
开始之前
在开始设计整个系统的体系结构之前,您必须做出如下决定——何时使用移动 Web 服务以及何时不使用移动 Web 服务。
对于移动设备,Web 服务是利用工作站的强大计算功能的一种最佳方式。Java Specification Request 172 (JSR-172) 定义了用于 Java 2 Platform Micro Edition (J2ME) 平台的 Web 服务 API。由于移动服务主要从客户端的角度进行编程并且是服务使用者,因此本文只需要介绍一部分远程服务调用 API (JAX-RPC) 和 JAXP (Java API for xml Parsing)。
设计移动 Web 服务的主要目的在于使嵌入式设备能够使用由服务器提供的服务,换句话说,移动 Web 服务是从 Web 服务使用者的角度进行设计的,目的在于支持轻量级设备共享服务器的计算功能和数据库。
移动 Web 服务无缝地集成了运行在不同平台上的两种不同的应用程序,并且提供了它们之间的互操作性。通常,在考虑移动设备的参与时,有三种类型的集成技术可以运用:
与套接字通信和消息传递技术相比,Web 服务有一些突出的优势。Web 服务使用可扩展标记语言 (XML) 来传输消息(包括结构良好的数据信息),使用简单对象访问协议 (SOAP) 来传输对象。如果您使用的是套接字通信,则必须完全负责定义要传输的数据结构。而且,如果客户端和服务器是用不同的编程语言编写的(例如 C++ 和 Java 编程语言),则您的工作量将大大增加——您必须负责数据传输和 C++ 和 Java 编程中的编码细节。
消息传递软件可能是一种解决方案,但如果您所关注的是性能,并且不担心事务和安全级别,则消息传递软件真的不是一个非常好的选择。如果使用消息传递软件,您将花大量的时间和精力解决安全问题,并且您的客户很有可能站在您的门口问:“为什么这么慢?”
我不准备告诉您正确的选择应该是什么,而给出一些理由来说明为什么 Web 服务可能是一个好的选择。
其中的一个理由可能是服务器端编程。即使是像 Web 服务这样好的机制,也仍然由于 XML 处理以及传输和接收 SOAP 消息的开销的原因而不能满足严格的实时处理需求。因此在设计时需要考虑两个问题:
现在,我假定您已经决定使用 Web 服务。那么,在总体设计方面需要考虑哪些问题?
在决定使用 Web 服务之后
您需要考虑的下一个问题是 Web 服务的总体设计。可以运用下列通用设计指导原则:
下面详细介绍这些设计考虑事项。
管理 Web 服务粒度
关于粒度管理需要谨记以下几点:
在本文中,我以 Task Management 系统中的登录功能为例。驱动程序首先登录到系统,如果登录成功,则有两列任务显示在屏幕上。
对于此登录问题有两种解决方案:
对于第一种解决方案,它是细粒度登录系统,而显示任务列表需要两次调用 Web 服务;这可能带来很大的延迟。第二种解决方案是粗粒度登录系统,它返回您在一次调用中需要的所有信息,并确保由于网络延迟和系统 I/O 带来的影响最小。
首先定义 Web 服务接口,然后进行实现。
这不仅从移动 Web 服务的角度来看是适用的,而且从 Web 服务设计和(更高的层次)面向对象的软件设计来看也是适用的。正如您所知道的,接口是客户端和服务提供程序之间的契约,而且保持稳定非常重要(因为正如您所知道的,实现容易改变)。
定义功能接口非常直接和直观——只需考虑:
然后,您应该将该接口的相关要点写下来,并在这些要点的指导下完成所有实现细节。
这是一条简单的设计指导原则。了解您的目标非常重要,目标可以驱动您编写测试用例,并且指导您编写功能实现,这也是为什么测试驱动开发 (TDD) 广泛地应用于各种开发技术的原因。
使用 Document/literal 作为编码样式
目前,JAX-RPC 支持三种操作模式:
在一些资料中,您还可能发现名为 Document/encoded(使用这种模式的人不多)和 Document/literal wrapped(由 Microsoft 定义,但是没有相关规范,其缺点在于比其他模式复杂)的模式。对这些操作模式的详细解释可以在参考资料中的“Which style of WSDL should I use?”内找到。
在这些模式中,WS-I 标准仅支持 RPC/literal 和 Document/literal。对于移动 Web 服务,JAX-RPC 实现必须使用 Document/literal 将基于 Web 服务描述语言 (WSDL) 的服务描述映射到相应的 Java 表示形式。因此,如果您只使用 Document/literal 作为编码样式,则您是最安全的。
优先选择 JavaBeanser 组件而不是 EJB 组件作为服务提供程序
在使用 Java 编程语言公开服务时,需要考虑两种类型的服务提供程序:
虽然在某些情况下,EJB 组件非常有用,但是 JavaBeans 组件常常是更好的选择,特别是在开发移动 Web 服务时。实现使用 JavaBean 组件生成的服务提供程序比较简单和容易,而且与相应的会话 EJB 组件相比,JavaBean 组件更稳定。但是,如果您需要从使用 EJB 组件开发的现有 J2EE 应用程序公开 Web 服务,则请使用 EJB 组件。
避免 XML 元素嵌套太深
如果数组的数组、复杂类型的数组或包含另一个自定义复杂类型的复杂类型等嵌套太深,则将大大影响 Web 服务的性能。清单 1 显示一个 XML 描述示例——一个自定义数据类型 Task
类的数组:
import java.io.Serializable;public class Task implements Serializable { /** * The id of the task */ private int taskID = 0; /** * Owner name of the task */ private String ownerName; /** * public default non-argument constrUCtor * */ public Task() { } /** * Constructor of the Task class * * @param taskID * id of the task * @param ownerName * owner name of the task */ public Task(int taskID, String ownerName) { this.taskID = taskID; this.ownerName = ownerName; } /** * @return Returns the ownerName. */ public String getOwnerName() { return ownerName; } /** * @param ownerName * The ownerName to set. */ public void setOwnerName(String ownerName) { this.ownerName = ownerName; } /** * @return Returns the taskID. */ public int getTaskID() { return taskID; } /** * @param taskID The taskID to set. */ public void setTaskID(int taskID) { this.taskID = taskID; }}
如果一个方法返回如 清单 1 中定义的 Task
的数组,则该方法的源代码包含在下面的清单中,方法 getTasks()
返回一个由五个 Task
对象组成的数组,如清单 2 所示。
public Task[] getTasks(String name){ Task[] tasks = new Task[5]; for(int i=0; i<5; i++){ tasks[i] = new Task(i, name); } return tasks; }
当使用 getTasks()
(如清单 3 所示)所属的 JavaBean 组件公开 Web 服务时,Task
类映射到其中包含 Task
类的名称空间的 tn2:Task
。
<complexType name="Task"> <sequence <element name="ownerName" nillable="true" type="xsd:string"/> <element name="taskID" type="xsd:int"/> </sequence></complexType>
同时,数据类型 Task[]
映射到 ArrayOf_tn2_Task
;ArrayOf_tn2_Task
的 XML 描述如清单 4 所示:
<complexType name="ArrayOf_tns2_Task"> <sequence> <element maxOccurs="unbounded" minOccurs="0" name="Task" nillable="true" type="tns2:Task"/> </sequence> </complexType>
如清单 4 所示,为单个自定义复杂类型数组生成的 XML 描述很长。相反,Java 语言中的单个 String
类型映射到 xsd:string
,而没有生成 complexType
元素;诸如 boolean
、int
和 byte
这样的基元类型分别映射到 xsd:boolean
、xsd:int
和 xsd:byte
。
您可能已经注意到 XML 元素的嵌套(避免嵌套太深)和粒度考虑(使用粗粒度)之间的冲突。在实际运用中,嵌套和粒度之间应该有一个平衡。如果您更关注应用程序的性能,则应该仔细地权衡这两个考虑事项,以获得一个更好的解决方案。
移动 Web 服务的设计考虑事项
我已经讨论了设计 Web 服务的指导原则,现在我将把重点放在移动 Web 服务的考虑事项上。在大多数情况下,当将 JAX-RPC 值类型用于移动 Web 服务时需要考虑一些事情。JAX-RPC 值类型(遵循 JSR-101)是 Java 类,其值可以在服务客户端和服务端点之间移动。为了获得一致的值类型,必须遵循一系列规则。我只列出其中的几条,与本文关系最大的规则是:
您必须具有公共缺省构造器
在反序列化的过程中,SOAP 运行时环境使用缺省构造器来构造对象。如果您试图在没有公共缺省构造器的情况下编写值类型(也称为数据传输对象),在当 JAX-RPC 运行时尝试序列化和反序列化数据对象时可能会遇到错误。对于像 IBM Rational® application Developer (RAD) 6.0 这样的 IDE,将不为该数据类型生成序列化和反序列化 Helper 类(由 RAD 通过前缀 _Helper
、_Ser
和 _Deser
生成),所以在调用与自定义数据类型相关的方法时会出现序列化错误。不带参数的构造器确保可以根据序列化状态远程构造对象。
您必须具有用于网络传输字段的 setter 和 getter 方法
首先,看一看清单 5 中的类 FailTask
的源代码:
public class FailTask { /** * The owner of the task */ private int ownerid; /** * The name of the task */ private String name; /** * Default public non-argument constructor * */ public FailTask(){ } /** * Constructor of FailTask class * @param ownerid Owner of the task * @param name Name of the task */ public FailTask(int ownerid, String name){ this.ownerid = ownerid; this.name = name; } /** * Getter method * @return the ownerid of the task */ public int getOwnerid(){ return ownerid; } /** * Setter method * @param ownerid the ownerid to be set */ public void setOwnerid(int ownerid){ this.ownerid = ownerid; } }
您可以将清单 6 中所示的方法添加到 Web 服务中,该方法将返回单个 FailTask
对象。
public FailTask getFailTask(int ownerid, String name){ return new FailTask(ownerid, name); }
当使用 RAD 6.0 附带的 Universal Test Client 中的 1
和 Rachel
参数调用 getFailTask()
方法时,所得到的响应如图 1 中所示。
name 字段在哪里?它不在这里,因为我没有通过 getter 和 setter 方法提供 name 字段。Setter 和 getter 方法是必须提供的两个方法。和 FailTask_Ser
类中一样,name 字段 getter 方法用于将 name 字段值写入 SOAP 消息。在 FailTask_Deser
类中,name 字段 setter 方法用于设置反序列化的 FailTask
对象的 name 值。
在处理数据集合时您应该使用数组
为了有效地使用 Web 服务,您必须或多或少地使用数据集合。但是,必须提醒:当处理许多值类型时,事情会变得比较麻烦,因此需要考虑以下问题。
当需要动态长度的数组时,请考虑 ArrayList
。您已经反复听说过,如果不考虑同步,则 ArrayList
比 Vector
更有效。但遗憾的是,JSR-101 JAX-RPC 规范没有强制要求支持 Java Collection 类型。有些 Web 服务引擎可能没有为 ArrayList
提供支持。例如,IBM Web 服务引擎只正式支持 Java Collection Framework 中的一小部分类,包括 java.util.Vector
、java.util.HashTable
和 java.util.HashMap
。
那么,尝试一下另一个动态数组 Vector
会如何呢?如果在相同平台上生成存根文件,它将正常工作。但是,如果在不同的平台上生成存根文件,则将遇到一些问题。例如,在 Web 服务描述语言 (WSDL) 文件中,Vector
或其他 Collection 类型映射到 ArrayOfAnyType
。其他平台可能不知道将其映射到哪个 Collection 类型,而且 Vector
中包含的数据元素也映射到 WSDL 中的 AnyType
。(这里存在的一个大问题是,其他的平台不知道 AnyType
代表什么类型。)有关该主题的详细信息,请参阅参考资料中的“ Web services programming tips and tricks: Improve the interOperability between J2EE and .NET ”。
使用数组的最后一个原因是,移动 Web 服务不支持 Java Collection 类型,这使得所有其他的解释都显得没有必要。这意味着您可能无法从形式良好的 WSDL 文件为移动 Web 服务生成存根文件。
移动 Web 服务中的首选的一些数据类型
使用基元类型 long
传输 Date 或 Calendar 表示形式
对于标准 JAX-RPC 运行时实现,有两种支持的标准类型映射:
在 JAX-RPC 子集规范中,只需要第二种映射。表 1 显示了从支持的 XML 数据类型到 Java 类型的映射的简要列表;有关详细信息,请参阅 JSR-172。
xsd:string
java.lang.String
xsd:int
int
xsd:long
long
xsd:short
short
xsd:boolean
boolean
xsd:byte
byte
xsd:float
java.lang.String
或 float
xsd:double
java.lang.String
或 double
xsd:QName
javax.xml.namespace.QName
xsd:base64Binary
byte[]
xsd:hexBinary
byte[]
从表 1 中您可以清楚地看出,该列表中不存在像 xsd:dateTime
、xsd:date
或 xsd:time
这样的元素,而在标准 JAX-RPC 规范中,这三个元素确实是映射到 java.util.Calendar
的 XML 类型。请注意,在 JAX-RPC1.1 中定义的 Java 据类型映射到 XML 类型的映射中,java.util.Date
映射到 xsd:dateTime
。
那么,在尝试传输日期或时间表示形式时,您应该使用什么?改为使用 long
类型的时间。long
类型的日期格式与不同时区的时间表示形式无关,并且因为它是基元类型,所以比其他类型的 Java 对象更有效。
注意 float 和 double 类型的使用
首先需要注意的一点是,正如您所知,CLDC 1.0 (Connected Limited Device Configuration) 并没有出于性能的原因而提供 float 和 double 本机类型,即使 CLDC 1.1 和 CDC 都为其提供了支持。那么,如果您必须使用针对 CLDC 1.0 的 Web 服务,您该如何做呢?JSR-172 为您提供了部分答案。
为了在 CLDC 1.0 中缺省支持 xsd:float
和 xsd:double
,实现必须 生成代码来将这些类型映射到 java.lang.String
。为了支持为 float 和 double 提供本机支持的配置和平台(CLDC 1.1 和 CDC),存根生成器实现也必须 生成代码来将这些类型映射到适当的本机 Java 类型。(详细信息,请参阅参考资料,以获得指向 JSR-172: J2ME Web 服务规范的链接。)
我将演示一个添加两个 float 数的简单 Web 服务(清单 7)。
public class TaskWs { public TaskWs() { } /** * Adding two float numbers and return their sum * @param a First number to add, * @param b Second number to add * @return The sum of a and b. */ public float addTwo(float a, float b) { return a + b; }}}
所生成的 WSDL 定义中的 XML 数据类型定义如清单 8 所示。
<wsdl:types> <schema targetNamespace="http://ws.test.ibm.com" xmlns="http://www.w3.org/2001/XMLSchema" xmlns:impl="http://ws.test.ibm.com" xmlns:intf="http://ws.test.ibm.com" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <element name="addTwoResponse"> <complexType> <sequence> <element name="addTwoReturn" type="xsd:float"/> </sequence> </complexType> </element> <element name="addTwo"> <complexType> <sequence> <element name="a" type="xsd:float"/> <element name="b" type="xsd:float"/> <element name="b" type="xsd:float"/> </sequence> </complexType> </element> </schema> </wsdl:types>
对于针对 CLDC 1.0 的 Web 服务客户端,所生成的存根如清单 9 所示。
public interface TaskWs extends java.rmi.Remote { public java.lang.String addTwo(java.lang.String _a, java.lang.String _b) throws java.rmi.RemoteException, javax.xml.rpc.JAXRPCException;}
所以,当调用 CLDC 1.0 中的 Web 服务时,您必须使用 addTwo()
方法提供两个 String 参数,而对于针对平台 CLDC 1.1 的 Web 服务客户端,所生成的服务接口与清单 10 中所描述的类似:
public interface TaskWs extends java.rmi.Remote { public float addTwo(float _a, float _b) throws java.rmi.RemoteException, javax.xml.rpc.JAXRPCException;}
这将 xsd:float
映射到所生成的客户端存根中的 float 类型。看到 CLDC 1.0 和 CLDC 1.1 之间的不同之处了吗?
在为移动设备开发 Web 服务时,请注意 float 和 double 类型,因为 CLDC 1.0 虚拟机实现无法加载为 CLDC 1.1 生成的存根(使用到 float 和 double 的本机映射)。同时针对 CLDC 1.0 和 CLDC 1.1 的 Java 2 Platform Micro Edition (J2ME) 应用程序的开发人员应该使用到 java.lang.String
的缺省映射,以获得最好可重用性。
在处理输入和输出参数时注意可能出现的问题
JSR-172 指定了以副本的形式传送并以副本的形式创建返回值的所有参数。但是,当处理数据集合时,零数组 (返回零)和空数组 (返回其本身)需要密切关注。
我的建议是尽可能地避免使用空数组。当处理移动 Web 服务时,空数组可能是一个问题。
假定您需要返回任务对象数组。原始代码如清单 11 所示。
public class SimpleTask { /** * The name of the task */ private String name; /** * The default constructor * */ public SimpleTask() { } /** * @return Returns the name of the task. */ public String getName() { return name; } /** * @param name * The name to set. */ public void setName(String name) { this.name = name; }}
Web 服务实现如清单 12 所示。
1 public SimpleTask[] getSimpleTasks(){2 SimpleTask[] tasks = null;3 /*4 * Your code dealing with DB goes here5 * ....6 * tasks = ...7 */8 return tasks;9 }
当生成 Web 服务存根和使用生成的存根测试 Web 服务时,本例中的每一个部分都将正常工作。但是,因为在结束一个阶段之前您需要使用 Jtest 进行完整的代码检查,所以当您对代码片段运行 Jtest 时,您将看到一条建议:“Return zero-length arrays instead of null”。在犹豫片刻之后后,您将赞同 Jtest 的建议。如果您返回零数组,该代码的客户端必须编写额外的代码来检查返回值是否为零(如清单 13 所示)。
SimpleTask[] tasks = service.getSimpleTasks(); if(tasks != null){ int length = tasks.length; //do something here }
当您将 SimpleTask[] tasks = null;
(清单 12 中的第 2 行)修改为 SimpleTask[] tasks = new SimpleTasks[0];
时,您只需将清单 13 编写为:
SimpleTask[] tasks = service.getSimpleTasks(); int length = tasks.length;
在修改之后,您会认为代码逻辑没有更改,并再次运行客户端来调用 Web 服务,但是现在却引发了异常。到目前为止,您已经根据 Jtest 的建议做了许多小的修补——您忘记修改了什么——这可能导致需要花额外的时间来努力找到发生错误的原因。这个过程真的漫长而乏味。
那么,问题究竟出在什么地方呢?一般来说,对于零对象数组(如 SimpleTask
),返回的 SOAP 消息如清单 14 所示。
<soapenv:Body><p147:getSimpleTasksResponse xmlns:p147="http://ws.test.ibm.com"> <getSimpleTasksReturn xsi:nil="true" /></p147:getSimpleTasksResponse></soapenv:Body>
对于空数组(如 SimpleTask[] tasks = new SimpleTask[0]
),SOAP 消息如清单 15 所示。
<soapenv:Body><p147:getSimpleTasksResponse xmlns:p147="http://ws.test.ibm.com"> <getSimpleTasksReturn /> </p147:getSimpleTasksResponse> </soapenv:Body>
其不同之处在于 <getSimpleTasksReturn/>
和 <getSimpleTasksReturn xsi:nil = true>
之间。图 2 说明了空数组参数大部分时间是无效的。对于自定义的数据类型(包括另一个自定类型的数组),不要将类变量初始化为空数组——相反,要将其初始化为零数组,尽管所生成的空数组和零数组的 WSDL 定义是相同的。
结束语
在处理移动 Web 服务时,您需要更加谨慎,因为移动 Web 服务规范只支持部分 API。如果您计划开发移动 Web 服务,则当您处理值类型和集合类型时,我向您介绍了一些窍门。此外,我还提供了以下信息:
参考资料
学习
关于作者
Shu Fang Rui 毕业于中国上海交通大学。她对无线技术和 Web 服务非常感兴趣。除了旅行之外,她还喜欢从事一些运动。
(出处:http://www.VeVb.com)
新闻热点
疑难解答