了解 Web 服务规范: 第 1 部分:SOAP

面向服务的体系结构(Service-Oriented Architectures,SOA)当前强调的重点在 Web 服务上,但很容易被所传播的各种信息搞得昏头转向。本系列教程将对主要 Web 服务规范进行全面说明,从简单对象访问协议(Simple Object Access Protocol,SOAP)一直介绍到 WS Business Process Execution Language (WS-BPEL)。本教程将介绍 Web 服务和 SOAP 基本概念,并说明如何构建 SOAP 服务器和客户机。

Nicholas Chase (ibmquestions@nicholaschase.com), 自由撰稿人, Backstop Media

Nicholas Chase 曾参与多家公司的网站开发工作,如 Lucent Technologies、Sun Microsystems、Oracle 和 Tampa Bay Buccaneers。Nick 担任过高中物理教师、低辐射废料处理设施管理员、在线科幻小说杂志编辑、多媒体工程师、Oracle 讲师以及一家交互通信公司的首席技术官。他已经出版了多本图书,其中包括 XML Primer Plus (Sams)。



2006 年 9 月 11 日

开始之前

本系列教程将说明构建一家假想的报社 Daily Moon 的基于 Web 访问的工作流系统的详细步骤。主要针对的是希望了解有关 Web 服务的更多信息的开发人员,以便他们能更为高效地创建应用程序。非技术用户也可以从中获得一些好处,因为在讨论如何编程前对相关概念进行了解释。

您应该具有基本编程知识,如果希望按照实际编程示例进行操作,还需要一定的 Java 知识。我们将讨论 XML(但没有必要对其进行深入讨论),并将涵盖任何必要的概念。

关于本系列

本系列教程以假想的报社 Daily Moon 为例,为了提高在竞争激烈的环境中的生产力,其员工将使用各种 Web 服务来创建工作流系统,我们将在此过程中讲述各个 Web 服务基本概念。

第 1 部分比较简单,将说明 Web 服务背后的基本概念,并演示如何使用 SOAP(以后要讨论的大部分内容的基础规范)来将 Classifieds Department 连接到内容管理系统。

本系列以后的部分将以这些基本概念为基础:

  • 第 2 部分进一步深入说明如何使用 Web 服务描述语言(Web Services Description Language,WSDL)定义 Web 服务预期产生的消息,从而使团队更方便地创建服务以及连接到服务的客户机。
  • 在第 3 部分中,团队希望准备一系列服务,并希望能方便地查找这些服务。与此对应,统一描述、发现和集成(Universal Description, Discovery and Integration,UDDI)提供了可用服务的可搜索注册中心,以便将自己的服务发布给其他人。
  • 第 4 部分和第 5 部分讨论 WS-Security 和 WS-Policy,将详细说明如何保证该报社的服务的安全,以及团队为了访问这些刚提供了安全保护的服务需要进行哪些更改。
  • 第 6 部分重点讨论的是互操作性,因为必须从单个系统访问来自几个不同实现的服务。第 6 部分讨论了在 WS-I 证书中涉及的要求和测试。
  • 最后,第 7 部分演示了如何使用业务流程执行语言(Business Process Execution Language,WS-BPEL)来从各个服务创建复杂应用程序。

接下来让我们更为详细了解一下本教程中将讨论的内容。

关于本教程

本教程将向您以及假想的 Daily Moon Classifieds Department 介绍 Web 服务的概念。您将了解该团队如何与现有 Web 服务系统集成以及如何创建服务的详细过程。重点将是简单对象访问协议 (SOAP)。

在本教程中,您将了解以下内容:

  • Web 服务的基本概念
  • XML 的基本知识
  • SOAP 消息的结构和用途
  • 如何安装应用服务器以在其上运行 Web 服务应用程序。
  • 如何将 Web 服务实现安装到应用服务器中
  • 如何以编程方式创建 SOAP 消息。
  • 如果使用 Java 和 Apache Axis2 为基于 SOAP 的 Web 服务创建客户机
  • 如何使用 Java 和 Apache Axis2 创建基于 SOAP 的 Web 服务

在本教程中,Classifieds Department 将通过创建客户机来与内容管理系统集成。您还将了解该部门与之交互的一个服务的创建过程。编程示例采用 Java 语言,并使用了 Apache Axis2 项目,但其中的概念几乎适用于任何语言和环境。

先决条件

为了按照本教程所示的处理代码,您需要有以下软件:

  • Apache Geronimo 或其他应用服务器。您将在本教程中创建各种 Web 服务,需要在其上运行这些服务的应用程序。当然,由于 Web 服务应当可互操作,因此使用何种应用服务器并不重要。在本教程中,我们将演示如何安装和使用 Apache Geronimo(IBM® WebSphere® Community Edition 也是构建于其上的)。还可以使用 WebSphere Application Server 等其他应用服务器。可以从 Apache Geronimo Downloads 站点下载 Apache Geronimo。
  • Apache Axis2 或其他 SOAP 实现。可以手动创建 SOAP 消息,也可以手动对其进行解释,但手边如果有一个可用实现就会方便得多。您将使用的是 Apache Axis2,其中包含了各种 SOAP 相关的 API,可极大地简化您的工作。可以在以下网址下载 Apache Axis2:Apache.org。本教程使用的是 0.94 版,但应该也能使用更高版本。
  • Java™ 2 Standard Edition 1.4 或更高版本。这两个工具都基于 Java,和将在本教程中构建的服务和客户机一样。可以从此处下载 J2SE SDK。
  • 另外,还需要 Web 浏览器和文本编辑器,但我想您已经有了这两个工具。如果愿意,还可以使用 Eclipse 之类的 IDE,但由于本教程的重点是技术而不是工具,因此我将使用文本编辑器和命令行来编辑和编译文件。

什么是 Web 服务?

首先,让我们从总体上了解一下什么是 Web 服务,以及它们为何对软件开发重要。

究竟为什么重要呢?

如果您没有听说过有关面向服务的体系结构 (SOA) 和 Web 服务的大量信息,您就不会阅读本文,那么问题就应该是,为什么此内容这样重要?答案是,此内容之所以重要,是因为这是应用程序彼此进行通信的方式的典型变化。SOAs 已经存在很长很长时间了。SOA 最初主要由中间件应用程序组成,至少进行连接的两端都属于同一种类型的中间件。另一方面,Web 服务由一组标准组成,用于在不需要特定类型的中间件、编程语言甚至操作系统的前提下让各种不同的系统进行通信。接下来,让我们了解一下其发展的历程。

传统应用程序

首先从计算机发明开始,当时给人感觉非常不错。计算机能执行奇迹般的任务,可实现很多手动工作的自动化,包括复杂的计算、财务工作等等很多其他任务。

但传统应用程序是“竖井”(Silo) 型的。人力资源应用程序无法与财务应用程序真正通信,而后者又无法和分布应用程序进行真正的通信。所有这些应用程序都有独立的领域,在独立的计算机上运行,尽管很有用,但并不能很好地在彼此间共享数据。当时可以选择对批处理流程进行连接,以将数据从一个系统移动到另一个系统,但这并不适合进行实时集成。

分布式计算

在我们的进化链中的第二步是分布式计算。分布式计算允许不同的应用程序彼此进行通信(即使位于不同的计算机上也是如此)。CORBA、MTS 和 Enterprise Java Bean (EJB) 等技术提供了包含各种类别的注册中心的系统,因此应用程序可以找到其希望与之进行交互的组件,然后像调用本地的组件一样调用这些组件。

这些系统由可同时满足这两个要求的中间件(或更具体一些,面向消息的中间件)提供支持。现在能以特定的方式构建应用程序,即使位于不同的地理位置,也能访问其他系统上的资源。

但仍然有一个问题。虽然系统可以自由地与系统内的任何对象进行通信,但仍然是一个封闭的系统。至少,客户机应用程序必须与服务器应用程序使用相同的技术。另外,通常并不会将系统设计为从创建其的个体组织外进行访问。

