重新考虑“代码优先”的 Web 服务

阅读数:722 2007 年 9 月 24 日

话题:SOA架构

你开始开发 SOAP Web 服务了吗?如果是的话,那么你有两种开发风格可选择。第一种被称为“由 WSDL 开始(start-from-WSDL)”,或是“契约优先(contract first)”,牵涉构建一个 WSDL 服务描述,并直接关联用于数据交换的 XML 模式。第二种被称为“由代码开始(start-from-code)”,或是“代码优先(code first)”,牵涉将例子服务代码插入你选择的框架,并由那个代码产生 WSDL+ 模式(schema)。

不论使用哪种开发风格,最终目标都是相同的——你想要你的服务有一个稳定的 WSDL+ 模式定义。当你正工作在一个 SOA 环境时,这个目标尤其重要。SOA 要求服务松耦合,服务间的接口是固定的,且与实现相分离。XML Web 服务为实现 SOA 创建了一个重要的基础,这很大程度上是因为 WSDL 和模式可以让你以平台中立的方式去指定被一个 Web 服务使用的 XML 消息交换。如果 WSDL 和模式不是稳定的,服务就只能被由服务提供者直接或间接控制的客户端使用——这可不是 SOA。

“由代码开始”的问题

“由代码开始”开发 Web 服务的想法被许多 Web 服务和 SOA 领域的权威人士反对。他们觉得“由代码开始”将 XML 消息结构绑定到了一个特定的实现,这废弃了使用 WSDL 和模式的整个目的。对于“由代码开始”的最初形式——SOAP 编码模式(SOAP encoding scheme)——的确是这样,它被广泛使用以支持 rpc/encoded。使用 SOAP 编码,XML 模式直接由服务提供者应用数据结构产生,客户端代码使用这些产生的数据结构副本进行工作。这种数据模型和 XML 之间自动转换的特性使得 rpc/encoded 在早期的 SOAP 中流行——但是它也是这种风格后来被废止的一个重要原因。它意味着,每次你的服务数据结构的改变都会引起模式的改变,客户端将需要使用新的模式重新生成它们的代码。



图 1. “由代码开始”的 SOAP 编码方式

使用 SOAP 编码除了产生紧耦合,在 XML 数据表示和模式定义方面它也有一些缺点。SOAP 编码是用于对象图的 XML 序列化算法,以编程语言独立的方式定义。既然它是一个序列化算法,因此就最后产生的 XML 结构来说没有灵活性——你应用这个算法到你的数据结构上,你所得到的就是用于那些结构的 SOAP 编码。不幸的是,所使用的序列化规则导致了 XML 模式对于其他非 rpc/encoded 消息交换几乎没有任何用处(包括文档校验)。序列化格式同样还导致较差的效率,这是因为引用结构的花销、过多的运行时打印,以及为所有组件使用子元素(而不是在合适的地方使用属性)。

大多数这些问题适用于那些只是在数据结构和 XML 间序列化的技术。但是“由代码开始”并不意味着必须通过直接序列化来暴露数据模型。使用某种形式的数据绑定,当前的 Web 服务栈通常都支持数据模型和 XML 间的灵活转换。使用数据绑定,你可保持对数据的 XML 表示的控制。那种控制意味着你的模式定义至少可以与实际的数据模型间稍有隔离,并且你可以选择适合你数据的 XML 表示。使用数据绑定方法,大多数与 SOAP 编码有关的问题就不再会有了。

“由 WSDL 开始”的问题

与使用代码工作相比,使用“由 WSDL 开始”的最大问题就是使用 WSDL 和模式定义工作的麻烦本性。现代 IDE 一般都配备了“智能”编辑器和令代码变更容易的强大的重构工具。目前还没有用于 WSDL 和模式的等价工具。即便是最基础的模式重构,如转换局部定义到全局定义,也不被主流 WSDL 和模式工具所支持。

