基于SOAP协议的Web服务风格之比较
前面我们已经看到,SOAP可以在后台替我们完成那些比较困难的工作。但我们并没有看到服务器端和客户端交互的XML文档,实际上,我们可以利用一些TCP/IP监测工具,比如Apache的TCPMon工具来查看SOAP中传输的XML文档,其结果如代码清单16所示:
代码清单16 – SOAP中的XML请求文档
<?xml version="1.0" ?>
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:ns1="http://item.service.soap.soajava.packt.com/">
<soapenv:Body>
<ns1:insert>
<arg0>
<code>XY</code>
<description>xy desc</description>
<id>26</id>
</arg0>
</ns1:insert>
</soapenv:Body>
</soapenv:Envelope>
代码清单17 – SOAP中的XML应答文档
<?xml version="1.0" ?>
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<ns2:insertResponse
xmlns:ns2="http://item.service.soap.soajava.packt.com/">
<return>
<retCode>OK</retCode>
<retMessage>Item was inserted successfully</retMessage>
</return>
</ns2:insertResponse>
</S:Body>
</S:Envelope>
您可以看出,SOAP的XML消息以Envelope元素开始,包含一个必须的Body元素,但SOAP消息中的header元素是可选的。Body元素中包含的内容就是要传输消息的有效负载。然而,我们在使用SOAP时会有多种选择,就前面的例子而言,我们为了简明起见,只采用了SOAP的缺省设置。您有可能听说过SOAP的绑定风格(Binding style,其值为RPC或Document风格)、使用(use,其值为Encoded或Literal)或参数风格(parameter style,其值为Bare或Wrapped)。在本节中,我们将探讨一下这些概念,重点介绍一下SOAP的绑定风格。
在解释这些概念之前,我们先花点时间解释一下WS-I这个概念,它是Web Service-Interoperability(Web服务的可互操作性)的缩写,它代表了一整套的标准,这些标准组合起来可以让我们在异种环境中(比如.Net和Java)进行数据交换。绑定风格、使用和参数风格可以任意组合,但只有以下的组合和WS-I兼容,所以它们是我们讨论的重点:
(1) RPC/Literal
(2) Document/Literal(bare或unwrapped)
(3) Document/Literal wrapped
WS-I规定,use属性值不能等于”encoded(已编码的)”。如果use属性值等于encoded,数据将按照SOAP编码规范进行编码,但要验证经过SOAP编码的消息是否违反WSDL描述将非常困难,而验证工作又是可互操作性的基础,所以WS-I不对数据进行编码,它只传输literal(不经编码的)的数据。
SOAP服务的可选架构一 : RPC/Literal
SOAP服务的第一个可选架构为RPC/Literal,它决定我们是否要在Web服务中使用RPC还是要使用Document作为SOAP的绑定风格。RPC(远程过程调用)是远程调用的一种通用机制,驻留在一台计算机(或虚拟机)上的程序可被运行在远程机器(或虚拟机)上的程序调用,这种方法已经使用了几十年。RPC可由多种技术实现,其中比较流行的有CORBA、DCOM和RMI。实现技术虽然不同,但RPC调用都具有以下特点:
(1) 需要有远程地址
(2) 需要调用的方法(或操作)名
(3) 需要一系列参数
(4) 需要同步应答
请注意,除了上面的第一个特点,RPC调用与本机调用几乎相同。那么,古老的RPC方法和SOAP服务又有什么关系呢?其实它们之间关系非同一般,SOAP在其早期定义(尚未发布之前)中,其设计只支持RPC。从某种意义上说,当时的SOAP是当时各种分布式编程技术的集大成者,它对这些技术进行了标准化。
我们可以对前面的Web服务稍加修改,加些注解(annotation),我们就可以将它转化为RPC风格(JAX-WS的缺省风格为Document而非RPC),其实现代码如下:
代码清单18 – SOAP的RPC风格
@WebService
@SOAPBinding(style=SOAPBinding.Style.RPC)
public class ItemWs {
@WebMethod
public Outcome insert(@WebParam(name="itemParam") Item item,
@WebParam(name="categoryParam") String category)
{
//Insert item ...
您会看到,我们在方法中加了另一个参数category,这样我们就可以把某个商品插入到某一指定的商品集中,并且我们使用了@WebParam注解给方法中的参数命名。
现在我们来发布这个服务,我们只需要运行带有EndPoint.publish的那个主类即可,而不需要运行wsgen来生成服务器端的类。然后,请使用wsimport工具根据发布的WSDL生成客户端类,并导入到客户端项目中,这样,我们就可以通过下面的方式在客户端调用该RPC风格的SOAP服务。
Outcome outcome = itemWs.insert(item1, "A");
如果我们监测XML请求文档,我们将会看到如下结构:
代码清单19 –RPC风格的SOAP请求文档
<soapenv:Body>
<ans:insert xmlns:ans="http:// ... ">
<itemParam>
<code>XY</code>
<description>xy desc</description>
<id>26</id>
</itemParam>
<categoryParam>A</categoryParam>
</ans:insert>
</soapenv:Body>
我们可以从上面的请求文档中看到RPC的典型风格,即方法名加上一系列参数。其相应的WSDL(您可以使用TCP监测工具查看)文档如下所示:
代码清单20 –RPC风格的WSDL文档
<types>
<xsd:schema>
<xsd:import schemaLocation="http://127.0.0.1:8002/SoaBookSOAP_RPC_server/itemWs?xsd=1"
namespace="http://item.service.soap.soajava.packt.com/"></xsd:import>
</xsd:schema>
</types>
<message name="insert">
<part name="itemParam" type="tns:item"></part>
<part name="categoryParam" type="xsd:string"></part>
</message>
<message name="insertResponse">
<part name="return" type="tns:outcome"></part>
</message>
<portType name="ItemWs">
<operation name="insert" parameterOrder="itemParam categoryParam">
<input message="tns:insert"></input>
<output message="tns:insertResponse"></output>
</operation>
</portType>
<binding name="ItemWsPortBinding" type="tns:ItemWs">
<soap:binding style="rpc"
transport="http://schemas.xmlsoap.org/soap/http">
</soap:binding>
<operation name="insert">
<soap:operation soapAction=""></soap:operation>
<input>
<soap:body use="literal" namespace=
"http://item.service.soap.soajava.packt.com/">
</soap:body>
</input>
<output>
<soap:body use="literal" namespace=
"http://item.service.soap.soajava.packt.com/">
</soap:body>
</output>
</operation>
</binding>
<service name="ItemWsService">
<port name="ItemWsPort" binding="tns:ItemWsPortBinding">
<soap:address location=
"http://127.0.0.1:8002/SoaBookSOAP_RPC_server/itemWs">
</soap:address>
</port>
</service>
</definitions>
上面WSDL的XML Schema文档可以通过下面的URL地址来访问:
http://127.0.0.1:8001/SoaBookSOAP_RPC_server/itemWs?xsd=1
WSDL的XML Schema文档内容如下所示:
代码清单21 –RPC风格WSDL文档的XML Schema(XSD)文档
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:tns="http://item.service.soap.soajava.packt.com/" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://item.service.soap.soajava.packt.com/" version="1.0">
<xs:element name="Item" type="tns:item"></xs:element>
<xs:element name="Outcome" type="tns:outcome"></xs:element>
<xs:complexType name="item">
<xs:sequence>
<xs:element name="code" type="xs:string"
minOccurs="0"></xs:element>
<xs:element name="description" type="xs:string"
minOccurs="0"></xs:element>
<xs:element name="id" type="xs:int"></xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="outcome">
<xs:sequence>
<xs:element name="retCode" type="xs:string"
minOccurs="0"></xs:element>
<xs:element name="retMessage" type="xs:string"
minOccurs="0"></xs:element>
</xs:sequence>
</xs:complexType>
</xs:schema>
这里,我们需要注意的是,上面的XSD只定义了复杂类型元素,如本例中的Item和Outcome类型,但它没有办法提供有效的信息来验证简单数据类型的参数,也不能验证SOAP消息的其它部分。因此,采用RPC架构的主要问题是,如何才能验证要交换消息的XML文档符合XSD规范?
接下来我们看看Document风格的架构能否客服这一缺点。
SOAP服务的可选架构二 : Document/Literal
这种SOAP风格的实现也称之为”bare”或”unwrapped”(未组装的)的Document风格,为什么叫未组装的Document,我们马上会解释其中的含义。但现在请您注意,JAX-WS中的参数类型的缺省值是wrapped(已组装的),因此,为了使用bare类型的document风格,我们需要在源代码中显式声明:
代码清单22 –Document风格SOAP服务实现
@SOAPBinding(style=SOAPBinding.Style.DOCUMENT,
parameterStyle=SOAPBinding.ParameterStyle.BARE)
public class ItemWs {
@WebMethod
public Outcome insert(@WebParam(name="itemParam") Item item,
@WebParam(name="categoryParam") String category) {
...
如果您现在按正常的步骤在客户端调用上面的服务,您会在导入由WSDL产生的客户端类时发现下面的错误:
error: operation "insert": more than one part bound to body
这个错误有助于我们理解RPC和Document风格的SOAP实现方法之间的区别,我们已经了解,WS-I只允许SOAP消息体中只能包含一个XML根元素,这是为什么呢?
Document风格是SOAP实现的一种新方法,它不同于以往的方法。其表现在,服务要求的输入是”一个文档”,而不是一个带方法名及其相应参数的一个请求。这意味着在Document风格中,我们应该传入一个对象,这个对象将是Web服务的唯一输入。Document中会包含服务调用的所有有用信息,但不是方法名及其相应参数值的组合。这就是WS-I为什么只允许Document只能包含一个根元素的原因。
因此,为了调用上面的Document风格的服务,把商品插入到商品目录中,并保持和WS-I兼容,我们应重构服务的源代码。我们应该新建一个对新,比如ItemInsertRequest对象,这个对象将包装所有需要的信息(商品及其目录)。这就是为什么这种风格又称之为”bare”或”unwrapped”的Document风格的由来,它没有对XML消息的组成部分进行包装,我们必须手动的显式包装它们。
代码清单23 –Document风格的SOAP服务对请求进行组装
@XmlRootElement(name = "ItemInsertRequest")
public class ItemInsertRequest {
private Item item;
private String category;
...
而且,为了使Web服务只需要一个参数,我们还需要对Web服务的实现进行重构,其实现代码如下:
代码清单24 –Document风格的SOAP服务实现代码(使用了上面定义的封装类)
@WebMethod
public Outcome insert(@WebParam(name="itemInsertRequestParam")
ItemInsertRequest itemInsertRequest) {
...
此时,但客户端使用wsimport工具生成客户端需要的类时,就不会产生上面的错误,客户端最终调用服务的代码如下:
代码清单25 –Document风格的客户端调用代码
ItemInsertRequest req = new ItemInsertRequest();
req.setItem(item1);
req.setCategory("A");
Outcome outcome = itemWs.insert(req);
上面的代码执行后,它会想服务器发出SOAP请求,其请求消息体的内容如下:
代码清单26 –Document风格XML请求
<ns1:itemInsertRequestParam>
<category>A</category>
<item>
<code>XY</code>
<description>xy desc</description>
<id>26</id>
</item>
</ns1:itemInsertRequestParam>
请注意,上面Document风格的XML请求在结构上虽然和RPC风格相似(请参展代码清单19),但我们现在处理的是XML文档(Document),而不是对远程方法的调用。在Document文档中,商品及其目录只是文档的属性;但在RPC风格的方法调用中,它们是作为方法的参数传入的。
代码清单27–Document风格WSDL
<message name="insert">
<part element="tns:itemInsertRequestParam"
name="itemInsertRequestParam"></part>
</message>
...
<binding name="ItemWsPortBinding" type="tns:ItemWs">
<soap:binding style="document"
这时,上面WSDL附属的XSD就可以用来对整个WSDL文档进行验证。
代码清单28–Document风格WSDL附属的XSD文档
<xs:element name="Item" type="tns:item"></xs:element>
<xs:element name="ItemInsertRequest"
type="tns:itemInsertRequest"></xs:element>
<xs:element name="Outcome" type="tns:outcome"></xs:element>
<xs:element nillable="true" name="insertResponse"
type="tns:outcome"></xs:element>
<xs:element nillable="true" name="itemInsertRequestParam"
type="tns:itemInsertRequest"></xs:element>
<xs:complexType name="itemInsertRequest">
<xs:sequence>
<xs:element name="category" type="xs:string"
minOccurs="0"></xs:element>
<xs:element name="item" type="tns:item"
minOccurs="0"></xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="item">
<xs:sequence>
<xs:element name="code" type="xs:string"
minOccurs="0"></xs:element>
<xs:element name="description" type="xs:string"
minOccurs="0"></xs:element>
<xs:element name="id" type="xs:int"></xs:element>
</xs:sequence>
</xs:complexType>
Document/Literal风格的SOAP服务的主要优势在于,它能对整个XML交换消息进行验证。
但是,使用这种方法,我们会丢掉一些信息,如我们在SOAP消息中没有发现操作的名称;在某些情况下,比如在使用SMTP等异步TCP/IP协议来传输消息时,这可能会有所不足。因为此时消息的派发将变得非常困难,有时甚至不可能做到。
另外,这种方法的另一个缺点是,对那些已经开发完成的应用,我们得考虑花力气对它们进行改造,即对服务器端和客户端的代码进行重构。
SOAP服务的可选架构三 : Document/Literal Wrapped
Document/Literal Wrapped风格是JAX-WS的缺省风格,在我们第一个SOAP示例中,我们用的就是这种风格(请参考程序清单14)。因此,下面的注解其实是没用的。
代码清单29–Document Wrapped风格的服务实现
@SOAPBinding(style=SOAPBinding.Style.DOCUMENT,
parameterStyle=SOAPBinding.ParameterStyle.WRAPPED)
现在,我们在Document bare风格代码的基础上,初始化Web服务的结构。
代码清单30–Document Wrapped风格的服务实现
@WebMethod
public Outcome insert(@WebParam(name="itemParam") Item item,
@WebParam(name="categoryParam") String category) {
要在客户端调用该Web服务,按照正常的步骤进行即可(此时需要调用wsgen工具生成服务器端需要的类)。
现在,即使Web方法中带有类似于RPC风格的多个参数,在服务的调用过程中也不会产生错误。这是如何做到的呢?原来,它只是对方法名和参数进行简单的封装,将方法名和参数当成一个新的对象,新对象名和Web方法名正好一致。当然,这个封装过程是自动完成的。此时,SOAP消息体的内容如下:
代码清单31–Document Wrapped风格的XML请求
<ns1:insert>
<itemParam>
<code>XY</code>
<description>xy desc</description>
<id>26</id>
</itemParam>
<categoryParam>A</categoryParam>
</ns1:insert>
总之,Document/literal wrapped风格集成Document和RPC方法的优势,它具有以下优点:
(1) SOAP消息可以用XML Schema进行验证
(2) SOAP消息体只有一个子元素,因此它和WS-I兼容
(3) 如果Web方法带有多个参数,不需要修改代码就可发布这种类型的服务
(4) 操作的方法名包含在SOAP消息中