Web 服务

此进化链中下一个几乎不可避免的链接点就是 Web 服务。“Web 服务”基于 XML 和 HTTP(大多数情况下),对很多人具有不同的含义,但在此处,我们要将 Web 服务作为系统间基于 SOAP 的消息交换进行讨论。

这些消息由 XML 组成(XML 是一个基于文本的开放标准),可由来自任何应用程序(任何设计为接收此类消息的应用程序)的任何人进行访问。这就扩展了应用程序的范围,从而包含任何可通过网络对其进行访问的任何人。(如果这让您开始考虑安全问题,不要紧,您将在本系列的第 4 部分了解如何处理这方面的问题。)

基于 SOAP 的 Web 服务将要发送与清单 1 中所示类似的 XML 消息。

清单 1. 基于 SOAP 的 Web 服务
<SOAPenv:Envelope 
       xmlns:SOAPenv="http://schemas.xmlSOAP.org/SOAP/envelope/" 
       xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <SOAPenv:Body>
  <req:getNumberOfArticles xmlns:req="http://daily-moon.com/CMS/">
     <req:category>classifieds</req:category>
  </req:getNumberOfArticles>
 </SOAPenv:Body>
</SOAPenv:Envelope>

这些消息将从一个系统进入另一个系统(通常通过 HTTP)。接收系统对消息进行解释,进行应该进行从处理,然后发送另一个 SOAP 消息作为响应。

这个系统很简单,正因为如此,有很多企业级计算方面的内容都尚未涉及。幸运的是,其中的很多方面已被纳入考虑范畴,且具有自己的相关规范来确定此事务应如何进行,以包含传统的面向消息的中间件的很多安全和其他方面的内容。

其他类型的 Web 服务

如果我没有说明 SOAP 并不是唯一的处理 Web 服务的方法,那就有些失职了。有很多其他基于 XML 的方法用于在系统间发送信息,其中一些适用于企业环境,而另一些则不适合此用途。例如,Amazon 是为公众提供对其系统的 Web 服务访问的第一批基于 Web 服务的公司之一。Amazon 包含了一个基于 SOAP 的服务,但也提供了一个基于 Representational State Transfer (REST) 的服务。

REST 是一种 Web 服务类型,其中,用户直接访问 URL,相应的响应是与清单 2 中所示类似的简单 XML 文档。

清单 2. REST 响应
<currentArticles>
   <category>classifieds</category>
   <subcategory>forsale</subcategory>
   <article id="888204">
       <articleHeadline></articleHeadline>
       <articleText>30 ft ladder, only used once.  Willing to let
go for half it's worth. Has slight dent near the middle.  
Harder than a human head. $150 OBO.</articleText>
    </article>
   <article id="888242">
       <articleHeadline></articleHeadline>
       <articleText>Vintage 1963 T-Bird.  Less than 300 miles.  
Driven by my daughter until I took it away.  Serious inquires only.
555-3264 after 7 PM.</articleText>
    </article>
</currentArticles>

这些消息并没有特定的格式。可以为任何数据。

另一种 Web 服务类型要使用 XML-RPC 之类的标准。在这种情况下,命令将通过与清单 3 中所示类似的 XML 发送到系统。

清单 3. XML-RPC
<?xml version="1.0"?>
<methodCall>
   <methodName>CMS.getNumberOfArticles</methodName>
   <params>
      <param>
         <value><string>classifieds</string></value>
      </param>
      <param>
         <value><string>forsale</string></value>
      </param>
   </params>
</methodCall>

其响应将采用类似的格式。

在学习使用 SOAP 的过程中,您可以会打心底里认为 REST 和 XML-RPC 比基于 SOAP 的系统要简单得多。的确如此。在某些情况下 REST 和 XML-RPC 比 SOAP 系统简单。不过,我们讨论的不是在网站上显示天气的简单应用程序。我们此处讨论的是企业级应用程序,而企业级的应用程序需要企业级的属性,如安全、互操作性等等。这些功能在有关基于 SOAP 的 Web 服务的其他规范中进行了定义,因而,从长期来看,SOAP 更适合用于企业级应用程序。

让我们了解一下这些规范。

基本 Web 服务规范

Web 服务规范通常归为两类:基本 Web 服务规范和扩展 Web 服务规范。基本规范有:

  • SOAP:SOAP 规范是所有基于 SOAP 的 Web 服务的基础,详细说明了实际消息的格式。其中还详细说明了应用程序应如何对待消息的特定方面(如“Header”中的元素),从而可以创建特定类型的应用程序,使其中的消息在达到最终的目的地前在多个中间层之间进行传递。本教程将涵盖 SOAP 规范的内容。
  • WDSL:Web 服务描述语言是详细说明描述基于 SOAP 的 Web 服务的标准方式的规范,包括消息应采用的形式以及应将其发送到何处。其中还详细说明了此类消息的响应。当与相应的工具结合使用时,WSDL 允许以编程方式创建对 Web 服务的调用,甚至不用知道所查找的 Web 服务是什么;应用程序可以从 WSDL 文件中提取这些详细信息,并提供要使用的编程接口。我们将在本系列的第 2 部分讨论 WSDL。
  • UDDI:统一描述、发现和集成 (Universal Description, Discovery and Integration) 是一项从最初提出后发生了一系列变化的标准。其最初的目的是为了给各个公司提供在全球注册中心中注册服务并在此注册中心中搜索可能想使用的服务的机制。不过,由于很多公司对于将其系统对外开放的问题上都相当保守,这个目标并没有完全实现。但是,很多公司已将 UDDI 作为内部的服务及服务信息注册中心使用,本系列第 3 部分将对其使用进行详细论述。

以上是一些基础知识。另外差不多还有数十种扩展标准,可进一步扩展基于 SOAP 的服务的用途。

扩展 Web 服务规范

总共有数十种 WS-* 规范,其中几种对企业尤为有用。即:

  • WS-Security:此规范处理加密和数字签名,允许创建特定类型的应用程序,以防止窃听消息,且能实现不可否认功能。本系列的第 4 部分将讨论 WS-Security。
  • WS-Policy:此规范对 WS-Security 进行了扩展,允许更具体地说明谁可以采用何种方式使用服务。本系列的第 5 部分将讨论 WS-Policy。
  • WS-I:尽管 Web 服务应设计成具有互操作性,但在实际中,各个规范对不同实现的解释的灵活性常常足以导致出现问题。WS-I 提供了一组可用于防止出现各种问题的标准和实践,并提供了标准化测试来检查问题。WS-I 是本系列的第 6 部分将要讨论的主题。
  • WS-BPEL:单个服务很好处理,但应用程序在大多数情况下则较难处理。企业级计算要求至少将多个服务组合为一个完整的系统,而 WS-BPEL 提供了用于指定创建此类系统所必需的交互(如分支和并发处理)。本系列的第 7 部分将讨论 WS-BPEL。

在 Web 服务中扮演重要角色的其他规范将不在本系列中讨论,其中包括 WS-ReliableMessaging(允许确定检索一个且仅一个消息副本)、Web 服务资源框架 (WSRF)(允许使用在无状态环境中非常重要的状态)和 Web 服务分布式管理 (WSDM)(讨论 Web 服务的管理和使用问题)。可以在本教程的参考资料中获得有关这些规范及其他规范的更多信息。

将要完成的工作

在本教程中,您将了解 Daily Moon 报社的 Classifieds Department 如何将其自己的系统与内容管理系统使用的基于 Web 服务的接口进行集成。将创建一个应用程序,以创建基于 SOAP 的消息,将其发送到服务,然后检索响应。完成了此工作后,您将了解如何创建服务来响应请求并发送自己的响应。将通过创建 Java 应用程序(使用或不使用应用服务器)来完成此工作。


设置

现在已经了解了所涉及的基本原理,接下来让我们看看如何实际创建应用程序。首先要使所需软件就绪。

设置 Apache Geronimo

