如何为支持会话并使用了复杂类型的Web服务编写Java客户端程序。实现Web服务的主要前提条件是应该能够从任何其他计算机上用任一种语言编写程序来调用Web服务,只要客户端能够
建立一个到Web服务主机的网络连接
使用目标Web服务(通常是HTTP)支持的协议进行对话
创建输出的XML消息并解释传入的XML消息
为Web服务创建客户端的过程中用到的概念对所有语言来说都是通用的。但由于BEA是一家专注于Java的公司,我这里的重点也放在明确地讲述如何从Java客户端调用Web服务。
WebLogic Workshop 7.0包含了一套演示如何创建Java客户端的示例,这些例子位于proxy文件夹的WebLogic Workshop samples project中。然而,这些示例有两方面的限制:没有说明如何管理会话,并且只能处理简单数据类型。本文中的例子将论证这两方面的概念。
BEA的WebLogic Platform提供了两种创建Web服务的方法:WebLogic Workshop和servicegen。WebLogic Workshop是为非J2EE专家准备的,它能帮助你很轻松地从头开始创建Web服务。而servicegen则是一个瞄准J2EE开发者的WebLogic Server工具,它能将现有的Enterprise Java Beans (EJB)用作Web服务。本文用到的用于说明客户端概念的Web服务示例是用WebLogic Workshop创建的,但原理是一样的,因为Web服务都遵循着一套标准。
Web服务概念
让我们从一个定义开始吧:Web服务是一种应用组件,要想对它进行操作可以通过合适的协议向它发送适当格式化过的XML消息。Web服务定义了操作(operation),还定义了能触发操作的消息的格式,操作正是在Web服务收到这些消息时触发的。这种格式可以是"原始的"XML,也可以是简单对象访问协议(SOAP)。SOAP规范为Web服务操作调用消息定义了特定的消息格式化规则。
SOAP有一个细节你需要知道,因为它影响到了Web服务的互操作性。SOAP规范定义了两种对消息进���编码的方式。它们称为"document literal"(也叫"SOAP Section 5",编码正是在SOAP规范文档中的这一部分定义的)和SOAP-RPC(或者"SOAP Section 7")。document literal编码不是专门为远程过程调用准备的,它为文档定义了一种通用格式。SOAP-RPC编码则是直接瞄准了远程过程的调用。
WebLogic Workshop Web服务默认情况下使用document literal编码方式。使用WebLogic Server的servicegen工具创建的Web服务使用SOAP-RPC。
关于Web服务的更多信息,请参见dev2dev方面的文章Introduction to Web Services
WSDL文件是调用Web服务的向导
Web服务为客户端提供了一个向导,告诉客户端如何调用Web服务的操作。这个向导是在一个使用Web服务描述语言(Web Service Description Language,WSDL)格式的XML文件中提供的。如果Web服务提供者希望别人使用他的Web服务,他必须提供一个获取该Web服务的WSDL文件的方法。该文件除了描述Web服务使用的每个操作,还描述了你所发送的用于调用各个操作的消息的具体格式。WSDL还指定了该Web服务的网络地址。
如果WSDL文件中包含了这些信息,那么客户端就有了调用Web服务的操作所需的全部信息。从概念上讲这一任务相当简单:
1. 创建一个XML(通常是SOAP)消息。
2. 按照Web服务期望的样式对操作参数进行编码。
3. 向Web服务的URL发送消息。
4. 等待响应消息
5. ���响应消息的XML转换成可以被客户端代码理解的数据类型。
从理论上说,这很简单,但实际上如果没有其他方面的帮助这将很复杂。这意味着为了编写出Web服务客户端,你需要:
l 能在Web服务操作所需的任意复杂的Java类型和具体的XML或者SOAP编码方式这两者之间进行转换的代码,以及
l 使用Web服务可以理解的协议(像HTTP)来建立网络连接的代码。
第一种代码如果通用性足够好,能处理任意复杂的组合数据类型,那么将尤为复杂。
JAX-RPC和客户端JAR文件
幸运的是,有一种Java API,它提供了所有你所需要的支持,它就是Java API for XML-based RPC (JAX-RPC)。JAX-RPC可用来基于Web服务的WSDL文件为该Web服务实现一个客户端。更幸运的是,WebLogic Server提供了clientgen工具,该工具能生成一个包含完整JAX-RPC基础的客户端JAR文件。你需要这些JAX-RPC基础来轻松调用目标Web服务的一个操作。WebLogic Workshop为你从WebLogic Workshop的测试视图中调用该clientgen工具,并为你返回一个需要和你的客户端一起使用的客户端JAR文件。每个客户端JAR文件都是针对它所创建的Web服务的。为了实现一个客户端,你不但需要客户端JAR文件,而且还需要存储在webserviceclient.jar文件中的一套通用Web服务客户端支持类。webserviceclient.jar文件在WebLogic Workshop的测试视图中也是可用的(通过Proxy Support Jar链接)。
编写基于JAX-RPC的客户端
既然所有概念和逻辑上的背景知识都讲清楚了,现在就可以为一个真正的Web服务创建客户端了。
Web服务和客户端的所有代码都可以在文件RegisterClient.zip中找到。也可以找到一个主机版本的Web服务RegisterPerson.jws。要将该示例安装到WebLogic Workshop中,需要将文件解压缩到BEA_HOME/weblogic700/samples/workshop/applications/samples/proxy目录中。关于该示例的使用说明,请参考readme.html文件。
我将用于这个演示的Web服务叫做RegisterPerson.jws,它接收一个叫做Person的记录。该记录中包含两个联系(地址)记录,一个家庭的和一个工作的。Web服务的其他操作允许查询或者更改当前的家庭或工作联系信息,或者允许查询整个Person记录。Person记录包含两个其他记录这一事实告诉你如何处理一个Web服务接口中的复杂数据类型。另外,RegisterPerson.jws是会话的。setPerson方法启动一个会话,除endSession之外的所有其他方法继续这个会话。endSession用来结束这个会话。本文稍后会解释Web服务会话的概念。
JAX-RPC客户端都遵循同一个样式。一旦创建了一些客户端并熟悉了它们的样式,只要看一下Web服务的文档(例如,WebLogic Workshop Web服务的测试视图)就可以猜出Web服务的JAX-RPC接口了。如果没有Web服务文档的话,可以通过检查那些生成客户端JAR文件的Java文件来找出接口。下面的部分描述了基本的JAX-RPC模式。
例示顶级代理类
你的客户端第一个例示的是一个名为_Impl的顶级类。顶级代理类名称的开始部分应该是Web服务的名称。例如,对于RegisterPerson.jws来说,该顶级代理类的名称就是RegisterPerson_Impl.。
在示例客户端RegisterClient.java中,顶级代理类是在第225行例示的。
m_proxyImpl = new RegisterPerson_Impl(
"http://localhost:7001/samples/proxy/register/RegisterPerson.jws?WSDL");
顶级代理类的构造器参数是URL或者Web服务WSDL文件的文件系统路径。如果在Web服务URL的末端加进一个"?WSDL",那么按照Web服务工具(WebLogic Workshop,.NET,Apache)上的约定,Web服务将返回它的WSDL文件。(你可能会问,"如果Web服务不可用怎么办?",那还用说,你的Web服务客户端就不能正常工作了呗,难道不是吗?)使用URL form是个好办法,因为这允许JAX-RPC客户端代码按照Web服务的当前WSDL来验证自己。如果指定了WSDL文件的本地副本,并且由于你获得了副本Web服务也更改了它的接口,那么Web服务客户端就可能出现故障,但原因不明。通过指定WSDL的URL,可以立即发现你的客户端代码是否过时。
获得一个协议类代理对象
JAX-RPC包括了每个协议的协议类代理类,这些协议可以被Web服务理解(特别是对于WSDL文件中定义的各个端口类型)。代理类都被起了个名字。在示例Web服务中,只支持SOAP协议,所以代理类被称为RegisterPersonSoap。在顶级代理类中通过调用一个accessor方法,就可以得到协议类代理类。所以在我们的例子中,RegisterPerson_Impl定义了方法getRegisterPersonSoap,该方法返回一个RegisterPersonSoap代理类的例子。
In RegisterClient.java, this happens on line 235:
在RegisterClient.java中,这出现在第235行。
m_proxy = m_proxyImpl.getRegisterPersonSoap();
调用协议类代理对象上的方法
协议类代理类,我们的例子中是RegisterPersonSoap,定义了精确反映Web服务操作的stub方法。通常,proxy方法被称为stub方法,因为它们实际上并没有做真正的方法应该做的事情,而是调用远程方法来完成真正的任务。由于示例Web服务定义了一个setPerson方法,所以SOAP类代理类也定义了一个setPerson stub方法。为了调用Web服务的setPerson方法,首先应该调用代理的setPerson stub方法。
记住这个看似简单的方法调用实际上为你做了不少事情。它将方法的所有参数编组,以组成一个适当地格式化过的SOAP消息(意味着它将参数从Java类型转换成XML类型),再将这个消息发送给Web服务,然后等待SOAP响应消息,最后解分组响应消息中的数据并把这些数据作为Java类型返回给你。
对RegisterClient.java中setPerson的调用发生在第278行。
m_proxy.setPerson(p, startHeader);
这里,p是一个Person对象,startHeader是一个会话头部(本文后面会解释)。
客户端的Java类路径
RegisterClient.java示例中包含了一个可用来编译客户端的build.xml ant script,以及可分别在Windows和Linux/UNIX机器上执行客户端的run.bat和run.sh文件。可以检查这些文件来查看客户端使用的类路径。简单地说,类路径必须包括:
1.客户端类本身
2.使用clientgen创建的或者通过Java Proxy链接从WebLogic Workshop的测试视图中得到的客户端代理JAR文件
3.通用Web服务客户端支持JAR文件webserviceclient.jar,该文件可以在BEA WebLogic Platform 7.0安装文件下的WL_HOME/server/lib目录中找到,也可以通过Proxy Support Jar链接从WebLogic Workshop测试视图中获得。
在webserviceclient.jar中发现的支持类也可以在weblogic.jar中找到,但是类路径中包含的weblogic.jar在测试时容易混淆。你的客户端应该在那些能够访问目标Web服务主机的主机中工作,不管这些主机是否安装了WebLogic Platform。
这(几乎)就是全部事情了
这样说很有道理。Clientgen产生的客户端JAR文件以及底层的JAX-RPC基础使从Java代码中调用Web服务变得很容易。
"等一会儿",你会说,"Person类是从哪里来的,会话头部又是什么"?不要着急,请继续往下看。
Web服务接口中的复杂Java类型
SOAP规范定义了如何将参数构建成一个方法,而WSDL规范则使用XML Schema类型定义了如何在XML中表示Java类型。当为Web服务生成一个WSDL文件时,Web服务操作期望的参数XML Schema被包含在WSDL文件中。当客户端支持代码被生成时,比如说是由clientgen生成的吧,这时clientgen工具就可以从WSDL文件中精确地判断出要在客户端语言中生成何种类型,以便通过客户端代码构建操作参数。至于JAX-RPC,Web服务接口中提到的所有类型都被生成了一个Java类。
在RegisterPerson.jws中,Web服务用到了两个Java类:Person和Contact。它们是在WSDL文件中描述的,并且clientgen为客户端JAR文件生成了具有代表性的类。
客户端JAR文件中的类和Web服务中的类是不一样的,知道这一点很重要。它们只不过是方便创建必需的数据结构以往Web服务的操作中传送数据的类而已。如果检查这些类,就会注意到它们除用来设置和获得类的数据成员所必需的accessor和mutator之外,不包含其他任何方法。它们不包含(也不能)Web服务使用的实际类的任何逻辑,因为代理类是从WSDL文件创建的,而该文件只包含类型和结构信息。
默认情况下,代理类是使用反映了Web服务(或者XML映像中相应的元素,当存在时)的XML命名空间的包分层生成的。RegisterPerson.jws使用WebLogic Workshop的默认命名空间http://www.openuri.org,所以代理类是在org.openuri.www包中生成的。
org.openuri.www.Person
org.openuri.www.Contact
对于那些使用document literal编码方式的Web服务来说,这些类被用来向proxy stub方法传送参数。想知道如何处理各种不同的SOAP编码方式,请继续往下读。
记住当你使用Java Proxy链接来从WebLogic Workshop的测试视图中获取Web服务的客户端JAR文件时,WebLogic Workshop按照默认设置为你调用了clientgen。如果你手工调用clientgen,可能还提供了自定义clientgen行为的参数。例如,你可以控制生成类的包层次。
SOAP document/literal与SOAP-RPC的比较
本文刚开始的时候,我就提到了SOAP规范中定义的两种消息编码样式:document literal和SOAP-RPC。WebLogic Workshop Web服务使用document literal作为默认的编码方式,但是在每Web服务(per-Web-service)或者每方法(per-method)基础中可以忽略这种规定。WebLogic Server (servicegen) Web服务只使用SOAP-RPC。
虽然不同的SOAP编码方式说明了用来调用Web服务操作的SOAP消息也必须以不同的方法格式化,但JAX-RPC隐藏了大部分的区别。作为Web服务客户端的开发者,你能看到的唯一区别在于由代理类生成的包。
在RegisterPerson.jws中,我囊括了setHomeContact和setWorkContact方法的document literal和SOAP-RPC两种版本。这些方法的SOAP-RPC版本被分别命名为setHomeContactRPC和setWorkContactRPC。
org.openuri.www.encodedTypes.Contact
用于document literal 和SOAP-RPC的proxy stub方法除了用于传递参数的代理类外是完全一样的。对于SOAP-RPC方法来说,代理类在包分层中生成了一层。在SOAP-RPC方法中只用到了Contact类,生成的代理类为:
org.openuri.www.encodedTypes.Contact
由于这个类表示的Web服务内部类和org.openuri.www.Contact表示的一样,它们具有同样的接口,所以必须严格地用相同的方法来使用它。RegisterClient.java中的示例在第343行。
m_proxy.setHomeContactRPC(soapRPCContactB, contHeader);
会话Web服务
在其他应用中,Web服务可能被用于展示一些业务操作,这些操作是那些可能是长时间的业务处理中的步骤。不同的操作可能需要不同的反应时间。有些操作甚至还需要人来查看数据并作出决策,这比起全自动的操作来慢多了。这种不可预测的发应时间要求Web服务必须能记录状态--当正在等待的慢操作完全结束时,Web服务要正确地记住它们的状态。不幸的是,Web服务默认的协议HTTP是一种无状态协议。Web服务器和浏览器通过cookie来逃避这个问题,但是Web服务体系结构并不能指望客户端可以管理cookie。
为了解决这个问题,WebLogic Workshop引入了会话式Web服务这个概念。Web服务的每个操作都被标识成一个会话的开始、继续和结束。无论会话处在哪个阶段,Web服务的状态(包括创建Web服务的人所需的数据)都是自动保持的。将Web服务操作标识为一个会话的某个参与阶段就像为一个操作设置属性一样简单。
每个会话都由一个独一无二的会话ID来标识。当Web服务客户端调用一个会话开始操作时,该客户端就负责生成一个惟一的会话ID,并且当客户端需要调用在同一个会话中执行的以后的Web服务操作时,客户端必须具有同样的会话ID。会话ID是一个字符串。在本例中的客户端,我使用了一种简单的策略来设计会话ID:先将当前的日期和时间表示成long型的值,然后转换成字符串。在RegisterClient.java中,会话头部在第264-266行被初始化:
java.util.Date now = new Date();
startHeader = new StartHeader((new Long(now.getTime())).toString(), null);
contHeader = new ContinueHeader((new Long(now.getTime())).toString());
注意到,继续头部与开始头部一样,都有相同的会话ID。因为我希望这个客户端的所有操作都在同一会话中发生。
如果Web服务操作被标识为会话开始操作(在JWS文件中),则代理stub方法将接收一个StartHeader对象作为一个额外参数。如果Web服务操作被标识为会话继续或会话结束操作,那么stub方法将接收一个ContinueHerder对象作为一个额外参数。正如前面我们在本文中对复杂类型所作的描述那样,Java包层次(其中定义了这些头部类)起源于WSDL文件中的数据结构的XML命名空间。对于这些会话头部类,包层次(以及命名空间)还包括版本信息。开始头部的完整的Java类是这样的:
org.openuri.www.x2002.x04.soap.conversation.StartHeader
从中可以得出,在WSDL文件中,会话头部的XML命名空间如下:
http://www.openuri.org/2002/04/soap/conversation/
构造了会话头部对象后,就将其传给需要这个对象的代理stub方法。在RegisterClient.java中,每个代理stub方法都需要重复这一过程。在321行就有这样的例子:
m_proxy.setHomeContact(soapLiteralContactA, contHeader);
你可能会问:"为什么它们叫做'头部'呢?"这里之所以将它们作为头部来讲,是因为它们将作为另一部分SOAP消息的SOAP头部。另一方面,stub方法所带的参数将被传给SOAP的主体。
会话和用于管理来自客户端的会话的SOAP头部机制是一种仅仅WebLogic Workshop Web服务才有的概念。然而,所有的Web服务都会被要求能够参与长时间运行的、异步的业务处理。EBA Systems公司正在与工业组织和标准化组织一道,致力于会话这一概念的发展。
总结
在最底层,调用一个Web服务操作是件相当复杂的事情,涉及到网络连接管理和数据的编组和解编组,数据的编组和解编组又牵涉到广义Java-XML转换。但是,正如我在此所作的演示那样,有了JAX-RPC API以及WebLogic Server所提供的clientgen工具,为Web服务编写Java客户端不是什么难事。虽然我在这里举的例I正好是一个WebLogic Workshop Web服务,你也应该可以将这些概念应用于为任意Web服务创建Java客户端。