网络服务程序接口Web services APIs
我们将集中描述一种用于编写Web service客户端的逐步式方法,该方法使用Sun的JAX-RPC标准实现(JAX-RPC SI)作为其Web service工具包。这个工具包是可靠的,能够生产部署在生成环境中的Web service,后者实质上是JAX-RPC标准的参考实现,而JAX-RPC规范受到了广泛的支持。例如,BEA和IBM都支持JAX-RPC,而且事实上,您可以把一个Sun实现生产的Web service部署到BEA WebLogic服务器中。另外,Sun的实现符合各种其他规范,比如WS-Security,这些规范正在变得越来越重要。您可以下载用于构建一个Web service客户端的源代码。
要编写一个客户端,您需要一台服务器进行编程。可用的服务器有几种,比如Google或eBay,但是这里我们将使用Amazon的Web service。但是,仅仅知道我们想要使用的服务还不够。我知道Amazon有一个Web service,这个事实并不能让我随心所欲。我需要知道如何调用该Web service。它支持哪些操作?我应该传递哪些参数?我会获得什么返回值?
这些问题说明,要编写一个Web service,必须存在针对该Web service的可用描述。除了客户端/服务器系统之外,服务器也有描述语言,例如,DCOM和CORBA均使用接口定义语言(Interface Definition Language,IDL)。DCOM和CORBA IDL并非同一种语言——也就是说,CORBA客户端无法使用DCOM服务器的IDL来描述对客户端可用的远程服务。
描述服务
通过提供对Web service的描述,Web service描述语言代替了IDL在Web service中的位置。WSDL是一种机器可以使用的XML语法。代码生成器可以读取它。人类也可以读取它,但是它不是使用或者生产起来最容易的语言。我的一个同事Simon Horrell,他喜欢说下面这句话,“WSDL就像太阳。没有它你无法生存,但是如果您盯着它的时间过长,您就会变瞎!”
Amazon提供一个Web service工具包,您可以从Amazon站点下载它。这个工具包提供针对Amazon Web service的文档,而且它还指定了用于描述Web service的WSDL文档的位置,您也可以在Amazon的站点上找到这些文档。当您打开这份WSDL文档时,如果它看起来十分复杂(它的确也十分复杂),不要过于担心,但是您要明白, 它多多少少提供了对Web service的完整描述。当不需要Amazon工具包时,您将需要一个开发人员令牌(同样可以下载)。Amazon使用这个令牌来跟踪Web service的使用,并检查是否有滥用服务的情况发生(参见参考资料)。
编写Web service时,您可以采用很多方法,但是这些方法大致可以分为两类。您可以使用所选择的工具包提供的工具来使用WSDL,并生产进行调用的客户端桩;或者您可以使用底层API来手动编写Web service。
使用这两种方法时需要进行权衡。第一种方法(使用WSDL)相对较为容易,但是,它不是很灵活。许多可用的工具会为您做大量的工作。尽管工具易于使用,而且它们生成的代码也易于使用,您可能会发现,这些代码无法完成您想要的全部功能,而且它们的性能可能不够好。例如,您可能需要访问原始的HTTP通信,而生成的代码可能不会让您这样做。
第二种方法较难,但是您可以获得更大的灵活性。从头开始编写代码可能很困难而且耗时,但是这样做最终会获得很好的灵活性和性能。可以通过不同的方式做到这一点:您可以使用Java java.net.URL及相关类,或者可以使用java.net.Socket进行低级工作。也可以使用HTTP API,比如Jakarta Commons项目提供的那些API(参见参考资料)。通常,您会使用代码生成工具,而有时候,您可能需要手动地编写客户端的某些特定部分。
向下直到桩
一开始,我们将使用Sun的JAX-RPC SI构建客户端(和服务器)。您可以从Sun的Java开发人员网站上下载Java Web Services Development Pack (JWSDP)(参见参考资料)。您下载并安装了该工具包之后,它包括了JAX-RPC SI。在安装期间,您会被要求选择一台特定的Web服务器。您可以在此处选择下载一台Web服务器,或者不选择Web服务器。
有关安装的细节可以成为另一篇文章的主题了;您只要知道我们将使用%JWSDP-HOME%来指定安装JWSDP的位置就可以了。安装之后,确保您的系统路径中有%JWSDP-HOME%/jaxrpc/bin。这个目录包含了Windows批处理文件和Unix shell脚本,以便运行用作JWSDP一部分的工具。您还可以把%JWSDP_HOME%/jaxb/bin添加到其他工具的路径。
JAX-RPC提供一个可以读取WSDL并生成客户端桩的工具。这些桩是将为我们的代码所用的Java类和接口。这些桩给服务器端功能提供了一个客户端接口。例如,如果我们的服务器提供一个Maths服务,该服务带有一个叫做add的方法。我们的客户端代码将调用桩上的一个方法,而桩实现将对该方法使用参数,把Java方法调用变为Web service请求。这个请求将基于HTTP发送给服务器,而且将使用SOAP作为RPC协议(参见图1)。
发出一个请求
客户端调用桩上的一个方法,而桩实现对该方法使用参数,把Java方法调用变为将基于HTTP发送给服务器的Web service请求。
客户端调用桩,而桩把调用转换为SOAP消息,然后桩使用RPC发送该消息。监听服务器接收该SOAP消息,然后(十有八九)将其转换为服务器处的一次方法调用。如果服务器是用Java编写的,上述SOAP消息就会被转换为Java调用。如果服务器是.Net服务器,调用将很可能是C# 或VB.Net对象。服务器的返回值被转换回为SOAP消息,然后返回给桩,而桩会把返回的SOAP消息转换为Java响应。
wscompile工具生成了桩,而且它具有选项加载。对于此种讨论,我们只需要关心运行wscompile生成客户端代码的过程。对于wscompile来说,首要的输入是配置文件(通常叫做config.xml)。wscompile读取这个文件,用于指定一些信息,比如用于生成客户端的WSDL文档的位置。或者,如果我们准备生成服务器,您也可以指定要用于服务器端生成的Java类名。在这种情况下,我们会生成客户端代码,所以配置文件应该包含WSLD位置。
如果您计划遵循这里开发出来的代码,您应该创建一个目录来包含要用到的所有部分,然后在该目录中创建一个src、一个classes和一个etc目录。src目录将包含我们编写的源代码,classes目录将包含编译以后的代码,而etc目录将保存配置信息。我们将在etc目录中创建config.xml文件。
<?xml version="1.0" encoding="UTF-8"?>
<configuration xmlns="http://java.sun.com/xml/ns/jax-rpc/ri/config">
<wsdl location="http://soap.amazon.com/schemas3/AmazonWebServices.wsdl"
packageName="javapro.amazon" />
</configuration>
配置元素
文件包含什么内容呢?首先,所有的元素都位于http://java.sun.com/xml/ns/jax-rpc/ri/config名称空间中。配置元素始终是root元素,而且还有一个wsdl元素,它包含了两个属性:location和packageName。location属性显然是要读取的WSDL文件所在的位置。wscompile使用packageName属性作为用于保存所生成代码的包的名称。位置可以是本地文件或者URL.config.xml。
现在从命令行运行wscompile,同时传递-help标志(参见图2)。正如您看到的那样,有很多可用的标志和特性。现在我们关心的是-gen(或者-gen:client,二者是一样的);-d,告诉wscompile在哪里存放编译以后的文件;还有-s,是生成源代码的位置。您还可以使用-verbose,它让您知道wscompile正在做什么,以及-keep,它指定保存所生成的源文件。您还需要把配置文件作为一个参数传递给wscompile。
特性列表
wscompile 命令有很多可用的标志和特性,您可以通过使用-help标志来查看它们。与这里的讨论有着密切关系的是-gen (或-gen:client)、-d和-s。
[client].sh
-verbose -gen -d classes
-s src -keep etc/config.xml
这个参数会生成大量的输出,详细描述为Web service生成的代码。wscompile工具生成源代码并将其编译为类。如果没有传递-keep标志,源代码就会丢失。这里有着大量的代码,但是我们只会对客户端使用其中的一小部分。
现在从命令行运行wscompile,同时传递-help标志(参见图2)。正如您看到的那样,有很多可用的标志和特性。现在我们关心的是-gen(或者-gen:client,二者是一样的);-d,告诉wscompile在哪里存放编译以后的文件;还有-s,是生成源代码的位置。您还可以使用-verbose,它让您知道wscompile正在做什么,以及-keep,它指定保存所生成的源文件。您还需要把配置文件作为一个参数传递给wscompile。
[client].sh
-verbose -gen -d classes
-s src -keep etc/config.xml
这个参数会生成大量的输出,详细描述为Web service生成的代码。wscompile工具生成源代码并将其编译为类。如果没有传递-keep标志,源代码就会丢失。这里有着大量的代码,但是我们只会对客户端使用其中的一小部分。
WSDL文档包含一个服务元素。这个元素提供对Web service的最高级描述——实质上就是它的名称。(WSDL中)一个服务是端口的集合,而端口是对于如何调用服务的高级描述。特别地,端口包含Web service的地址。如果查看生成的类,您将看到两个文件:AmazonSearchService.java和AmazonSearchService_Impl.java。这两个文件分别是一个接口和一个实现类,代表了Amazon的WSLD中定义的服务。还有一个叫做AmazonSearchPort.java的接口,它对应于WSDL端口定义。如果查看Port接口,您将看到它包含Amazon Web service所能执行的所有操作的列表。桩类实现了这个服务接口。该桩叫做AmazonSearchPort_Stub.java,而且这是我们的客户端将会使用的类(参见清单1)。
查找桩
清单1. 服务接口是由叫做AmazonSearchPort_Stub.java 的桩类实现的,Web service客户端将会使用这个桩类。
public class AmazonSearchService_Impl extends
com.sun.xml.rpc.client.BasicService implements
AmazonSearchService {
private static final QName serviceName =
new QName("http://soap.amazon.com",
"AmazonSearchService");
private static final QName
ns1_AmazonSearchPort_QNAME = new QName(
"http://soap.amazon.com", "AmazonSearchPort");
private static final Class
amazonSearchPort_PortClass =
javapro.amazon.AmazonSearchPort.class; public java.rmi.Remote getPort( QName portName, Class serviceDefInterface) throws javax.xml.rpc.ServiceException { // implementation elided } public java.rmi.Remote getPort( Class serviceDefInterface) throws javax.xml.rpc.ServiceException { // implementation elided } public javapro.amazon.AmazonSearchPort getAmazonSearchPort() { String[] roles = new String[] {}; javapro.amazon.AmazonSearchPort_Stub stub = new javapro.amazon.AmazonSearchPort_Stub(
handlerChain); return stub; } }
为了使重要的数据更加显而易见,我们省略了很多实现代码。注意,正是getAmazonSearchPort() 方法创建了AmazonSearchport_Stub(就是桩)的一个实例。桩是使用handlerChain进行初始化的(参见清单2)。
信息结构
清单2. AmazonSearchPort_Stub是使用handlerChain初始化的桩。我们传递给方法一个AuthorRequest结构,而该方法返回一个ProductInfo结构。
public class AmazonSearchPort_Stub extends com.sun.xml.rpc.client.StubBase implements javapro.amazon.AmazonSearchPort { public AmazonSearchPort_Stub( HandlerChain handlerChain) { super(handlerChain); _setProperty(ENDPOINT_ADDRESS_PROPERTY, "http://soap.amazon.com/onca/soap3"); } public ProductInfo authorSearchRequest( AuthorRequest authorSearchRequest) throws java.rmi.RemoteException { try { StreamingSenderState _state = _start( _handlerChain); InternalSOAPMessage _request = _state.getRequest(); _request.setOperationCode( AuthorSearchRequest_OPCODE); AmazonSearchPort_AuthorSearchRequest_ RequestStruct requestStruct = new AmazonSearchPort_AuthorSearchRequest_ RequestStruct(); requestStruct.setAuthorSearchRequest( authorSearchRequest); SOAPBlockInfo _bodyBlock = new SOAPBlockInfo( ns1_AuthorSearchRequest_AuthorSearchRequest_ QNAME); _bodyBlock.setValue (requestStruct); _bodyBlock.setSerializer( ns1requestStruct_SOAPSerializer); _request.setBody(_bodyBlock); _state.getMessageContext().setProperty( HttpClientTransport.HTTP_SOAPACTION_PROPERTY, "http://soap.amazon.com"); _send((String) _getProperty( ENDPOINT_ADDRESS_PROPERTY), _state); AmazonSearchPort_AuthorSearchRequest_ ResponseStruct responseStruct = null; Object _responseObj = _state.getResponse(). getBody().getValue(); if (_responseObj instanceof SOAPDeserializationState) { responseStruct =( AmazonSearchPort_AuthorSearchRequest_ ResponseStruct) ((SOAPDeserializationState)_responseObj). getInstance(); } else { responseStruct = ( AmazonSearchPort_AuthorSearchRequest_ ResponseStruct) _responseObj; } return responseStruct.get_return(); } catch (RemoteException e) { // let this one through unchanged throw e; } catch (JAXRPCException e) { throw new RemoteException(e.getMessage(), e); } catch (Exception e) { if (e instanceof RuntimeException) { throw (RuntimeException)e; } else { throw new RemoteException(e.getMessage(), e); } } } }
同样,您可以安全地忽略这里的很多代码;它就是实现细节。然而,注意传递给该方法的是一个AuthorRequest结构,而该方法返回的是一个ProductInfo结构。此外还要注意,我们将会发送请求至的URL被设置为带有下列调用的方法的一部分:
setProperty HttpClientTransport. HTTP_SOAPACTION_PROPERTY, "http://soap.amazon.com");
创建客户端
现在,让我们编写客户端代码,这实际上是相当容易的事情(参见清单3)。通常,JAX-RPC看起来就像是您在清单3中看见的代码那样:创建服务实现,向它要求所需的Web service端口,并调用适当的方法。其中的特殊之处在于调用authorSearchRequest()方法时,您必须使用作者名初始化AuthorRequest对象。在这种情况下,您想查看书籍部分,您想要概要情况,而不是详细数据,而且您限制了搜索,让它包含关键字“Web”。您需要设置的其他重要值是开发人员令牌。出于明显的理由,我还没有在这里进行赋值!
客户端代码
清单3. JAX-RPC客户端创建了服务实现,向它要求所需的Web service端口,并调用适当的方法。
public class Client { public static void main(String[] args) { AmazonSearchService_Impl impl = new AmazonSearchService_Impl(); AmazonSearchPort port = impl.getAmazonSearchPort(); AuthorRequest authorRequest = new AuthorRequest(); authorRequest.setAuthor("Kevin Jones"); authorRequest.setDevtag("[developer token here]"); authorRequest.setMode("books"); authorRequest.setKeywords("Web"); authorRequest.setType("lite"); try { ProductInfo productInfo = port.authorSearchRequest(authorRequest); Details[] details = productInfo.getDetails(); for (int ndx = 0; ndx < details.length; ndx++) { Details detail = details[ndx]; System.out.println(detail.getProductName()); } } catch (RemoteException e) { e.printStackTrace(); } } }
URL硬编码在桩中。URL不会经常变化,但是假定您需要发送请求给另外一个URL,那该怎么办呢?您可以让一个服务位于多台服务器上,或许是出于故障恢复的理由或负载平衡的目的,或者可能您想通过跟踪工具(可以显示每个方向上正在发送的SOAP数据)发送请求和响应。要完成这些任务,您可以在桩中修改Web service地址:
AmazonSearchPort_Stub port = ( AmazonSearchPort_Stub)impl. getAmazonSearchPort(); port._setProperty( AmazonSearchPort_Stub. ENDPOINT_ADDRESS_PROPERTY, "http://localhost:9090/onca/ soap3");
注意,getAmazonSearchPort()的返回值被转给实现类而不是接口,所以我们可以设置端点地址,以便调用localhost。
然后,您可以运行一个跟踪工具,比如来自Axis的tcpmon或者我编写的HttpProxy(下载HttpProxy的代码)。这个工具被设置为监听客户端要调用的端口上的请求,并把它们转发给原来的端口(参见图3)。
监听
HttpProxy跟踪工具被设置为监听客户端要调用的端口上的请求,并把它们转发给原来的端口。
如果有些问题难于跟踪,您可以使用这些工具来检验SOAP和HTTP。如果使用了代理,您需要告诉Java运行时通过该代理进行调用。您可以通过设置Java系统属性来做到这一点:
public static void main( String[] args) { System.setProperty( "proxySet", "true" ); System.setProperty( "http.proxyHost", "myproxy" ); System.setProperty( "http.proxyPort", "8080" ); System.setProperty( "http.proxyUser", "xxx"); System.setProperty( "http.proxyPassword", "yyy"); ... }
Web service正在快速成为构建客户端/服务器应用程序的一项标准。编写Web service有多种方式,包括手写和使用工具包。有各种可用于Java的工具包,包括开源的Axis和Sun编写的JAX-RPC。关于通过使用wscompile读取WSDL和生成必需的客户端代码,并使用JAX-RPC来实现Web service客户端,我们已经接触到了其方方面面。下一步将是了解如何编写服务器。请参见“编写一个Web service服务器”一文。
我们尚未讨论的还有一些其他的方面,比如管理HTTP会话、HTTP身份验证和管理错误,以及各种Java规范,比如SOAP with Attachments API for Java (SAAJ)和Java Architecture for XML Binding (JAXB),在Web service领域中所扮演的角色。对于Web service也有讲解不够深入的方面,比如管理附件,理解WSDL,以及rpc/encoded和document/literal风格服务之间的差别。我们将来会对这些主题进行更为深入的探讨。