将需要的第一个软件是 Web 应用服务器。为什么需要 Web 应用服务器呢?因为您会发现,没有 Web 应用服务器很难提供 Web 服务。Web 应用服务器将侦听请求,将这些请求转换为实际服务可以理解的内容,然后进行任何必要的处理。

此过程中几乎可以使用任何 Web 服务器,但在大多数情况下都将使用能简化此过程的软件(通常要求使用某种类型的实际应用服务器)。具体来说,本教程假定您将使用 Java 应用服务器。(实际上,将使用 J2EE 应用服务器。)

对于 J2EE 服务器,有很多选择;在本例中将使用 Apache Geronimo。Geronimo 是作为 IBM WebSphere Application Server Community Edition 基础的开放源代码应用服务器。Geronimo 很小,非常易于安装和管理。它还能与其他 Apache 项目(如稍后将安装的 Axis2)一起良好地工作。

请下载相关软件(请参阅先决条件)并将文件提取到目标目录中。您会发现所提取的文件中包含自身的目录,因此可以直接对其进行解包,并可将其移动到任何位置。任何目录都是可接受的,但要避免名称中包含空格的目录,如“Program Files”、“Documents and Settings”或其子目录。例如,本教程的测试安装使用的是 e:\geronimo-1.0。

就是这里。已经在其中安装了 Geronimo。这不是很简单么?

要启动此服务器,请打开命令提示符窗口,并执行以下命令:

cd <GERONIMO_HOME>

java -jar server.jar

要确保服务器正在运行,请打开浏览器,并指向清单 4 中所示的 URL:

清单 4. 服务器 URL
http://localhost:8080

应该看到与图 1 中所示类似的页面。

图 1. 服务器正在运行
服务器正在运行

现在让我们开始安装 Web 服务软件。

安装 Apache Axis2

完全可能从普通 HTTP 服务器提供 Web 服务。不过,这样并不明智。处理 SOAP 消息需要进行很多工作,但没有必要完全从头进行所有工作。Apache Axis 项目几年前就对此任务进行了简化,创建了可方便地创建和处理 Web 服务的环境。该软件包含了多个应用程序,可帮助从普通项目创建 Web 服务,从 Web 服务创建 Java 对象,并对二者进行处理。Apache 小组已推出了 Axis 的新版本 Axis2,该版本继承了 Axis 上的所有已有功能,并通过更改体系结构实现了更大的灵活性,从而将其推向了一个新的高度。这很重要,因为 Web 服务规范随时都在增加。Axis2 的构造允许它更方便与 WSS4J、Apache 的 WS-Security 实现等项目集成。由于我们将在以后使用这些项目,因此请现在安装 Axis2(有关下载信息,请参阅先决条件)。

请确保同时下载 Binary Distribution 和 War Distribution。前者将帮助构建客户机,而后者用于帮助构建服务。

要将 Axis2 安装到 Web 服务器中,请将 axis2.war 文件复制到 Geronimo 的 deploy 目录。(您将需要确保已经至少启动过 Geronimo 一次,才能保证存在此目录。)Geronimo 将自动检测其是否存在,因此不必手动部署任何内容。

验证 Axis 的安装

要确保已经恰当安装了所有内容,请将浏览器指向 http://localhost:8080/axis2/index.jsp 并单击 Validate。

应该看到与图 2 中所示类似的页面。

图 2. Axis 安装成功
Axis 安装成功

在继续本教程的其他内容前,请确保成功安装了 Axis。

安装示例服务

将要编写的第一个应用程序是客户机,因此将需要其可访问的服务。幸运的是,Axis2 分发版提供了若干此类服务。首先,将按照以下所示安装 MyService 服务:

  1. 通过将浏览器指向 http://localhost:8080/axis2/Login.jsp 并登录,从而通过身份验证。缺省用户名和密码分别为 adminAxis2
  2. 单击 Upload service>Browse
  3. 导航到 MyService.aar 文件。可以在标准 Axis2 分发版的 samples\userguide 目录下找到该文件。单击 OK
  4. 单击 Upload

应该看到一个通知,指示服务已添加(请参阅图 3)。在缺省情况下,Axis2 包含“热部署”功能,因此不必进行任何操作来激活。

如 3. MyService.aar 已上载
MyService.aar 上载

现在,让我们了解一下将要构建的内容。


理解 SOAP

现在已经安装了软件,接下来可以开始着手处理实际的 Web 服务了。正如我在其他类型的 Web 服务中提到的,您可以选择多种格式。在本系列中,将使用 SOAP。

XML 简介

进行传递的所有这些消息都基于可扩展标记语言(Extensible Markup Language,XML)。如果完全不熟悉 XML,在深入了解各个 Web 服务主题前,真的应该进行一些相关研究。不过,以下提供了继续学习本教程所需的基本知识。

XML 是一种“标记语言”,即给出了一种提供实际内容的附加信息的方式。此信息以“标记”的形式提供,这些标记用于指示“元素”。例如,考虑一下清单 5 中所示的简单 XML 文档。

清单 5. 包含基本内容的 XML 文件
<article articleId="88271" categoryId="classifieds" subcategoryId="forsale">
  <articleHeadline>Fun, fun, fun</articleHeadline>
  <articleText>Vintage 1963 T-Bird.  Less than 300 miles.
Driven by my daughter until I took it away.  Serious 
inquires only.  555-3264 after 7 PM.</articleText>
</article>

请留意此文本中的几个值得注意的地方。首先,这是文本。这就使其可以供任何人阅读,或在其中包含关于任何事物的内容。其次,标记使用 > 和 < 指示,开始标记具有一个名称,并可能带有各种属性(如文章 ID),而结束标记以反斜杠 (/) 表示。元素必须为自包含的,并进行了恰当嵌套。也就是说,不能使用与清单 6 所示类似的 XML 文档。

清单 6. 无效 XML 文档示例
<article articleId="88271" categoryId="classifieds" subcategoryId="forsale">
  <articleHeadline>Fun, fun, fun
  <articleText></articleHeadline>Vintage 1963 T-Bird.  
Less than 300 miles. Driven by my daughter until I 
took it away.  Serious inquires only.  555-3264 after 
7 PM.</articleText>
</article>

XML 还提供了将内容划分为不同“命名空间”的方法,以便由应用程序对其进行不同的处理。例如,SOAP 消息可能与以下的清单 7 类似。

清单 7. 示例 SOAP 消息
<?xml version='1.0' ?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope"> 
 <env:Header>
 </env:Header>
 <env:Body>
  <cms:getNumberOfArticles xmlns:cms="http://www.daily-moon.com/cms">
    <cms:category>classifieds</cms:category>
    <cms:subcategory>forsale</cms:subcategory>
  </cms:getNumberOfArticles>
 </env:Body>
</env:Envelope>

不要担心消息的实际结构,但要注意存在两种不同的“前缀”,每个前缀与特定的命名空间对应。在这种情况下,我们是为了将 SOAP“信封”与实际的有效负载进行区分。

再次说明,关于 XML 有很多需要学习,但这些只是本教程需要了解的基础知识。

SOAP 信封

Web 服务消息的基本单元是实际的 SOAP 信封。这是包含处理消息所必需的所有信息的 XML 文档(请参见清单 8)。

清单 8. 示例 SOAP 信封
<?xml version='1.0' ?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope"> 
 <env:Header>
</env:Header>
<env:Body>
</env:Body>
</env:Envelope>

在本例中,获得了一个简单的 Envelope,其命名空间指定为 SOAP 1.2 版本。其中包含两个子元素 HeaderBody。让我们了解一下这两个子元素所起的作用。

SOAP Header

SOAP 消息中的 Header 用于提供有关消息本身的信息,与用于应用程序的信息相对。例如,Header 可以包括路由信息,像清单 9 中的示例类似。

清单 9. Header 中的路由信息
<?xml version='1.0' ?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope"> 
 <env:Header>
  <wsa:ReplyTo xmlns:wsa=
        "http://schemas.xmlSOAP.org/ws/2004/08/addressing">
   <wsa:Address>