因为工具软弱,“由 WSDL 开始”还需要扎实理解 WSDL 和模式,这样才能获得良好的效果。如果可供开发者使用的工具没有立足于标准之上,那么最终的 WSDL 和模式常常是丑陋的大杂烩,它使得服务和数据的结构更加模糊,而不是更清晰。就 WSDL 部分而言,要有效地理解它还不太困难,但是模式这一部分就不同了。W3C XML 模式推荐(“模式”的全称)至少与大多数编程语言一样复杂,要精通它需要花很多工夫。拥有专门架构团队的大型组织可以负担得起雇用或培训模式专家,但是对于更小的组织而言,模式的复杂性是“由 WSDL 开始”的服务规格的真正障碍。

即使在开发出 WSDL 和模式定义的初始集合之后,这些“使用的方便性”问题仍然适用。服务综合集合的开发总是一个迭代过程,反复经过规格、原型和测试周期。在每一阶段,功能蹩脚工具所带来的工作不便都会是开发的障碍。

使“由代码开始”起作用

目前,针对“由代码开始”完成服务规格的可用工具已经远远超越令这种开发类型声名狼藉的 SOAP 编码模型。它们提供了灵活性和可扩展性,使得它能与相当复杂的数据结构一起工作。最重要的是,它们在代码定义的数据结构与相应的 XML 表示之间增加了一个解耦层。

微软的.NET 框架和 Sun 的 JAX-WS 2.0/JAXB 2.0 是两个流行的例子。两者都使用内嵌于源代码中的配置信息(在.Net 中是属性,在 JAX-WS/JAXB 中是标注)来控制数据结构与 XML 间的相互转换。由这种内嵌配置提供的控制是有限的,而且通常等于只需详列出不同于缺省序列化选择的差异。那意味着 XML 并不必需与数据结构的细节相隔离——例如,如果你给对象增加一个域,它将自动成为 XML 表示的一部分,除非你显式列出要包含的域——但是它要比一个纯粹的序列化方式要好得多。



图 2. “由代码开始”的.NET 和 JAX-WS 2.0/JAXB 2.0 方式

使用 JiBX 解耦

