从独立的 Java 客户端访问 Web 服务
作者:Inderjeet Singh
问题描述
在设计某些 Web 服务时会考虑使用具有丰富功能的客户端用户界面。Internet 聊天 Web 服务就是这样一个示例,它可以使用 GUI 提供警报、视频会议以及其他类似功能。某些客户业务服务也可以从具有丰富功能的用户界面中获益,例如,邮件和日历服务。Java[TM] Swing API 提供了一个功能强大的可移植机制,用于创建这种功能丰富的独立桌面客户端。本解决方案论述了从独立的 Java 客户端访问 Web 服务时要注意的一些问题。
从独立的 Java 客户端访问 Web 服务时,需要考虑下列事项:
- 独立的客户端不像 J2EE[TM] 组件那样在管理环境中运行。J2EE 组件(如 Servlet 或 Enterprise Bean)由其容器进行管理,该容器负责为其提供访问 Web 服务的运行时机制(桩模块或动态代理)。该组件以声明方式指定其要使用的 Web 服务,而容器负责在需要时实例化和配置要访问服务的桩模块(或任何其他机制)。独立的客户端不在管理环境内运行,因此必须亲自管理对 Web 服务的访问。这通常包含实例化和配置 WSDL 中指定的要访问 Web 服务的桩模块或其他所需的类。出于安全考虑,独立的客户端还可能需要将桩模块配置为使用 HTTPS 和基本认证。
- 在 Java 中,Web 服务是使用“基于 XML 的远程过程调用的 Java API”(JAX-RPC) API 访问的。由于 JAX-RPC 技术还不是标准 Java 运行环境 (JRE) 的一部分,因此只有具有 JAX-RPC 实现类的客户端才能使用 JAX-RPC API。对于 J2EE 组件,JAX-RPC 运行环境是通过容器来提供的,因为 J2EE 1.4 包含 JAX-RPC 技术。但是,只有当 Swing 客户端的类路径中具有 JAX-RPC 实现类时,它才能使用 JAX-RPC API。
- 独立的 JAX-RPC 客户端可以使用以下任意三种 JAX-RPC 通信模式:桩模块、动态代理和 DII。每种模式都会对 WSDL 的易于开发性、可移植性以及可用性产生各自的影响。开发者必须决定哪种通信模式最适合他们的需要。
- 从 WSDL 生成的服务端点接口可能与 JAX-RPC 编译器用于生成 WSDL 的接口不同。这是因为 XML 结构中的类型与 Java 语言中的类型并非一一映射。例如,
xsd:date
和 xsd:dateTime
都映射到了 java.util.Calendar
。生成的服务接口还取决于所用的编码样式。例如,如果 Web 服务使用 document-literal 编码,则生成的 Java 接口将包含每个类的封装版本。这意味着即使某方法参数为简单的 String
类型,它也会与某个包含单一 String
类型属性的对象封装在一起。使用生成接口的开发者可能不喜欢出现这种情况,因为接口会变得很复杂。
- 更高级的设计目标应该是减少对服务 WSDL 的依赖性。这一点非常有用,因为随着服务的发展,客户端代码受到的影响会越来越小。
解决方案
下表概述了通过独立的客户端访问 Web 服务的建议。有关这些建议的详细信息,请参见下文。
现在让我们详细了解一下上述每个问题,首先从第一个问题选择通信模式开始。
选择通信模式
最适合独立客户端的通信模式是桩模块,因为与 DII 或动态代理相比,桩模块最易于使用。有关这些方法的比较,请参见《Designing Web Services with the J2EE(TM) 1.4 Platform: JAX-RPC, SOAP, and XML Technologies》一书的 5.3.1 节和 5.3.2 节。
使用桩模块
桩模块是在服务 WSDL 上运行 JAX-RPC 编译器而生成的。生成桩模块时,请确保将 JAX-RPC 编译器配置为使用与 WS-I Basic Profile 兼容的 document-literal 编码(通过使用 -f:wsi
开关实现),同时设置为展开参数(通过使用 -f:unwrap
开关实现)。也可在 Ant 目标中完成此操作。例如,代码示例 1 从 WSDL 文件生成桩模块类:
<taskdef
name="wscompile"
classname="com.sun.xml.rpc.tools.ant.Wscompile">
<classpath
refid="jaxrpc.classpath"/>
</taskdef>
<wscompile gen="true"
base="${output.classes.dir}" features="explicitcontext,wsi,unwrap"
keep="true" debug="true"
config="${jaxrpc.client.config.dir}/client-config.xml">
<classpath
refid="jaxrpc.classpath"/>
</wscompile>
代码示例 1:用于生成桩模块类的 Ant 任务
在以上示例中,client-config.xml
文件包含一个指向 WSDL 文件位置的指针。另请注意,keep
属性被设置为 true
以确保保留生成的桩模块类的 Java 源代码。此源代码是必需的,因为只有通过它,客户端开发者才能了解生成的桩模块接口是什么,以及应该如何使用该接口。然后便可使用生成的 Java 源代码了解桩模块以及与之相关联的类。客户端开发者既可以直接使用此源代码,也可以从中生成 Javadoc[TM] 代码,以了解如何对服务进行调用。
独立的客户端会配置生成的桩模块类并对其进行实例化以便访问 Web 服务。代码示例 2 说明了如何使用桩模块访问 Web 服务:
StringPurchaseOrderService_Impl svc = new
StringPurchaseOrderService_Impl();
StringPurchaseOrderServiceSEI poservice =
svc.getStringPurchaseOrderServiceSEIPort();
((Stub)poservice)._setProperty(Stub.ENDPOINT_ADDRESS_PROPERTY,
"http://localhost:8080/webservice/StringPurchaseOrderService");
String purchaseOrder = ... // This is the string
representation
of a purchase order in XML
String result = poservice.submitPO(purchaseOrder);
代码示例 2:将桩模块与独立的客户端一起使用
请注意,StringPurchaseOrderService_Impl
不能在不同的 JAX-RPC 运行环境之间进行移植,但建议使用该选项,因为与其他任何可移植的备选项相比,它更易于使用。这与 J2EE 客户端的情况不同,因为在 J2EE 客户端中可以编写完全可移植的代码。同时还需要指出的是,从 Java 平台的角度来看,独立的客户端本身是可移植的:它不需要进行任何更改就可以在各种硬件平台和操作系统上使用。它只要求使用相同的 JAX-RPC 运行环境,该环境必须与应用程序捆绑在一起,因为 JAX-RPC 还不是 Java SE 平台的一部分。桩模块类的可移植性信息将成为 JAX-RPC 2.0 规范的内容之一,此规范会在 Java EE 5 平台中使用。
通常,客户端不会对其使用的 Web 服务 URL 进行硬编码。客户端可能选择在运行时或者基于用户输入的内容,选择服务 URL。可通过将 Stub.ENDPOINT_ADDRESS_PROPERTY
设置为运行时所需的服务 URL,利用桩模块完成此操作,如以上示例所示。
使用动态代理
当桩模块因其不可移植性而无法使用时,应使用动态代理。当运行时可用的 JAX-RPC 实现与编译时的 JAX-RPC 实现不同时,可能会发生这种情况。下面的代码示例 3 说明了如何将动态代理与独立的客户端一起使用:
ServiceFactory
sf = ServiceFactory.newInstance();
URL wsdlURL = new URL("
http://localhost:8080/webservice/StringPurchaseOrderService
?WSDL");
QName serviceQname = new
QName("urn:StringPurchaseOrderService", "StringPurchaseOrderService");
Service s = sf.createService(wsdlURL,
serviceQname);
QName portQname = new
QName("urn:StringPurchaseOrderService",
"StringPurchaseOrderServiceSEIPort");
StringPurchaseOrderServiceSEI poservice =
(StringPurchaseOrderServiceSEI) s.getPort(portQname,
StringPurchaseOrderServiceSEI.class);
String purchaseOrder = ... // This is
the string representation of a purchase order in XML
SubmitPO param = new SubmitPO(purchaseOrder);
SubmitPOResponse response = poservice.submitPO(param);
String result = response.getResult();
代码示例 3:将动态代理与独立的客户端一起使用
请注意,动态代理需要使用封装版本的服务端点接口,而不是更方便的未封装版本的服务端点接口。这是因为 Web 服务使用的是与 WS-I Basic Profile 兼容的 document
,而不是 rpc
样式的 SOAP 绑定。document
样式的绑定将映射到封装版本的接口中,而 wscompile 添加了一种便捷的方法通过桩模块类来访问未封装版本的接口。但是,动态代理不使用桩模块类,因此必须使用封装版本的接口。在调用 wscompile 从 WSDL 生成类时,可通过省略 unwrap
开关生成封装版本的服务端点接口。请注意,动态代理代码仍要求从 WSDL 生成类。但是,它仅需要访问与服务端点接口相关的类,如 StringPurchaseOrderServiceSEI
、SubmitPO
和 SubmitPOResponse
。
使用动态调用接口 (DII)
使用 DII 的情况很少见。当客户端不能访问 WSDL 但有时却知道要调用的方法时,可使用 DII。下面的代码示例 4 说明了如何将 DII 与独立的客户端一起使用:
ServiceFactory
sf = ServiceFactory.newInstance();
URL wsdlURL = new URL("
http://localhost:8080/webservice/StringPurchaseOrderService
?WSDL");
QName serviceQname = new QName("
urn:StringPurchaseOrderService",
"StringPurchaseOrderService");
Service s = sf.createService(wsdlURL, serviceQname);
QName portQname = new
QName("
urn:StringPurchaseOrderService",
"StringPurchaseOrderServiceSEIPort");
Call call = s.createCall(portQname);
call.setTargetEndpointAddress(serviceUrl);
call.setProperty(Call.SOAPACTION_USE_PROPERTY, new
Boolean(true));
call.setProperty(Call.SOAPACTION_URI_PROPERTY,"");
// Note that the operation name need not be set by
calling
// call.setOperationName(new QName(NS_BODY,
"submitPO"));
// This is because the SOAP binding used by the Web
service is document, not rpc.
// For WS-I compliant document-literal, need to set
the encoding style
// to literal by specifying "" as the encoding, and
the operation style to document
call.setProperty("javax.xml.rpc.encodingstyle.namespace.uri", "");
call.setProperty(Call.OPERATION_STYLE_PROPERTY,
"document");
// The types for the request parameter
and return value are defined in the
// WSDL file itself, so their qnames are defined
with the namespace of the body
QName requestQname = new
QName("
urn:StringPurchaseOrderService", "submitPO");
QName responseQname = new
QName("urn:StringPurchaseOrderService",
"submitPOResponse");
// Define the type of the return value for the DII
call.
// SubmitPOResponse must match the wrapped type sent
by the Web service.
call.setReturnType(responseQname,
SubmitPOResponse.class);
// Define the type of the method parameter for the
DII call.
// In the WSDL file, the name of the message part
for submitPO is "parameters"
// Hence the request parameter is defined in this
way.
call.addParameter("parameters", requestQname,
SubmitPO.class, ParameterMode.IN);
String purchaseOrder = ... // This is
the string representation of a purchase order in XML
SubmitPO param = new SubmitPO(purchaseOrder);
Object[] params = {param};
// Invoke the DII call
SubmitPOResponse response = (SubmitPOResponse)
call.invoke(params);
String result = response.getResult();
代码示例 4:将 DII 与独立的客户端一起使用
如以上示例所示,使用 DII 会非常复杂,因此应该只在异常情况下才使用 DII。J2EE 1.4 SDK 提供的 JAX-RPC 实现还包含一个错误,当 Web 服务方法的参数类型为 string
时,将无法正确生成封装类。开发者必须使用一个将成员变量重命名为 String_1
而不是 string_1
的等效类来覆盖生成的封装类 SubmitPO,从而修正此错误。
必须随应用程序一起提供 JAX-RPC 运行环境。这可以通过从应用服务器安装中复制必要的类来实现。执行此捆绑时,请注意再分发应用程序时的许可默示。代码示例 5 中提供的 ant 生成文件片段说明了如何使用 J2EE SDK 完成此操作。
<target
name="copy-jaxrpc-runtime" depends="init">
<unjar
src="${j2ee.home}/lib/j2ee.jar" dest="${build.classes.dir}"/>
<unjar
src="${j2ee.home}/lib/jaxrpc-api.jar"
dest="${build.classes.dir}"/>
<unjar
src="${j2ee.home}/lib/jaxrpc-impl.jar"
dest="${build.classes.dir}"/>
<unjar
src="${j2ee.home}/lib/saaj-impl.jar"
dest="${build.classes.dir}"/>
<unjar
src="${j2ee.home}/lib/mail.jar" dest="${build.classes.dir}"/>
<unjar
src="${j2ee.home}/lib/dom.jar" dest="${build.classes.dir}"/>
<unjar
src="${j2ee.home}/lib/xercesImpl.jar"
dest="${build.classes.dir}"/>
</target>
代码示例 5:复制 JAXRPC 运行环境的 Ant 任务
处理错误
Web 服务客户端必须处理两类错误:系统异常和特定于服务的异常。如果出现不可恢复的系统错误(如网络连接故障,或者 Web 服务关闭),则会发生系统异常。JAX-RPC 客户端将系统异常作为 RemoteException
来接收。特定于服务的异常是由 Web 服务抛出的,表示应用程序逻辑中有错误,例如传递了不正确的参数,或者尝试在数据库中创建重复的键。JAX-RPC 客户端将特定于服务的异常作为应用程序的异常类来接收。这些异常类是由 JAX-RPC 编译器根据 WSDL 中指定的故障生成的。
对独立的客户端的异常处理通常比 Servlet 或 Enterprise Bean 进行 Web 服务调用的情况要简单。因为独立的客户端通常在其终端拥有一位能够处理错误情况的用户。这样,简单的异常处理策略可能需要将异常情况转换为用户能够理解的消息,然后提示用户重试操作或执行更正操作。通常可通过确保在调用 Web 服务之前验证参数,来避免或减少特定于服务的异常。
将应用程序逻辑与 Web 服务工件分离
在出色的设计中,Web 服务客户端的应用程序逻辑对服务 WSDL 依赖性会降到最低。这一点非常有用,因为随着服务的发展,客户端代码受到的影响会越来越小。通过创建委托类,使其封装使用桩模块及其生成类的代码,便可实现此分离。委托的公共 API 不会公开任何桩模块类。而是将这些类映射到其余应用程序所理解的类中。
本地缓存数据
典型的 Swing 客户端提供一个功能丰富的用户界面,可以很好地响应用户。Swing 客户端可选择在本地而非网络上缓存数据,以提高用户界面的响应能力。但是,这样做会产生维护数据一致性的问题。客户端和 Web 服务都需要被设计为使用协议(如 SyncML)以保持数据的同步。
基于浏览器的客户端比桌面客户端更受青睐的一个主要原因是:您可以通过任何一台连接到 Internet 的机器访问这些客户端,事先无需进行安装。可通过 Java 平台提供的 Java Web Start 技术管理独立的 Java 客户端的部署。但是,对 Web 服务客户端使用 Java Web Start 技术会涉及下列问题:
- 必须编写 JNLP 文件,并且该文件在 Web 站点上可以与独立的客户端应用程序的 jar 文件一起使用。提供 Web 服务的 Web 站点可能是提供这些文件的很好场所。由于客户端应用程序需要了解所需连接的 Web 服务 URL,因此最好动态地生成 JNLP 文件(例如通过 JSP 文件)。
- 客户端应用程序在 Java Web Start 提供的沙盒中运行。这意味着访问本地文件系统或打开网络连接等功能受到限制。通过对应用程序使用的所有 Jar 文件进行签名,可以解决此问题。
- 访问 Web 服务的独立的 Java 客户端需要使用 JAXRPC 实现来调用 Web 服务。提供 JAXRPC 运行环境的一种方法是在 JNLP 文件中指出必需的 jar 文件,以便 Java Web Start 可根据需要下载这些文件。但是,除非对所有 jar 文件进行签名,否则无法在沙盒中正确运行 JAXRPC 实现。处理此问题的另一种方法是要求用户在其计算机上安装 JAXRPC 运行环境。
参考资料
有关本主题的详细信息,请参阅以下资料:
© Sun Microsystems 2005。Java BluePrints Solutions Catalog 中的所有内容受版权保护,未经 Sun Microsystems 的明确书面许可,不得在其他产品中发布。