http://schemas.xmlSOAP.org/ws/2004/08/addressing/role/anonymous
   </wsa:Address>
  </wsa:ReplyTo>
  <wsa:From>
   <wsa:Address>
http://localhost:8080/axis2/services/MyService</wsa:Address>
  </wsa:From>
  <wsa:MessageID>ECE5B3F187F29D28BC11433905662036</wsa:MessageID> 
  </env:Header>
<env:Body>
</env:Body>
</env:Envelope>

本例中有一个 WS-Addressing 元素,其中包含有关消息将送达何处以及应将应答送达何处的信息。Header 可包含关于消息本身的所有类型的消息。事实上,SOAP 规范中使用了大量篇幅说明哪些元素可以放入 Header以及应由“SOAP 中间层”如何对其进行处理。也就是说,SOAP 规范并不假定消息将直接从一个点传递到另一个点(从客户机到服务器)。规范考虑了 SOAP 消息在送达最终目的地的过程中可能实际由多个中间层处理的情况,很清楚地说明了中间层应如何对待在 Header 中找到的信息。不过,对此的讨论不在本教程的范围之内。因此,目前只要知道 Header 可以提供许许多多的功能(如果您需要)即可。

接下来让我们看看实际的有效负载。

SOAP 体

发送 SOAP 消息时,都是有目的性的。您在尝试告诉接收者执行某种操作,或尝试向服务器传递相关信息。此信息称为“有效负载”。有效负载位于 EnvelopeBody 中。它还具有自己的命名空间,在本例中其命名空间与内容管理系统对应。在此情况下,可以完全随意地选择命名空间。只需要与 SOAP 命名空间相异即可(请参见清单 10)。

清单 10. 有效负载示例
<?xml version='1.0' ?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope"> 
 <env:Header>
...
</env:Header>
 <env:Body>
  <cms:addArticle xmlns:cms="http://www.daily-moon.com/cms">
    <cms:category>classifieds</category>
    <cms:subcategory>forsale</cms:subcategory>
    <cms:articleHeadline></cms:articleHeadline>
    <cms:articleText>Vintage 1963 T-Bird.  Less than 300 miles.  
Driven by my daughter until I took it away.  Serious inquires only.  
555-3264 after 7 PM.</cms:articleText>
  </cms:addArticle>
 </env:Body>
</env:Envelope>

在此例中,有效负载很简单,其中包含将文章添加到 Daily Moon 的内容管理系统的指令。

如何设计有效负载的选择过程将涉及到样式和编码的内容。

样式和编码

本系列教程的第 2 部分将更深入地了解此主题的内容(该部分讨论 Web 服务描述语言——WSDL),但在创建应用程序时,您将需要确定要发送和接收的实际有效负载的结构。为此,让我们花点时间讨论一下编程样式和编码。

简单来说,有两种不同的主流 SOA 消息编程样式。第一种是 RPC 样式,基于使用 SOAP 消息创建远程过程调用(Remote Procedure Call)的概念。在此样式中,基本思路是在向服务器发送命令(如“添加文章”),并将该命令的参数(如要添加的文章和应该添加到的类别)作为整个方法的子元素包含在其中,如清单 11 中所示。

清单 11. RPC 样式
<env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope">
 <env:Header>

RPC 样式的替代方法将数据直接作为 SOAP 体的内容处理,并在应用服务器对消息进行路由的信息中包含有关其所属的过程或函数的信息。(请参见清单 12)。

清单 12. 将数据作为 SOAP 体中的内容
<env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope"> 
 <env:Header>
 </env:Header>
 <env:Body>
  <cms:addArticle xmlns:cms="http://www.daily-moon.com/cms">
    <cms:category>classifieds</category>
    <cms:subcategory>forsale</cms:subcategory>
    <cms:articleHeadline></cms:articleHeadline>
    <cms:articleText>Vintage 1963 T-Bird.  Less than 300 
miles.  Driven by my daughter until I took it away.  
Serious inquires only.  555-3264 after 7 PM.</cms:articleText>
  </cms:addArticle>
 </env:Body>
</env:Envelope>

RPC 样式的一个变体就是与上面看到的 RPC/literal 相对的 RPC/encoded。在这种情况下,消息中包含类型信息,如清单 13 中所示。

清单 13. RPC/encoded 示例
<?xml version='1.0' ?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope"> 
 <env:Header>
 </env:Header>
 <env:Body>
  <cms:addArticle xmlns:cms="http://www.daily-moon.com/cms">
    <cms:category xsi:type="xsd:string">classifieds</category>
    <cms:subcategory xsi:type="xsd:string">forsale
</cms:subcategory>
    <cms:articleHeadline xsi:type="xsd:string" />
    <cms:articleText xsi:type="xsd:string">Vintage 1963 
T-Bird.  Less than 300 miles.  Driven by my daughter until
I took it away.  Serious inquires only.  555-3264 after 7 
PM.</cms:articleText>
  </cms:addArticle>
 </env:Body>
</env:Envelope>

第二个样式称为 document/literal 样式,即将相应的数据直接添加到消息中,如清单 14 中所示。

清单 14. Document/literal 样式示例
<?xml version='1.0' ?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope"> 
 <env:Header>
 </env:Header>
 <env:Body>
    <category>classifieds</category>
    <subcategory>forsale</subcategory>
    <articleHeadline></articleHeadline>
    <articleText>Vintage 1963 T-Bird.  Less than 300 miles.
  Driven by my daughter until I took it away.  Serious 
inquires only.  555-3264 after 7 PM.</articleText>
</env:Body>
</env:Envelope>

在这种情况下,消息本身并不包含有关数据所提交到的进程的信息,此工作由路由软件进行。例如,所有对特定 URL 或端点的调用都可能指向特定的操作。另外,从技术上讲,可以使用 document/encoded 样式,但目前还没有人这样做,因此可以将其忽略。

在每个样式中都涉及到不同的折衷,本系列的第 2 部分将进一步对此进行详细讨论。不过,务必知道还有第三种样式“document wrapped”,并未正式地确定此样式,但由于各种互操作性原因而大受欢迎。在此情况下,有效负载的内容包装为单个元素,但元素并不据数据所属的过程或函数进行命名。从肉眼来看,这些消息几乎与 RPC/literal 消息完全相同。

消息交换模式

谈到发送消息,您有很多选择,可以发送请求并等待响应,发送请求但不等待响应,发送请求并在到达最终的目的地前通过多个中间层。但就实质而言,只有两个选择:

  • 请求/响应:在请求/响应模式种,以 SOAP 消息的形式发送请求,然后直接等待发送回响应。请求可以为同步的,也可以是异步的。
  • 单向消息传递:这种情况也称为“Fire and Forget”方法,发送请求但并不等待响应。可以在仅传递信息时或并不关心接收者对此如何响应时使用此方法。

现在,请注意并没有使用术语“客户机”和“服务器”。之所以这样,是因为这些消息交换模式几乎可以用于创建与上面提到的方法类似的任意数量的不同备选方法。例如,可以发送一条请求,然后依靠接收者对其进行处理,并在将来完成应完成的工作时发送一条消息。为此,将使用多个单向消息的组合,因此谈“客户机”和“服务器”并不合理,因为每个消息都有其接收方和发送方,所谓的客户机和服务器的位置会发生对换。


构建 SOAP 客户机

上面已经对理论进行了介绍,现在让我们开始构建实际的实现。首先从客户机开始。

以前的老方式

最初出现用于使用 SOAP 消息的 Java API 时,其用途非常特定化。它们的用途相当直接,用于创建 SOAP 消息。需要创建消息、EnvelopeHeaderBody 等等。例如,可以构建“旧式”客户机来访问前面安装的 MyService 服务的 echo 函数(请参见清单 15)。

注意:为了编译并运行此客户机,将需要 SAAJ 实现(如原始 Axis 软件)。可以从 http://ws.apache.org/axis/ 下载 Axis。据说 Axis2 0.95 也包含此实现,但本教程未针对其进行测试。

