UML软件工程组织

技巧:用 JAX-RPC 发送与接收 SOAP 消息
Russell Butek (butek@us.ibm.com)
开发人员,IBM
2003 年 10 月
在本技巧中,IBM 开发人员 Russell Butek 向我们介绍了 JAX-RPC,这是一种 Java API,有了它,应用程序不需要理解 SOAP 消息传递协议的细节,就可以与 Web 服务通信。

Web 服务的基础之一是互操作性。意思是说 Web 服务相互之间用一种标准的格式发送和接收消息。典型的格式就是 SOAP。发送 SOAP 消息的方法有很多。

最基本的操作就是对一个 URL 流执行 println,不过这要求您理解太多有关 SOAP 协议的知识,并且要知道某个服务期望收到的 SOAP 消息是什么样,发出的又是什么样。我们无法很方便地从服务本身获得这些信息,因此这种方法的可扩展性并不好。

再进一步就是使用 SAAJ API(SOAP with Attachments API for Java),这种技术在 developerWorks 前一篇技巧 中介绍过了。SAAJ API 使您能够在稍微抽象一点的层次上操纵 SOAP 结构,但是依然存在一些对 URL 流执行 println 的问题。

一种更加友好的方法是使用一种系统,这种系统能够将应用程序推到一个比 SOAP 消息传递协议抽象得多的层次上。这种系统可以带来以下好处:

  • 应用程序编程人员可以集中精力编写他们的应用逻辑,而不再被 SOAP 的严密逻辑困扰。
  • 可以使用 SOAP 以外的其他消息传递模式,而应用程序代码只需做微小修改。

基于 XML 的 Java RPC API (Java APIs for XML-based RPC,JAX-RPC) 正是这样一种抽象的方法。

JAX-RPC 以及 Barnes & Noble Web 服务
JAX-RPC 不依赖于 SOAP,而是依赖于 Web 服务描述语言(Web Services Description Language,WSDL)。WSDL 以声明性的标准化方式定义了访问 Web 服务的 API。WSDL 可以定义为将 SOAP 绑定到服务上,但是也可以在高于 SOAP 消息的层次上定义一种更加抽象的描述。(有关 WSDL 的更多信息,请参阅 参考资料)。

前面提到过一篇有关 SAAJ 的文章,其中的例子使用了位于 www.xmethods.net 的 Barnes & Noble Web 服务。XMethods 网站还提供了这个 Web 服务的 WSDL(请参阅 参考资料),请参见清单 1。本文的例子中使用到了这个 WSDL。

清单 1. XMethods 上的 Barnes & Noble WSDL
<?xml version="1.0" ?>
<definitions name="BNQuoteService"
    targetNamespace="http://www.xmethods.net/sd/BNQuoteService.wsdl"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
    xmlns="http://schemas.xmlsoap.org/wsdl/"
    xmlns:tns="http://www.xmethods.net/sd/BNQuoteService.wsdl">
  <message name="getPriceRequest">
    <part name="isbn" type="xsd:string" /> 
  </message>
  <message name="getPriceResponse">
    <part name="return" type="xsd:float" /> 
  </message>
  <portType name="BNQuotePortType">
    <operation name="getPrice">
      <input message="tns:getPriceRequest" name="getPrice" /> 
      <output message="tns:getPriceResponse" name="getPriceResponse" /> 
    </operation>
  </portType>
  <binding name="BNQuoteBinding" type="tns:BNQuotePortType">
    <soap:binding style="rpc"
        transport="http://schemas.xmlsoap.org/soap/http" />
    <operation name="getPrice">
      <soap:operation soapAction="" />
      <input name="getPrice">
        <soap:body
            use="encoded"
            namespace="urn:xmethods-BNPriceCheck"
            encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
      </input>
      <output name="getPriceResponse">
        <soap:body
            use="encoded"
            namespace="urn:xmethods-BNPriceCheck"
            encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
      </output>
    </operation>
  </binding>
  <service name="BNQuoteService">
    <documentation>Returns price of a book at BN.com given an
        ISBN number</documentation>
    <port name="BNQuotePort" binding="tns:BNQuoteBinding">
      <soap:address location=
          "http://services.xmethods.net:80/soap/servlet/rpcrouter" />
    </port>
  </service>
</definitions>

在任何 JAX-RPC 应用程序中,您都必须完成以下三件事情:

  1. 实例化一个 Service 类,这个类在客户端代表了一个 Web 服务。
  2. 在 Web 服务的应用程序中实例化一个代理(可能还要设置该代理)。
  3. 调用实现 Web 服务的应用程序中的操作。