在将 XML 表示从应用数据模型解耦方面,笔者自己的用于 Java(它可用于使用 Apache Axis2、XFire 和 JiBX/WS 栈的 Web 服务)的 JiBX(http://www.jibx.org)数据绑定框架甚至走得更远。JiBX 使用与源代码分离的绑定定义,要求每个想包含到 XML 表示中的项目要显式地在绑定中命名。数据模型和 XML 表示之间的结构性区别可在绑定中处理,即使数据模型随着时间而改变,XML 表示通常也可以得到保护。JiBX 还允许多个绑定应用于相同的代码,允许使用单一数据模型支持许多模式类型的版本标定变更。

相关的 Jibx2Wsdl 工具(http://www.sosnoski.com/jibx-wiki/space/axis2-jibx/jibx2wsdl)示范了使用“由代码开始”方式的潜在好处。它产生 WSDL 和模式,以及对应的 JiBX 绑定定义,确保所有的元件匹配。它还从 Java 源代码中导出 JavaDoc 文档,放入所产生的 WSDL 和模式中,这样服务描述被全部文档化,而不需要任何手工编辑。Jibx2Wsdl 使用一个合理的缺省算法产生用于数据模型类的绑定,但是算法可以在任何层级上通过提供 XML 文档形式的自定义而被修改。这些自定义有和.NET 属性、JAX-WS/JAXB 标注相同的效果,且不需要内嵌在源代码中。



图 3. 由代码开始的 JiBX/Jibx2Wsdl 方式

在效果上,Jibx2Wsdl 将“由代码开始”分成两步。在产生步骤,你使用 Jibx2Wsdl 创建实际的 WSDL+ 模式定义,以及相应的 JiBX 绑定定义。在部署步骤,你使用 JiBX 将产生的绑定定义应用到你的 Java 类。

在初始开发中,可结合这两个步骤以简化创建和精炼原型服务。一旦一个稳定的服务定义最终完成,产生步骤就不再是必须的了——JiBX 绑定定义可以被作为稳定元件对待,并直接用于部署,只要没有影响绑定数据(如缺少域,或改变类结构)的数据模型的变更。如果存在这样的变更,JiBX 绑定编译器将报告错误,部署步骤将失败。此时,你要么恢复绑定所期望的数据模型,要么修改绑定以匹配修改的数据模型(同时保护模式定义的 XML 格式——尽管目前这还不是由 JiBX 强制的)。

复制代码
package com.sosnoski.infoq.ex1;

/**



* Interface for placing orders and checking status.

*/

public interface StoreService

{

/**

* Submit a new order.

*

* @param order

* @return id

*/

public String placeOrder(Order order);

/**



* Retrieve order information.

*

* @param id order identifier

* @return order information

*/

public Order retrieveOrder(String id);

/**



* Cancel order. This can only be used for orders which have not been shipped.

*

* @param id order identifier

* @return <code>true</code> if order cancelled, <code>false</code> if already shipped

*/

public boolean cancelOrder(String id);

}

/**



* Order information.

*/

public class Order

{

/** Unique identifier for this order. This is added to the order information by the service. */

private String orderId;

/** Customer identifier code. */



private String customerId;

/** Customer name. */



private String customerName;

/** Billing address information. */



private Address billTo;

/** Shipping address information. If missing, the billing address is also used as the shipping address. */



private Address shipTo;

/** Line items in order. */



private List items;

/** Date order was placed with server. This is added to the order information by the service. */



private Date orderDate;

/** Date order was shipped. This is added to the order information by the service. */



private Date shipDate;

...

}

清单 1. 服务代码和数据模型例程(部分)

清单 1 给出了一个服务接口和根数据模型类的简单例子。清单 2 显示了用于 Jibx2Wsdl 的自定义文件,它增加了没有在清单 1 源代码中描述的额外信息。在本例中,被增加的信息包括指定在 WSDL 和模式中使用的命名空间,列出在每个数据中哪些值是必须的,并且哪些应该使用属性而不是子元素表示(在值名字前以 @开始),指明被包含在集合中的项目类型。

复制代码
<custom force-classes="true" namespace="http://ws.sosnoski.com/order/data"

namespace-style="fixed">

<wsdl namespace="http://ws.sosnoski.com/order/wsdl"

wsdl-namespace="http://ws.sosnoski.com/order/wsdl"/>

<package name="com.sosnoski.infoq.ex1">

<class name="Order" requireds="@customerId customerName billTo items"

optionals="orderId orderDate shipDate">

<collection-field field="items" item-type="com.sosnoski.infoq.ex1.Item"/>

</class>

<class name="Address" requireds="street1 city @state @zip"/>

<class name="Item" requireds="@id @quantity @price"/>

</package>

</custom>

清单 2. Jibx2Wsdl 自定义文件

清单 3 显示由 Jibx2Wsdl 产生的 WSDL 和模式的所选的一部分。你可以看到从源代码中抽出的 JavaDocs,在模式中以 <xsd:annotation>/<xsd:documentation> 组件形式,在 WSDL 中以 <wsdl:documentation> 元素出现。这些被产生的元件可能或不会用于最终的部署——首先,一些被加入的空格和格式化可能是帮助使文档更易让人读——但是至少它们的确是最终版的一个非常好的起点。

复制代码
<wsdl:definitions ... targetNamespace="http://ws.sosnoski.com/order/wsdl/StoreService">

<wsdl:types>

<xsd:schema ... targetNamespace="http://ws.sosnoski.com/order/wsdl/StoreService">

<xsd:import namespace="http://ws.sosnoski.com/order/data"

schemaLocation="data.xsd"/>

<xsd:element name="placeOrder">

<xsd:complexType>

<xsd:sequence>

<xsd:element type="ns1:order" name="order" minOccurs="0"/>

</xsd:sequence>

</xsd:complexType>

</xsd:element>

<xsd:element name="placeOrderResponse">

<xsd:complexType>

<xsd:sequence>

<xsd:element type="xsd:string" name="string" minOccurs="0">

<xsd:annotation>

<xsd:documentation>assigned order identifier</xsd:documentation>

</xsd:annotation>

</xsd:element>

</xsd:sequence>

</xsd:complexType>

</xsd:element>

...

</xsd:schema>

</wsdl:types>

...

<wsdl:portType name="StoreServicePortType">

<wsdl:documentation>Interface for placing orders and checking status.</wsdl:documentation>

<wsdl:operation name="placeOrder">

<wsdl:documentation>Submit a new order.</wsdl:documentation>

<wsdl:input message="tns:placeOrderMessage"/>

<wsdl:output message="tns:placeOrderResponseMessage"/>

</wsdl:operation>

...

</wsdl:portType>

</wsdl:definitions>

<xsd:schema ... targetNamespace="http://ws.sosnoski.com/order/data">



<xsd:complexType name="order">

<xsd:annotation>

<xsd:documentation>Order information.</xsd:documentation>

</xsd:annotation>

<xsd:sequence>

<xsd:element type="xsd:string" name="orderId" minOccurs="0">

<xsd:annotation>

<xsd:documentation>Unique identifier for this order. This is added to the order information by the service.</xsd:documentation>

</xsd:annotation>

</xsd:element>

...

<xsd:element ref="tns:address" minOccurs="0">

<xsd:annotation>

<xsd:documentation>Shipping address information. If missing, the billing address is also used as the shipping address.</xsd:documentation>

</xsd:annotation>

</xsd:element>

<xsd:element name="item" minOccurs="0" maxOccurs="unbounded">

<xsd:complexType>

<xsd:sequence/>

<xsd:attribute type="xsd:string" use="required" name="id"/>

<xsd:attribute type="xsd:int" use="required" name="quantity"/>

<xsd:attribute type="xsd:float" use="required" name="price"/>

</xsd:complexType>

</xsd:element>

</xsd:sequence>

...

</xsd:complexType>

</xsd:schema>

清单 3. WSDL 和模式(部分)

一旦你有了服务构造的基本例程,那么在其中填入更多的细节和转换数据模型以适合不同受益者的需要将是非常容易的事情。被产生的绑定可与 WSDL 和模式的组合,使用 Apache Axis2 Web 服务框架(或使用 XFire,或即将发布的 JiBX/WS)部署你的服务,并且相同的数据模型可被 Java 客户端直接使用。你自然也可使用其它框架和其它客户端——因为产生的 WSDL 和模式定义了服务接口——但是对于最初的开发,使用代码的单一版本进行工作提供了真正的方便,并且 Jibx2Wsdl 使你能方便地完成这些。

总结

已经被 SOA 社区广泛接受的“由 WSDL 开始”总是正确的方法,但是需要指出的是,现实世界的选择要比这个简单的判断要复杂得多。“由 WSDL 开始”要求一个高层的投资,包括关于学习 WSDL 和模式,以及使用通常乏味的支持这些格式的工具。这存在着许多预先努力,但不保证结果恰好适合你的需要,更别说清晰和结构良好了。

“由代码开始”也有其自身潜在的毛病,包括可能你会无意间将你的服务描述与特定实现捆绑在一起。但是现代数据绑定框架允许你从实际的 XML 表示中隔离数据模型的细节,从实用角度来看,开发者使用代码工作总是较使用 WSDL 和模式工作更具生产率。在很多情况下,Web 服务开发实际总是开始于现有代码,使用某种老技术实现服务。因此,不论专家给出哪些意见,从长远来看,“由代码开始”可能依旧是 Web 服务开发的重要组成部分。

不管使用哪种类型的数据绑定和 Web 服务框架,使用"由代码开始"作为快速跟进一个工作服务的做法都是可行的。一旦你使你的服务功能正确且根据你的用例进行了测试,你总是可以选择完全地破坏这种捆绑——只要采用所产生的 WSDL 和模式定义作为新的起点,并且如果必须,你可修改它们以清除任何不符合你组织需要的 XML 部分。接着,使用这个“最终的”WSDL 和模式来在你选择的框架中产生新的服务提供者代码,并转换你的服务器应用到那个代码上工作。

关于作者

Dennis Sosnoski是基于 Java 的 SOA 和 Web 服务方面的专业咨询师和培训促进者。他从事职业软件开发已超过 30 年,在过去 9 年,他关注于服务器端 XML 和 Java 技术。Dennis 是开源的JiBX XML 数据绑定框架和相关的Jibx2Wsdl 工具主要开发者,也是 Apache Axis2 Web 服务框架的提交者。他还是 JAX-WS 2.0 和 JAXB 2.0 规范专家组的成员之一。要获得更多信息,请访问他的网站或给他发 email enquiry@sosnoski.com

查看英文原文:"Code First" Web Services Reconsidered