清单 15. 旧式 SOAP 客户机
import javax.xml.SOAP.*; 
import javax.xml.transform.*;
import java.io.FileInputStream;
import javax.xml.transform.stream.*;
import org.w3c.dom.*;

public class SendSOAP {
    
   public static void main(String args[]) {
        
      try {
      
         MessageFactory messageFactory = MessageFactory.newInstance();
         SOAPMessage message = messageFactory.createMessage();
         
         //Create objects for the message parts            
         SOAPPart SOAPPart = message.getSOAPPart();
         SOAPEnvelope envelope = SOAPPart.getEnvelope();
         SOAPBody body = envelope.getBody();

         SOAPElement bodyElement = 
           body.addChildElement(envelope.createName("echo", 
            "req", "http://localhost:8080/axis2/services/MyService/"));
         bodyElement.addChildElement("category")
                             .addTextNode("classifieds");
         message.saveChanges();

         SOAPPart SOAPpartbefore = message.getSOAPPart();
         SOAPEnvelope reqenv = SOAPpartbefore.getEnvelope();

         System.out.println("REQUEST:");
         System.out.println(reqenv.toString());

         //Now create the connection
         SOAPConnectionFactory SOAPConnFactory = 
                           SOAPConnectionFactory.newInstance();
         SOAPConnection connection = 
                           SOAPConnFactory.createConnection();
         
         SOAPMessage reply = connection.call(message, 
                   "http://localhost:8080/axis2/services/MyService");

         SOAPPart SOAPpart = reply.getSOAPPart();
         SOAPEnvelope replyenv = SOAPpart.getEnvelope();

         System.out.println("\nRESPONSE:");
         System.out.println(replyenv.toString());

         connection.close();
     
     } catch (Exception e){
         System.out.println(e.getMessage());
     }
  }
}

请注意,您要直接创建 SOAPEnvelopeSOAPBody 等内容。可以向消息体添加 echocategory 等元素。将从其中创建连接,进行调用,同样也能遍历 SOAP 消息的结构来获取实际的内容。如果要运行此客户机,应该看到与清单 16 中所示类似的响应。

清单 16. 当时的客户机
REQUEST:
<SOAPenv:Envelope xmlns:SOAPenv=
       "http://schemas.xmlSOAP.org/SOAP/envelope/" 
     xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <SOAPenv:Body>
  <req:echo xmlns:req=
         "http://localhost:8080/axis2/services/MyService/">
     <req:category>classifieds</req:category>
  </req:echo>
 </SOAPenv:Body>
</SOAPenv:Envelope>

RESPONSE:
<SOAPenv:Envelope xmlns:SOAPenv=
      "http://schemas.xmlSOAP.org/SOAP/envelope/" xmlns:wsa=
      "http://schemas.xmlSOAP.org/ws/2004/08/addressing">
 <SOAPenv:Header>
  <wsa:ReplyTo>
   <wsa:Address>
http://schemas.xmlSOAP.org/ws/2004/08/addressing/role/anonymous
   </wsa:Address>
  </wsa:ReplyTo>
  <wsa:From>
   <wsa:Address>
http://localhost:8080/axis2/services/MyService</wsa:Address>
  </wsa:From>
  <wsa:MessageID>ECE5B3F187F29D28BC11433905662036</wsa:MessageID>
 </SOAPenv:Header>
 <SOAPenv:Body>
  <req:echo xmlns:req=
         "http://localhost:8080/axis2/services/MyService/">
     <req:category>classifieds</req:category>
  </req:echo>
 </SOAPenv:Body>
</SOAPenv:Envelope>

echo 服务所进行的所有工作实际就是对其所接收到的请求进行应答,通过这一点可以很好地了解旧式处理方法与新式处理方法间的差别。接下来让我们看看二者的差异。

新方式

现在越来越趋向于对程序员隐藏使用基于 XML 的 Web 服务消息的复杂性。为此进行了大量的工作,其中大部分的目标都是希望尽可能让 Web 服务编程与任何其他体系结构的编程工作一样进行。

在 Axis2 中,实际上并不仅限于此。Axis2 引入了一种全新的方式来使用表示 SOAP 消息的 XML(尽管表面看来与使用文档对象模型类似)。AxIs 对象模型(Axis Object Model,AXIOM)进行了一系列更改,但我暂时将仅提一下其对消息的信息集的关注,消息集是元素和属性中包含的实际信息,而不是通常看到的序列化版本。不过,更为重要的是,Axis2 将为您处理 SOAP 信封,从而可以将精力放在构建有效负载上(或者,如果是实际的服务,则是分析有效负载和创建响应)。接下来让我们看看如何进行此工作。

创建请求

要开始创建客户机,请确保 Axis2 lib 目录中的所有 *.jar 文件——指 Standard 分发版,而不是 War 分发版——都在您的 CLASSPATH 上,并创建名为 ClassifiedClient 的新类。创建有效负载,如清单 17 中所示。

17. 有效负载
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.client.Options;
import org.apache.axis2.client.ServiceClient;
import org.apache.axis2.om.OMElement;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import java.io.StringWriter;
import org.apache.axis2.om.OMAbstractFactory;
import org.apache.axis2.SOAP.SOAPFactory;
import org.apache.axis2.om.OMFactory;
import org.apache.axis2.om.OMNamespace;

public class ClassifiedClient {

    public static OMElement getEchoOMElement() {
        SOAPFactory fac = OMAbstractFactory.getSOAP12Factory();
        OMNamespace omNs = fac.createOMNamespace(
                "http://daily-moon.com/cms", "cms");
        OMElement method = fac.createOMElement("echo", omNs);
        OMElement value = fac.createOMElement("category", omNs);
        value.addChild(fac.createText(value, "classifieds"));
        method.addChild(value);

        return method;
    }

    public static void main(String[] args) {
        try {
            OMElement payload = ClassifiedClient.getEchoOMElement();
        } catch (Exception e) { //(XMLStreamException e) {
            System.out.println(e.toString());
        }
    }
}

首先,创建工厂和命名空间,并使用其创建各个元素。在本例中,您将创建与前面示例完全相同的元素,因为将再次使用此客户机访问 echo 函数。(稍后将对其进行更改,以访问真正的服务。)

接下来,将创建请求。

创建请求

下一步将创建实际的请求。同样,这方面也体现出技术随时间的发展。这里不会直接将请求发送到 URL,而要设置“端点引用”,如清单 18 中所示。

清单 18. 请求中的端点引用
...
public class ClassifiedClient {
    private static EndpointReference targetEPR = new
        EndpointReference(
           "http://localhost:8080/axis2/services/MyService");

    public static OMElement getEchoOMElement() {
...
    }

    public static void main(String[] args) {
        try {
            OMElement payload = ClassifiedClient.getEchoOMElement();
            Options options = new Options();
            options.setTo(targetEPR);
            options.setTransportInProtocol(Constants.TRANSPORT_HTTP);

        } catch (Exception e) { //(XMLStreamException e) {
            System.out.println(e.toString());
        }
    }
}

对 WS-Addressing 的全面讨论不在本教程的范畴之内,但完全可以说端点引用包含消息将定向到的 URL,还可以有选择地包含其他信息,如应答地址和其他资源属性等。

首先,为请求创建 Options,以便为请求设置 EPR 和其他信息(如打算使用的传输协议)。完成了这些后,就可以实际发送请求了。

发送请求

完成了设置后,就可以发送请求了。对于 Axis 的 0.94 版,首选的方式是通过 ServiceClient 类发送消息,如清单 19 中所示。

清单 19. 发送请求
...
    public static void main(String[] args) {
        try {
            OMElement payload = ClassifiedClient.getEchoOMElement();
            Options options = new Options();
            options.setTo(targetEPR);
            options.setTransportInProtocol(Constants.TRANSPORT_HTTP);

            ServiceClient sender = new ServiceClient();
            sender.setOptions(options);
            OMElement result = sender.sendReceive(payload);

        } catch (Exception e) { //(XMLStreamException e) {
            System.out.println(e.toString());
        }
    }
}