JAX-RPC DII 客户机应用程序
SAAJ API 并不依赖于 WSDL,它只依赖于 SOAP。但是您必须确切地知道这项服务需要的 SOAP 消息是什么样子。JAX-RPC 定义了一种动态调用接口(Dynamic Invocation Interface,DII) API,用于访问 Web 服务,严格地讲,这些 Web 服务也不依赖于 WSDL(不过您还是必须了解从 WSDL 中获取的一些信息)。然而,您不再需要了解 SOAP 消息的详细内容。DII JAX-RPC 应用程序包括三个特定的步骤:

  1. 实例化一个没有 WSDL 的 DII Service 类。
  2. 实例化一个 DII Call对象代理,并对其进行设置。
  3. 调用 Call 对象的 invoke 方法。

实例化一个没有 WSDL 的 DII Service 类
这个类不知道 WSDL,但是它必须有一个名字。因此我们使用 WSDL 中定义的服务名。

清单 2. 实例化 DII Service
import javax.xml.namespace.QName;
import javax.xml.rpc.Service;
import javax.xml.rpc.ServiceFactory;
public class DIITip {
    public static void main(String args[]) {
        try {
            // Create a service class with no WSDL information.  You
            // still must know something about the WSDL, however: the
            // service's name.
            QName serviceName = new QName(
                    "http://www.xmethods.net/sd/BNQuoteService.wsdl",
                    "BNQuoteService");
            ServiceFactory factory = ServiceFactory.newInstance();
            Service service = factory.createService(serviceName);
        }
        catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

实例化一个 DII Call 对象代理并对其进行设置
DII 代理对象实现 javax.xml.rpc.Call 接口。您可以从刚刚创建的那个服务中获得对 Call 接口的调用。

清单 3. 实例化 Call 对象

import javax.xml.namespace.QName;
import javax.xml.rpc.Call;
import javax.xml.rpc.Service;
import javax.xml.rpc.ServiceFactory;
public class DIITip {
    public static void main(String args[]) {
        try {
            // Create a service class with no WSDL information.  You
            // still must know something about the WSDL, however: the
            // service's name.
            QName serviceName = new QName(
                    "http://www.xmethods.net/sd/BNQuoteService.wsdl",
                    "BNQuoteService");
            ServiceFactory factory = ServiceFactory.newInstance();
            Service service = factory.createService(serviceName);
           // Now create a dynamic Call object from this service.
           // This call object is not yet associated with any
           // operation.  We'll do that below.
           Call call = service.createCall();
        }
        catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

您现在拥有的这个 Call 对象还不可以用。它并不知道有关操作的任何事情。您需要将以下数据提供给这个对象:

  • 操作名称:getPrice
  • 输入参数:一个字符串
  • 返回值类型:一个浮点数
  • 绑定信息:rpc 风格;编码风格
  • 访问点:http://services.xmethods.net:80/soap/servlet/rpcrouter
清单 4. 填充 Call 对象
import javax.xml.namespace.QName;
import javax.xml.rpc.Call;
import javax.xml.rpc.ParameterMode;
import javax.xml.rpc.Service;
import javax.xml.rpc.ServiceFactory;
import javax.xml.rpc.encoding.XMLType;
public class DIITip {
    public static void main(String args[]) {
        try {
            // Create a service class with no WSDL information.  You
            // still must know something about the WSDL, however: the
            // service's name.
            QName serviceName = new QName(
                    "http://www.xmethods.net/sd/BNQuoteService.wsdl",
                    "BNQuoteService");
            ServiceFactory factory = ServiceFactory.newInstance();
            Service service = factory.createService(serviceName);
            // Now create a dynamic Call object from this service.
            // This call object is not yet associated with any
            // operation.  We'll do that below.
            Call call = service.createCall();
          // Next, build up the information about the operation...
          // The operation name
          QName operationName = new QName(
                  "urn:xmethods-BNPriceCheck",
                  "getPrice");
                  call.setOperationName(operationName);
                  // The input parameter
                  call.addParameter(
                       "isbn",             // parameter name
                       XMLType.XSD_STRING, // parameter XML type QName
                       String.class,       // parameter Java type class
                       ParameterMode.IN);  // parameter mode
                       // The return
                       call.setReturnType(XMLType.XSD_FLOAT);
                       // The operation is an RPC-style operation.
                       call.setProperty(
            Call.OPERATION_STYLE_PROPERTY,
                       "rpc");
               // The encoding style property value comes from the
               // binding's operation's input clauses encodingStyle
               // attribute.  Note that, in this case, this call is not
               // really necessary - the value we're setting this
               // property to is the default.
               call.setProperty(
               Call.ENCODINGSTYLE_URI_PROPERTY,
                       "http://schemas.xmlsoap.org/soap/encoding/");
               // The target endpoint
               call.setTargetEndpointAddress(
               "http://services.xmethods.net:80/soap/servlet/rpcrouter");
        }
        catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

调用 Call 对象的 invoke 方法
最后,您就可以调用 getPrice 操作了。

清单 5. 调用 getPrice 操作
import javax.xml.namespace.QName;
import javax.xml.rpc.Call;
import javax.xml.rpc.ParameterMode;
import javax.xml.rpc.Service;
import javax.xml.rpc.ServiceFactory;
import javax.xml.rpc.encoding.XMLType;
public class DIITip {
    public static void main(String args[]) {
        try {
            // Create a service class with no WSDL information.  You
            // still must know something about the WSDL, however: the
            // service's name.
            QName serviceName = new QName(
                    "http://www.xmethods.net/sd/BNQuoteService.wsdl",
                    "BNQuoteService");
            ServiceFactory factory = ServiceFactory.newInstance();
            Service service = factory.createService(serviceName);
            // Now create a dynamic Call object from this service.
            // This call object is not yet associated with any
            // operation.  We'll do that below.
            Call call = service.createCall();
            // Next, build up the information about the operation...
            // The operation name
            QName operationName = new QName(
                    "urn:xmethods-BNPriceCheck",
                    "getPrice");
            call.setOperationName(operationName);
            // The input parameter
            call.addParameter(
                    "isbn",             // parameter name
                    XMLType.XSD_STRING, // parameter XML type QName
                    String.class,       // parameter Java type class
                    ParameterMode.IN);  // parameter mode
            // The return
            call.setReturnType(XMLType.XSD_FLOAT);
            // The operation is an RPC-style operation.
            call.setProperty(
                    Call.OPERATION_STYLE_PROPERTY,
                    "rpc");
            // The encoding style property value comes from the
            // binding's operation's input clauses encodingStyle
            // attribute.  Note that, in this case, this call is not
            // really necessary - the value we're setting this
            // property to is the default.
            call.setProperty(
                    Call.ENCODINGSTYLE_URI_PROPERTY,
                    "http://schemas.xmlsoap.org/soap/encoding/");
            // The target endpoint
            call.setTargetEndpointAddress(
            "http://services.xmethods.net:80/soap/servlet/rpcrouter");
           // Invoke the operation
              Object[] actualArgs = {"0672324229"};
              Float price = (Float) call.invoke(actualArgs);
              System.out.println("price = " + price);
        }
        catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

然后您运行 java DIITip,就得到价格:44.99

如果您将这段代码与 SAAJ 那篇文章中的进行比较,就可以看出,您的知识已经从 SOAP 协议向前迈进了一大步。您需要的信息中与 SOAP 有关的只有操作风格和编码风格。

正如其名字所暗示的,DII 编程模型是设计用于处理动态服务的。如果服务发生了微小改变,客户机的代码不需要修改太多就能与之相匹配,如果您真的很聪明的话,客户机代码也可以做成动态的,这样就根本不用修改了。

这种模型仍然是相当复杂的,尤其是您必须填充 Call 对象。操作越复杂,您需要提供给 Call 对象的信息也就越复杂。因此如果您知道服务是静态的,并且不会变化,那么您就可以在抽象的食物链上再上升一层。

JAX-RPC SEI 客户机应用程序
接下来的一层抽象需要更多的设置工作,但是最终客户机的代码可就简单得多了。您可以从 WSDL 开始入手。JAX-RPC 实现提供了将 WSDL 转换成 Java 的代码生成器,它除了别的东西之外,还可以生成服务端点接口(Service Endpoint Interface,SEI)。这是客户端访问 Web 服务应用程序的 Java 接口。清单 6 中显示了 XMethods Barnes & Noble 应用程序的一个 SEI。

清单 6. Barnes & Noble SEI
package xmethods.bn;
public interface BNQuotePortType extends java.rmi.Remote {
    public float getPrice(java.lang.String isbn)
            throws java.rmi.RemoteException;
}

在我所使用的 WSDL 转换成 Java 的工具中,我将 http://www.xmethods.net/sd/BNQuoteService.wsdl targetNamespace 映射为 Java 包 xmethods.bn,这样 SEI 就创建在这个包中。SEI 的名字派生于 WSDL 的 portType 名称:BNQuotePortTypegetPrice 方法派生于 portTypegetPrice 操作以及它所引用的消息及其类型。如果您运行的是自己喜欢的 WSDL 转换成 Java 的工具,您将看到 xmethods.bn 包中还生成了其他的类。您必须将这些类一起编译,但是您的客户机应用程序只需要知道 SEI。

为了用这个 SEI 来调用 Web 服务,您必须完成下面三个与 DII 相同的步骤:

  1. 用 WSDL 实例化一个 Service
  2. 实例化一个代理
  3. 调用代理的操作

SEI 和 DII 的第 1步是类似的。第 2步和第 3步则大大简化,尤其是第 2步。

用 WSDL 实例化一个 Service
SEI 模型和 DII 模型在这一步的唯一区别就是,在 SEI 中您需要多提供一些信息,即 WSDL。

清单 7. 为 SEI 实例化一个 Service

import java.net.URL;
import javax.xml.namespace.QName;
import javax.xml.rpc.Service;
import javax.xml.rpc.ServiceFactory;
public class SEITip {
    public static void main(String args[]) {
        try {
            // Create a service class with WSDL information.
            QName serviceName = new QName(
                    "http://www.xmethods.net/sd/BNQuoteService.wsdl",
                    "BNQuoteService");
            URL wsdlLocation = new URL
              ("http://www.xmethods.net/sd/2001/BNQuoteService.wsdl");
            ServiceFactory factory = ServiceFactory.newInstance();
            Service service = factory.createService(
                    wsdlLocation,
                    serviceName);
        }
        catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

实例化一个代理
一旦您获得了这个服务,就必须找到这个 SEI 的一个实现。这个实现就是用来访问真正应用程序的代理。所有有关如何访问那个应用程序的信息都隐藏在这个实现中,而这些信息是从 WSDL 的服务端口中搜集的,因此只要您获得了这个代理,您就不需要进行任何设置了;所有的工作都已经替您做好了。

清单 8. 实例化 SEI 的一个实现

import java.net.URL;
import javax.xml.namespace.QName;
import javax.xml.rpc.Service;
import javax.xml.rpc.ServiceFactory;
import xmethods.bn.BNQuotePortType;
public class SEITip {
    public static void main(String args[]) {
        try {
            // Create a service class with WSDL information.
            QName serviceName = new QName(
                    "http://www.xmethods.net/sd/BNQuoteService.wsdl",
                    "BNQuoteService");
            URL wsdlLocation = new URL
              ("http://www.xmethods.net/sd/2001/BNQuoteService.wsdl");
            ServiceFactory factory = ServiceFactory.newInstance();
            Service service = factory.createService(
                    wsdlLocation,
                    serviceName);
           // Get an implementation for the SEI for the given port
           QName portName = new QName("", "BNQuotePort");
           BNQuotePortType quote = (BNQuotePortType) service.getPort(
                      portName,
                      BNQuotePortType.class);
        }
        catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

调用代理的操作
最后,对操作的调用过程可能不太简单:

清单 9. 调用 SEI 上的一个操作

import java.net.URL;
import javax.xml.namespace.QName;
import javax.xml.rpc.Service;
import javax.xml.rpc.ServiceFactory;
import xmethods.bn.BNQuotePortType;
public class SEITip {
    public static void main(String args[]) {
        try {
            // Create a service class with WSDL information.
            QName serviceName = new QName(
                    "http://www.xmethods.net/sd/BNQuoteService.wsdl",
                    "BNQuoteService");
            URL wsdlLocation = new URL
              ("http://www.xmethods.net/sd/2001/BNQuoteService.wsdl");
            ServiceFactory factory = ServiceFactory.newInstance();
            Service service = factory.createService(
                    wsdlLocation,
                    serviceName);
            // Get an implementation for the SEI for the given port
            QName portName = new QName("", "BNQuotePort");
            BNQuotePortType quote = (BNQuotePortType) service.getPort(
                    portName,
                    BNQuotePortType.class);
           // Invoke the operation
              float price = quote.getPrice("0672324229");
              System.out.println("price = " + price);
        }
        catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

结束语
在调用 Web 服务的多种方法之中,最基本的方法是手工生成并发送 SOAP 消息。为此您必须掌握非常多的有关这项服务和 SOAP 协议的知识,因此这种方法不是非常有用。提高一步就是使用 SAAJ API。您仍然需要知道很多服务和 SOAP 协议的相关知识,所以这种方法也不是特别有用。从本文中再提高一步就是使用 JAX-RPC DII。这种方法使您摆脱了 SOAP,但是使用的代码还是很复杂。最好的方法就是根据服务的 WSDL 生成 SEI,并调用 SEI 代理。

参考资料

关于作者
Russell Butek 是 IBM WebSphere Web 服务引擎的一名开发人员。他还是 IBM 在 JAX-RPC Java Specification Request(JSR)专家组中的代表。他参与实现了 Apache 的 AXIS SOAP 引擎,并促使 AXIS 1.0 遵从 JAX-RPC 1.0规范。在此之前,他是 IBM CORBA ORB 开发人员和 IBM 在许多 OMG 工作小组的代表,包括:可移植拦截器(portable interceptor)工作小组(任主席)、核心工作小组、以及互操作性工作小组。



 

版权所有:UML软件工程组织