从独立的 Java 客户端访问 Web 服务

作者:Inderjeet Singh

问题描述

在设计某些 Web 服务时会考虑使用具有丰富功能的客户端用户界面。Internet 聊天 Web 服务就是这样一个示例,它可以使用 GUI 提供警报、视频会议以及其他类似功能。某些客户业务服务也可以从具有丰富功能的用户界面中获益,例如,邮件和日历服务。Java[TM] Swing API 提供了一个功能强大的可移植机制,用于创建这种功能丰富的独立桌面客户端。本解决方案论述了从独立的 Java 客户端访问 Web 服务时要注意的一些问题。

从独立的 Java 客户端访问 Web 服务时,需要考虑下列事项:

解决方案

下表概述了通过独立的客户端访问 Web 服务的建议。有关这些建议的详细信息,请参见下文。

问题
建议
选择通信模式 使用桩模块。使用附带 -f:unwrap 的展开参数在 WS-I Basic Profile 兼容模式(使用 -f:wsi w)下从 WSDL 生成服务接口。
提供 JAXRPC 运行环境 包含来自应用服务器的 JAXRPC 实现 jar。
处理错误 需要用户针对系统异常执行重试操作或选择备用服务,以及针对特定于服务的异常执行更正操作。
将应用程序逻辑与 Web 服务工件相分离 使用委托来封装所有的 Web 服务调用以及在 Web 服务中使用的参数。
避免由于 Web 服务开销而延迟创建响应性 GUI 对 Web 服务执行粗糙粒度调用,缓存接收数据并将其用于本地访问。
自动部署客户端 将 Java Web Start 与独立的客户端一起使用,以便从 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 生成类。但是,它仅需要访问与服务端点接口相关的类,如 StringPurchaseOrderServiceSEISubmitPOSubmitPOResponse

使用动态调用接口 (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 运行环境

必须随应用程序一起提供 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)以保持数据的同步。

使用 Java Web Start 进行部署

基于浏览器的客户端比桌面客户端更受青睐的一个主要原因是:您可以通过任何一台连接到 Internet 的机器访问这些客户端,事先无需进行安装。可通过 Java 平台提供的 Java Web Start 技术管理独立的 Java 客户端的部署。但是,对 Web 服务客户端使用 Java Web Start 技术会涉及下列问题:

参考资料

有关本主题的详细信息,请参阅以下资料:


© Sun Microsystems 2005。Java BluePrints Solutions Catalog 中的所有内容受版权保护,未经 Sun Microsystems 的明确书面许可,不得在其他产品中发布。