将创建 ServiceClient 对象,并为其设置前面创建的 Options。然后就可以直接发送消息了。由于希望得到响应,因此将使用 sendReceive() 方法(该方法用于 in/out 类型的消息)。将在本教程的单向服务部分讨论单向消息的信息。sendReceive() 方法将在客户机接收到实际响应时将其返回。

输出结果

实际上,sendReceive() 并不会返回整个响应,而仅返回有效负载。如果将结果输出到命令行,应该清楚地看到清单 20 中的内容。

清单 20. sendReceive 输出
...
            sender.setOptions(options);
            OMElement result = sender.sendReceive(payload);

            System.out.println(result.toString());

        } catch (Exception e) { //(XMLStreamException e) {
            System.out.println(e.toString());
        }
    }
}

运行此客户机将获得清单 21 中所示的响应。

清单 21. sendReceive 响应
<cms:echo xmlns:cms="http://daily-moon.com/cms"><cms:catego
ry>classifieds</cms:category></cms:echo>

当然,接收到此数据后,可以使用其进行任何工作。接下来,我们将构建实际的 getNumberofArticles() 服务。


构建 SOAP 服务

如果您觉得构建 Web 服务客户机的过程相当简单,事实的确如此。而就很多方面而言,构建服务的过程也同样简单。

总体过程

创建 Axis2 Web 服务的整个过程涉及以下步骤:

  1. 创建服务清单
  2. 创建类
  3. 将其打包为 Axis 存档文件
  4. 将 Axis 存档文件上载到 Axis2 Web 应用程序
  5. 重新启动服务器(如果有必要)

这就是全部步骤。让我们首先从服务清单开始。

创建清单

服务清单告知 Axis2 应用程序(就更大的范围而言,应用服务器)哪个请求与哪个类对应。例如,可以如清单 22 中所示的那样指定两个服务函数。

清单 22. 在清单中指定两个服务函数
<service name="CMSService">
    <description>
        This is a sample Web Service for the newspaper's
 Content Managment System.
    </description>

    <parameter name="ServiceClass" locked="false"
>CMSService</parameter>

    <operation name="getNumberOfArticles">
        <messageReceiver class=
"org.apache.axis2.receivers.RawXMLINOutMessageReceiver"/>
    </operation>
    <operation name="addArticle">
        <messageReceiver class=
"org.apache.axis2.receivers.RawXMLINOnlyMessageReceiver"/>
    </operation>
</service>

首先,定义服务,提供其名称和描述,并指定实际为请求服务的类。接下来,定义实际的操作。请注意,此示例指定了两种不同类型的 messageReceiver。第一个 RawXMLINOutMessageReceiver 用于传统的请求/响应服务。第二个 RawXMLINOnlyMessageReceiver 用于单向消息。操作的名称与有效负载的根元素以及要执行的方法对应。

将此文件保存为 services.xml。

接下来,让我们创建实际的应用程序。

创建应用程序

让我们首先创建模拟前面看到的 echo 函数的类(将直接返回原始有效负载的副本),如清单 23 中所示。

清单 23. CMSService 类
import org.apache.axis2.om.OMElement;
import javax.xml.stream.XMLStreamException;

public class CMSService {

    public OMElement getNumberOfArticles(OMElement element)
                              throws XMLStreamException {

        element.build();
        element.detach();

        return element;
    }
}

要编译此应用程序,请确保 <axis2_home>/lib 中的所有 *.jar 文件都在您的 CLASSPATH 上。

此应用程序相当简单,仅包含一个与 getNumbereOfArticles 操作对应的类。此函数和任何要作为操作的函数一样,接收单个 OMElement 参数(表示有效负载)。此处,您将首先使用 build() 方法来确定已接收到所有数据——AXIOM 使用一个 pull 方法访问数据——然后将元素从其当前树中分离,以便能够将其返回。

如果喜欢冒险,可以自由地部署服务访问服务,以访问服务并查看结果。应该看到与清单 24 中所示类似的结果输出。

清单 24. CMSService 类响应
<cms:getNumberOfArticles><cms:category>classifieds</cms:category></cms:
getNumberOfArticles>

接下来让我们了解如何实际处理数据。

提取有效负载

为了从有效负载提取信息,将使用与 DOM 非常类似的技术来对接收到的有效负载元素进行操作(请参见清单 25)。

清单 25. 提取有效负载信息
...
import javax.xml.stream.XMLStreamException;

public class CMSService {
    public OMElement getNumberOfArticles(OMElement element) 
                          throws XMLStreamException {
        element.build();
        element.detach();

        String rootName = element.getLocalName();
        OMElement categoryElement = element.getFirstElement();
        String categoryElementName = categoryElement.getLocalName();
        String categoryValue = childElement.getText();

        return element;
    }
}

请记住,有效负载的根是 getNumberOfArticles 函数接收的元素。在此情况下,将提取元素的名称,然后移动到第一个元素子项(与第一个子项不同,后者可能是空格文本节点)并提取其名称和值。请注意,使用的是 getText() 方法来提取实际上是 category 元素的文本节点子项的值。这无疑非常简捷!

创建并返回响应

最后,将需要使用从请求的有效负载提取数据来创建响应。在本例中,将从第二个函数(在实际应用程序中,该函数将进行一些其他的工作)提供响应(请参见清单 26)。

清单 26. 创建响应
...
import javax.xml.stream.XMLStreamException;

public class CMSService {
    public OMElement getNumberOfArticles(OMElement element) 
                         throws XMLStreamException {
        element.build();
        element.detach();

        String rootName = element.getLocalName();
        OMElement childElement = element.getFirstElement();
        String childName = childElement.getLocalName();
        String categoryValue = childElement.getText();

        SOAPFactory factory = OMAbstractFactory.getSOAP12Factory();
        OMNamespace namespace = factory.createOMNamespace(
                            "http://daily-moon.com/cms/", "resp");
        OMElement resultElem = factory.createOMElement(
                                   "numberOfArticles",namespace);

        String actualValue = 
                         (articleCount(categoryValue)).toString();
        resultElem.setText(actualValue);

        return resultElem;
    }

    private Integer articleCount(String catId){

       //Perform some function such as searching the CMS 
       //database, and return the actual value.  For our 
       //purposes, you'll hardcode it.
       return new Integer(42);

    }
}

首先,创建将用于创建所有其他对象的工厂,然后创建将添加到响应的有效负载的命名空间。接下来,创建实际结果元素,在本例中为名为 numberOfArticles 的元素。

numberOfArticles 元素的内容将为 articleCount() 函数返回的一个数字,在本例中,该函数可以为任何内容。在实际的应用程序中,将进行所需进行的任何工作来获取此数据。获取了此数据后,会将其设置为 numberOfArticles 元素的内容,并直接返回该元素。

现在剩下的就是部署服务了。

部署服务

为了部署服务,需要创建一个 Axis 存档文件。此文件和 *.jar 或 *.war 文件类似,实际是使用特殊文件扩展名(在本例中使用的是 .aar)的 zip 文件。请按照以下步骤创建此文件:

  1. 将 <AXIS2_HOME>/lib 目录中的所有文件添加到 CLASSPATH 并编译 CMSService.java 文件。
  2. 在与 CMSService.class 文件相同的目录中创建名为 META-INF 的新目录。
  3. 从包含 CMSService.class 文件的目录中发出以下命令:<code type="section" width="100"> jar cvf CMSService.aar ./* </code> 应该看到与以下类似的结果:<code type="section" width="100"> added manifest adding:CMSService.class(in = 513) (out= 330)(deflated 35%) adding:CMSService.java(in = 328) (out= 182)(deflated 44%) ignoring entry META-INF/ adding:META-INF/services.xml(in = 391) (out= 229)(deflated 41%) </code>
  4. 使用安装示例服务中列出的步骤将此服务添加到服务器上。(如果看到 Web 接口上有 Servlet 错误,请确保登录到了 Axis2 应用程序。如果会话已过期,应用程序将不一定会通知您,而可能会直接显示错误。)
  5. 如果有必要,请重新启动 Geronimo。(将可能不必在添加服务后进行此操作,但在进行更改后可能必须这样做。)

如果单击 View services 链接,应该看到与图 4 中所示类似的内容。

图 4. 可用服务
可用服务

访问服务

现在已经完成了服务构建,接下来要通过客户机对其进行访问。对前面创建的 ClassifiedClient.java 文件进行以下更改(请参见清单 27)。

清单 27. 修改 ClassifiedClient
...
public class ClassifiedClient {
    private static EndpointReference targetEPR = 
         new EndpointReference(
           "http://localhost:8080/axis2/services/CMSService");

    public static OMElement getEchoOMElement() {
        SOAPFactory fac = OMAbstractFactory.getSOAP12Factory();
        OMNamespace omNs = fac.createOMNamespace(
                "http://daily-moon.com/cms", "cms");
        OMElement method = fac.createOMElement("getNumberOfArticles", omNs);
        OMElement value = fac.createOMElement("category", omNs);
        value.addChild(fac.createText(value, "classifieds"));
        method.addChild(value);

        return method;
    }

    public static void main(String[] args) {
        try {
            OMElement payload = ClassifiedClient.getEchoOMElement();
            Options options = new Options();
            options.setTo(targetEPR);
            options.setTransportInProtocol(Constants.TRANSPORT_HTTP);

            ServiceClient sender = new ServiceClient();
            sender.setOptions(options);
            OMElement result = sender.sendReceive(payload);

            String response = result.getText();
            System.out.println("There are "+response+" classifieds at the moment.");

        } catch (Exception e) { //(XMLStreamException e) {
System.out.println(e.toString());
        }
    }
}

编译并运行了此应用程序后,应看到清单 28 中所示的响应。

清单 28. ClassifiedClient 响应
There are 42 classifieds at the moment.

单向服务

继续讨论之前,让我们了解一下处理单向服务(而非请求/响应服务)时涉及到的不同之处。

服务

创建单向服务非常简单。此过程与创建请求/响应服务完全类似,至少不会实际返回任何内容。例如,可以为 CMSService 类创建 addArticle 操作,如图 29 中所示。

清单 29. CMSServiceclass 中的 addArticle 操作
...

    private Integer articleCount(String catId){
...
    }

    public void addArticle(OMElement element) 
                             throws XMLStreamException{
       element.build();
       System.out.println(element);
    }
}

在 services.xml 文件中,将 addArticle 操作指定为“in only”操作,因此不会等待返回任何内容,但即使这样,也能看到会实际发生一些事项,会在命令行输出接收到的有效负载。您将在 Geronimo 窗口中看到此信息。

在实际应用程序中,此方法将从有效负载提取信息,并会实际添加到某种类型的数据库或其他存储库。

客户机

此服务的客户机也与请求/响应服务所使用的服务类似(请参见清单 30)。

清单 30. 创建客户机
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.client.Options;
import org.apache.axis2.client.ServiceClient;
import org.apache.axis2.om.OMElement;
import org.apache.axis2.SOAP.SOAPFactory;
import org.apache.axis2.om.OMAbstractFactory;
import org.apache.axis2.om.OMNamespace;

public class AddArticleClient {
    private static EndpointReference targetEPR = 
         new EndpointReference(
             "http://localhost:8080/axis2/services/CMSService");

    private static OMElement getOMElement(){

        SOAPFactory fac = OMAbstractFactory.getSOAP12Factory();
        OMNamespace omNs = fac.createOMNamespace(
                "http://daily-moon.com", "cms");
        OMElement method = fac.createOMElement("addArticle", omNs);

        OMElement category = fac.createOMElement("category", omNs);
        category.setText("classifieds");

        OMElement subcategory = 
                        fac.createOMElement("subcategory", omNs);
        category.setText("wantads");

        OMElement adtext = fac.createOMElement("article", omNs);
        adtext.setText("Do you  have good head for numbers"+
              " and a great deal of patience?  Do you like"+
              " to sit for hours sorting objects by their"+
              " size?  If so, then you could be the"+
              " next goober counter in the world famous"+
              " Murphy Brothers peanut factory. "+
              " Willingness to dress up as our mascot"+
              " helpful, but not required.");

        method.addChild(category);
        method.addChild(subcategory);
        method.addChild(adtext); 

        return method;

    }

    public static void main(String[] args) {
        try {
            OMElement payload = AddArticleClient.getOMElement();
            ServiceClient serviceClient = new ServiceClient();

            Options options = new Options();
            serviceClient.setOptions(options);
            options.setTo(targetEPR);

            serviceClient.fireAndForget(payload);

        } catch (AxisFault axisFault) {
            axisFault.printStackTrace();
        }
    }

}

尽管有效负载不同,但正如您在 getOMElement() 方法中看到的,编程方面目前的唯一真正的更改是使用 fireAndForget() 方法替代 sendReceive() 方法。此方法并不会返回响应。

如果运行此客户机,应该在 Geronimo 窗口中看到与图 5 中所示类似的输出。

图 5. 命令行输出
命令行输出

通过 GET 访问服务

在 SOAP 1.2 推出之前,使用 HTTP 访问基于 SOAP 的 Web 服务的唯一方法是使用 POST 请求。您将需要创建能创建 POST 请求并使用 SOAP 消息作为请求的内容的客户机。不过,SOAP 1.2 定义了使用 GET 请求访问基于 SOA 的 Web 服务的方法。

GET 与 POST 对比

继续我们的讨论之前,务必了解通过 HTTP 的 GETPOST 请求的区别。尽管很多 Web 程序员所进行的处理似乎表明二者之间是可以互换的,但实际上二者的用途并不相同。GET 中的所有关于所请求的资源的信息都包含在 URL(通常作为参数),仅用于等幂请求。这些请求是没有“副作用”的请求。也就是说,应该能够数十次、数百次、数千次地调用这个请求,但这个请求不会更改任何东西。例如,请求 Albuquerque 的当前气温的 Web 请求就是等幂请求。而将注释传入到博客数据库的 Web 请求则不是。

这是因为 GET 请求可以添加到用户的书签,能在不会引发警告的情况下进行访问。还可以对其进行引用,而不会引发警告。另一方面,POST 请求将其信息包含在请求的正文中,因此很难进行随机的重复。

就 SOAP 而言,这意味着应该能够对仅检索信息而不进行更改的 SOAP 请求使用 GET。对于进行更改的任何操作,仍然都应使用 POST

访问服务

在 Axis2 中,可以生成 GET 请求,服务器会将其转换为 SOAP 消息,然后将有效负载作为结果返回。例如,请将浏览器指向清单 31 中所示的位置。

清单 31. 访问服务
http://localhost:8080/axis2/services/CMSService/getNumberOfArticles?category=classifieds

如果使用 0.94 版,将看到清单 32 中所示的响应。

清单 31. SOAP 有效负载响应
<resp:numberOfArcticles>42</resp:numberOfArcticles>

不过,这并不十分准确。根据 SOAP 1.2 建议规范,应该能够看到整个 SOAP 响应。这在 Axis2 将来的版本中可能会发生更改。


处理附件

简单 SOAP 消息的另一个变体是附件。对于附件,多年来人们早已耳熟能详,但由于现在某些扩展规范要求使用附件,因此您必须对其进行处理。

二进制数据和 XML

尽管 XML 是基于文本的格式,但却不能忽略实际上是采用二进制进行表示的。因为这样,将会有需要向 Web 服务传递或从其检索二进制信息的情况。

可以采用两种方式中的一种来处理这种情况。第一种选择是将二进制数据实际包含在您的文档中。这种情况的一个例子是将 Microsoft Word 文档另存为 XML 文件时。如果在该文档中嵌入了任何图形,Word 会将其作为二进制数据嵌入到 XML 文档中(采用 Base64 编码)。第二种选择是直接引用该数据,以便处理该文档的应用程序能够找到此数据。一个极为常见的例子是 Web 浏览器以及其处理从 XHTML 文件引用的图像的方式。XHTML 文本包含一个 img 元素(或者,采用了更为先进的技术,则为 object 元素),该元素包含一个 src 属性,其中有指向实际数据的 URL。应用程序可以随后从该位置加载数据并相应地进行使用。

SOAP 文档也是这样。假如,如果向基于 SOAP 的服务提交了一个图像,有两个选择。可以将该数据嵌入在有效负载中,或可以想办法引用该数据。曾经由于涉及到带宽的一些问题对此进行过讨论。

XML 二进制优化打包

XML 已经比二进制对应项冗长得多了。正因为如此,它将使用更多的带宽。那么,当考虑使用向 XML 文本文档添加二进制数据时的首选方法(将其编码为 Base64)时,会由于两个或更多的因素而导致其尺寸增大,这就带来了一个非常实际的问题。

事实上,在过去的两三年,曾经有很多人强烈地批评缺乏对二进制数据的实时支持,几乎充斥着不满的声音,最终 W3C 开始着手处理这个问题。其工作的成果就是 XML 二进制优化打包(XML-binary Optimized Packages,XOP)。此协议提供了在 XML 文档中可靠地引用外部数据的方法。例如,SOAP with Attachments 规范规定二进制数据可以作为多部分 MIME 文档的的一部分发送,由 XML 数据组成第一部分,而二进制数据作为附加部分添加到其中。这样做的问题在于,尽管您的程序可能知道数据存在,但文档并不知道这一点。同时,还不允许对文档进行选择性优化或对包含二进制数据的现有文档进行回溯处理。

XOP 通过提供一个特殊的机制来改进这种情况,利用这种机制可选择性地提取要优化的信息,将其添加到多部分 MIME 消息中(其中也包括您的 SOAP 消息)并显式地对其进行引用。让我们看一个例子。

例如,假定员工不想将新文章作为文本元素添加,而希望将其作为二进制文档从字处理程序添加。如果将该内容包含在消息体中,将十分混乱,如清单 33 中所示:_

清单 33. 添加二进制文档
<?xml version='1.0' ?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope"> 
 <env:Header>
 </env:Header>
 <env:Body>
  <cms:addArticle xmlns:cms="http://www.daily-moon.com/cms">
    <cms:category>classifieds</category>
    <cms:subcategory>forsale
</cms:subcategory>
    <cms:articleHeadline><cms:articleHeadline>
    <cms:articleText>wvetwenptiubnweoirntuwopeirt4m[456n09ew7nv
sa0tv043u6y304o5mu60ew9rebtm45bm4-69nw-0er9utnv3094nb-26204
95u6-49kv6-m34956h-wb09emjb-0n67u-340v,=qw-enr7w8b64b03278-
ANDLOTSMOREBASE64ENCODEDDATAHERE</cms:articleText>
  </cms:addArticle>
 </env:Body>
</env:Envelope>

相反,XOP 规定对数据进行提取,然后使用一个引用其新位置的 Include 元素将其替换,如清单 34 中所示。

清单 34. 使用 XOP
MIME-Version: 1.0
Content-Type: Multipart/Related;boundary=MIME_boundary;
    type="application/xop+xml";
    start="<soapmsg.xml@daily-moon.com>";
    start-info="text/xml"
Content-Description: An XML document with binary data in it

--MIME_boundary
Content-Type: application/xop+xml; 
    charset=UTF-8; 
    type="text/xml"
Content-Transfer-Encoding: 8bit
Content-ID: <soapmsg.xml@daily-moon.com>

<env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope"> 
 <env:Header>
 </env:Header>
 <env:Body>
  <cms:addArticle xmlns:cms="http://www.daily-moon.com/cms">
    <cms:category>classifieds</category>
    <cms:subcategory>forsale
</cms:subcategory>
    <cms:articleHeadline><cms:articleHeadline>
    <cms:articleText><xop:Include 
  xmlns:xop='http://www.w3.org/2004/08/xop/include' 
  href='cid:http://daily-moon.com/tbird.doc'
/></cms:articleText>
  </cms:addArticle>
 </env:Body>
</env:Envelope>

--MIME_boundary
Content-Type: application/vnd.oasis.openoffice
Content-Transfer-Encoding: binary
Content-ID: <http://daily-moon.com/tbird.doc>

// binary octets for the word processing file

--MIME_boundary--

请注意,Include 元素中指定的位置与 Content-ID 减去协议 cid: 的值匹配。现在要发送的是此消息,而不是纯文本 SOAP 消息。

SOAP、二进制数据和 Axis2

在 SOAP 文档中使用 XOP 的过程称为 MTOM(即 SOAP 消息传输优化机制——Message Transmission Optimization Mechanism)。Axis2 提供了使用 SOA 数据的这个方法的支持,但必须确保对应用程序进行了恰当配置。

具体来说,您必须在 axis2.war 文件内的 axis2.xml 文件中启用此支持(请参见清单 35)。

清单 35. 将 XOP 与 Axis2 一起使用
<axisconfig name="AxisJava2.0">
    <!-- ================================================= -->
    <!-- Parameters -->
    <!-- ================================================= -->
    <parameter name="hotdeployment" locked="false">true</parameter>
    <parameter name="hotupdate" locked="false">true</parameter>
    <parameter name="enableMTOM" locked="false">true</parameter>
 
    <!-- Uncomment this to enable REST support -->
    <!--    <parameter name="enableREST" locked="false">true</parameter>-->

    <parameter name="userName" locked="false">admin</parameter>
    <parameter name="password" locked="false">axis2</parameter>
...

如果有必要,可以提取 axis2.war 文件,进行此更改,然后将其重新压缩成 .war 文件。

要替换 Axis2 应用程序,请使用清单 36 中所示的 URL 访问 Geronimo 控制台。

清单 36. Geronimo 控制台
http://localhost:8080/console

作为 system/manager 登录,并单击 Application>Web App WARs,然后卸载并重新安装 Axis2 应用程序。(请记住,执行此步骤后,必须重新加载 Web 服务。)

以编程方式使用 MTOM 不在本教程的讨论范围之内,但可以在参考资料部分获取有关此主题的更多信息。只是要注意,在 Axis2 的 0.95 版之前的版本上可能不会按照预期工作,因为该版本中包含了 SOAP with Attachments API for Java (SAAJ) 实现。


结束语

在系统间的互操作性非常重要的当今世界,可将 Web 服务视为面向服务的体系结构的基础。而 SOAP 则是企业级 Web 服务的基础。本教程介绍了 Web 服务的基础知识,并对了解和在自己的应用程序中使用 SOAP 所必需的概念和编程知识进行了说明。在本教程中,您了解了以下内容:

  • 有关 Web 服务的重要概念
  • 如何安装和使用 Geronimo 应用服务器
  • 如何安装和使用 Axis2 Web 服务应用程序
  • 如何创建客户机来访问 SOAP 服务
  • 如何创建 SOAP 服务
  • 有关 SOAP 服务的其他问题,如 GET 请求和附件

在本系列的第 2 将讨论 Web 服务描述语言,您将从中学习如何使用 WSDL 来实现在本文中执行的很多步骤的自动化,并提供更便于其他人访问您构建的服务的方法。


下载

描述名字大小
Source codews-understand-web-services1.zip12KB

参考资料

学习

获得产品和技术

  • 此处下载 Apache Geronimo。
  • 此处下载 Apache Axis2。本教程使用的是 0.94 版,但应该也能使用更高版本。
  • 此处下载 J2SE SDK。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=SOA and web services
ArticleID=158613
ArticleTitle=了解 Web 服务规范: 第 1 部分:SOAP
publish-date=09112006