一、SOA现状
SOA渐渐进入到开发的应用实践,其本质是保持业务敏捷,也就是“全心全意”的根据业务需要找到或集成相关的服务,然后快速适应业务变化。虽然通过很多管理的、技术协议的约束可以进行有效治理,但如果技术实现上缺乏灵活性,那么当技术环境变化的时候修改的代价一样会很大,为什么?
以往单应用的开发模式模式下,与之相关的技术实体有限,通过采用软件工厂、合理运用设计模式、集成成熟框架的手段可以一定程度上降低应用内部模块间的耦合度。到了SOA时代,虽然服务接口被简化,应用间的耦合度降低了,但同时也等于把自己的应用推到一个更广泛的空间,怎样灵活的适配同样被置于互联网的各种服务也许成了更大的挑战。
作为架构人员该考虑什么问题呢?把所有的变化“透明化”,无论是服务接口还是依赖的其它SOA组成变化时,要尽量确保应用感觉不到这些变化(或把变化集中在一个很小的范围)。
XML Web Service作为SOA建设采用的一项普适技术,设计不当很可能成为阻碍业务敏捷的绊脚石,成为应用的累赘。本系列借鉴设计模式、架构模式和部分产品对Web Service的使用情况,结合相关项目的实施经验,介绍一些Web Service 设计与整合模式(Web Service Design &Integration Patterns),目的是找到特定上下文环境下设计和集成Web Service的“相对优”解。与其它模式方法介绍方法不同,Web Service更多的需要被放在整个SOA环境下考虑,而不仅仅是几个类的关系。
下面是一个简化后的逻辑SOA环境:

图1:简化后的逻辑SOA环境
不难看出,在ESB之外,Web Service仅仅作为一个个Provider,以插件方式安置到ESB中,客户程序也更多的通过ESB找到每个Provider。但它仅仅是一个较为理想的状态,现实情况下很多时候根本没有中间的ESB,客户程序就是直接按需使用Web Service。因此,这个点对点的调用成了这样:

图2:点对点的调用
这里的中介者可能仅仅就是个网络,也可能包括一个在.Net或Java平台编译好的WSDL代理,也可能是更复杂的机制,它的设计和集成就成了很关键的因素,那么希望它起到什么作用呢?
笔者认为主要有三个:
•给客户程序一个透明的接口,借助它灵活的适应后端Web Service的变化。
•降低客户程序与Web Service直接调用带来的1:1甚至1:N直接耦合。
•提供高可用机制。
(注:本系列很多内容引用到Apress出版的《Web Service Patterns: Java Edition》(下文简称WSP),但介绍的模式不仅限于该书提及的14种,而且实现上会采用Visual Studio.Net 2005,客户程序会以Test Project的Unit Test形式出现。)
二、Web Service的模式化特征
使用或集成Web Service的时候主要有两种方法:一种是到集中的“字典”中找到相应的服务,然后绑定并使用,这种方式尤其适于使用公共服务。另一种就是直接连接到某个服务,然后通过.Net或Java开发工具生成WSDL代理的办法,但抽象来看基本的组织方式如下。

图3:Web Service的基本模式(静态)

图4:Web Service的基本模式(动态——准备阶段)

图5:Web Service的基本模式(动态——执行阶段)
三个基本抽象角色的作用:
•Service:SOA环境中的一个功能性提供者,向客户程序或其它服务提供特定功能(功能组)支持的功能性实体。
•Directory:管理需要使用的服务功能与实际服务之间的对应关系,包括怎么与目标服务通信、目标服务的实际位置和目标服务所提供的服务接口等信息。
•Client:消费Web Service的客户程序。
看上去没有增加中介者的必要,但实际项目中这种客户程序和服务绑定关系不是1:1的,一个客户应用经常会使用多个服务,而每个服务也往往不仅仅服务于一个系统的客户程序,加之服务的自治性,客户程序使用的服务都在自治的发生变化,甚至包括技术之外的(例如:服务的使用费用)因素,因此需要一个前置中介者处理这些问题。同时,设计每个服务的时候,也往往会调用到其它服务,服务本身可能会划分为不同层次,这时候情况更加复杂,但这又是SOA环境下Service-to-Service调用不得不经常面对的情况。

图6:包括Service-to-Service调用后的Web Service基本模式(静态)
三、第一层的包装
用Web Service开发的技术优势不谈了,有关性能相对二进制调用较低的这个劣势也不提,从开发角度看Web Service的接口很类似COM的接口,它不可以被继承、也没有所谓的多态特性,因此如果把每个Web Service看成一个服务实体的话它是“平面”的,为了组织这些Web Service,需要首先对它们进行第一层包装,另外们称为某类OO语言可以使用的类,参考COM实现方式主要有两种——Contain和Aggregation:

图7:对Web Service进行第一层包装的方法 Contain(左)和Aggregation(右)
下面是两种包装方式的示例,为了看起来更直观,这里对SI的描述没有采用WSDL,而是采用代码的方式,假设有两个Web Service,分别提供如下功能:
C#
using System;
using System.Web.Services;
namespace VisionTask.Training.ServicePattern.UtilityService
...{
[WebService]
public class ServiceA : WebService
...{
[WebMethod]
public int GetQuantity() ...{ return 5; }
[WebMethod]
public int GetTotalCost() ...{ return 1000; }
}
}
C#
using System;
using System.Web.Services;
namespace VisionTask.Training.ServicePattern.UtilityService
...{
[WebService]
public class ServiceB : WebService
...{
[WebMethod]
public int GetAverage(int total, int quantity) ...{ return (int)(total / quantity); }
}
}
这时客户程序如果需要包括“GetAveragaeCost”、“ GetQuantity”、“ GetTotalCost”的功能,采用Aggregation则是直接在Directory上把ServiceA和ServiceB暴露给客户程序,由客户程序完成“GetAveragaeCost”的实现;采用Contain方式则是通过一个一个新的Service(例如:叫AdvancedService),由它完成“GetAveragaeCost”,Directory中仅仅对客户程序发布“ServiceA”和“AdvancedService”两个服务,将“ServiceB”作为内部服务隐藏起来。两种实现方式如下。
方式1:Aggregation
using System;
using VisionTask.Training.ServicePattern.UtilityService.CurrentServiceA;
using VisionTask.Training.ServicePattern.UtilityService.CurrentServiceB;
namespace VisionTask.Training.ServicePattern.UtilityService.Aggregation
{
public class Client
{
private ServiceA serviceA;
private ServiceB serviceB;
/// <summary>
/// 通过Directory 直接获得ServiceA 和ServiceB 的引用。
/// 由客户程序自己完成需要包装的功能——GetAverageCost。
/// </summary>
/// <returns></returns>
public int GetAverageCost()
{
if ((serviceA == null) || (serviceB == null)) throw new NullReferenceException();
return serviceB.GetAverage(serviceA.GetTotalCost(), serviceA.GetQuantity());
}
}
}
方式2:Contain
Web Service
using System;
using System.Web.Services;
using VisionTask.Training.ServicePattern.UtilityService.CurrentServiceA;
using VisionTask.Training.ServicePattern.UtilityService.CurrentServiceB;
namespace VisionTask.Training.ServicePattern.UtilityService.Contain
{
/// <summary>
/// 新包装的Service,完成客户程序需要的新功能——GetAverageCost。
/// </summary>
[WebService]
public class AdvancedService : WebService
{
[WebMethod]
public int GetAveragaeCost()
{
ServiceA a = new ServiceA();
return (new ServiceB()).GetAverage(a.GetTotalCost(), a.GetQuantity());
}
}
}
C#
using VisionTask.Training.ServicePattern.UtilityService.CurrentServiceA;
using VisionTask.Training.ServicePattern.UtilityService.CurrentAdvancedService;
namespace VisionTask.Training.ServicePattern.UtilityService.Contain
{
/// <summary>
/// 客户程序需要使用“GetQuantity”或“GetTotalCost”是调用ServiceA。
/// 如果需要使用包装的功能时,直接调用“AdvancedService”的“GetAveragaeCost”。
/// </summary>
public class Client
{
private ServiceA serviceA;
private AdvancedService advancedService;
}
}
四、SOA环境下典型的Web Service开发方式
单独的Web Service开发已经非常成熟,无论是通过Visual Studio.Net还是Eclipse(还是那堆Java平台的类似工具)都很方便,而且无论是.Net阵营还是Java阵营也都拥有自己的Web Service开发框架。但是在每个Web Service之上的调度和协调工作(算是治理的一部分)更为重要,企业Web Service应用的程度愈深、应用范围愈广,这方面的管理也更加重要。下面是典型的Web Service开发、应用环境:

图8:实际开发中典型的Web Service环境
考虑到部署复杂程度的不同,Directory也经常由用户自定义的简单动态Service URI完成,而不是一味的要求集成UDDI服务。本系列由于目的在于介绍各种Web Service设计和集成模式,所以一般情况下Directory采用的都是“自制”的简化版本。
五、 为Web Service依赖倒置
相信在实践设计模式的过程中,开发人员已经对依赖倒置的概念有了深刻的体验,“不依赖于具体实现,而是依赖于抽象”,整理SOA环境下的Web Service一样需要借鉴这个概念,笔者将之称为“Web Service依赖倒置”。大概逻辑结构变成如下:

图9:概要Web Service依赖倒置后的逻辑关系
但正如上文介绍的,Web Service本身接口是“平的”,没有办法继承,只有用OO语言把它进行包装之后才可以成为对应的类,这时候才能有所谓的“继承”或“接口实现”;所谓“抽象”既可能是接口也可能是抽象类(当然,也可以考虑用实体基类),所以在处理ConcreteWebService与抽象Web Service的时候也有两种方式:
•通过继承的
•通过单继承 + 多接口组合的
笔者更倾向于后者,因为通过组合可以不断扩展。同时考虑到Web Service使用往往在一个分布式的环境中,因此参考RPC中常用的叫法,增加了一一个Stub(用接口IServiceX表示)和Proxy。修改后依赖倒置的关系如下:

图10:分布式环境下多组合服务接口实现的Web Service依赖倒置
六、如何实现一个示例
1、对业务数据建模(XSD)
假设业务对象为报价信息,报价分为报价头和明细(1:0..n),因此结构如下:

图11:报价信息的XSD
XSD
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema
xmlns="http://www.visionlogic.com/trade"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.visionlogic.com/trade"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xs:element name="Quote">
<xs:annotation>
<xs:documentation>Comment describing your root element</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element ref="QuoteItem" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="Id" type="xs:string" use="required"/>
<xs:attribute name="Company" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
<xs:element name="QuoteItem">
<xs:complexType>
<xs:attribute name="ProductId" type="xs:integer" use="required"/>
<xs:attribute name="Price" type="xs:double" use="required"/>
<xs:attribute name="QuantitiveInStock" type="xs:double"/>
</xs:complexType>
</xs:element>
</xs:schema>
2、完成XSD与对象实体的映射(XSD to Object)
通过Visual Studio.Net自带的Xsd.exe进行如下操作。
这样就生成了结构大概如下的对应的报价实体类:xsd Quote.xsd /c /n:DemoService
C#
using System;
using System.Xml.Serialization;
namespace DemoService
{
[System.SerializableAttribute()]
[XmlTypeAttribute(AnonymousType = true, Namespace = "http://www.visionlogic.com/trade")]
[XmlRootAttribute(Namespace = "http://www.visionlogic.com/trade", IsNullable = false)]
public partial class Quote
{
private QuoteItem[] quoteItemField;
private string idField;
private string companyField;
[XmlElementAttribute("QuoteItem")]
public QuoteItem[] QuoteItem
{
get { return this.quoteItemField; }
set { this.quoteItemField = value; }
}
[XmlAttributeAttribute()]
public string Id
{
get { return this.idField; }
set { this.idField = value; }
}
[XmlAttributeAttribute()]
public string Company
{
get { return this.companyField; }
set { this.companyField = value; }
}
}
[SerializableAttribute()]
[XmlTypeAttribute(AnonymousType = true, Namespace = "http://www.visionlogic.com/trade")]
[XmlRootAttribute(Namespace = "http://www.visionlogic.com/trade", IsNullable = false)]
public partial class QuoteItem
{
… …
}
}
3、完成抽象的Web Service定义(optional)
该步骤的目的是获取wsdl定义。这里笔者为了省事,用Visual Studio.Net自动生成,所以写了个抽象的Web Service类,实际开发中完全可以独立编写wsdl文件。
C#
using System.Web.Services;
using System.Xml.Serialization;
namespace DemoService
{
[WebService(Name="QuoteService", Namespace="http://www.visionlogic.com/trade")]
public abstract class QuoteServiceBase : WebService
{
[WebMethod()]
[return:XmlElement("Quote", Namespace="http://www.visoinlogic.com/trade")]
public abstract Quote GetQuote(string id);
}
}
WSDL
<?xml version="1.0" encoding="utf-8"?>
<wsdl:definitions
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:tm=http://microsoft.com/wsdl/mime/textMatching/
xmlns:soapenc=http://schemas.xmlsoap.org/soap/encoding/
xmlns:mime=http://schemas.xmlsoap.org/wsdl/mime/
xmlns:tns=http://www.visionlogic.com/trade
xmlns:s1=http://www.visoinlogic.com/trade
xmlns:s=http://www.w3.org/2001/XMLSchema
xmlns:soap12=http://schemas.xmlsoap.org/wsdl/soap12/
xmlns:http=http://schemas.xmlsoap.org/wsdl/http/
targetNamespace=http://www.visionlogic.com/trade
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
<wsdl:types>
<s:schema elementFormDefault="qualified" targetNamespace="http://www.visionlogic.com/trade">
<s:import namespace="http://www.visoinlogic.com/trade" />
<s:element name="GetQuote">
<s:complexType>
<s:sequence>
<s:element minOccurs="0" maxOccurs="1" name="id" type="s:string" />
</s:sequence>
</s:complexType>
</s:element>
<s:element name="GetQuoteResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="0" maxOccurs="1" ref="s1:Quote" />
</s:sequence>
</s:complexType>
</s:element>
… …
<wsdl:service name="QuoteService">
<wsdl:port name="QuoteServiceSoap" binding="tns:QuoteServiceSoap">
<soap:address location="http://localhost:2401/QuoteServiceBase.asmx" />
</wsdl:port>
<wsdl:port name="QuoteServiceSoap12" binding="tns:QuoteServiceSoap12">
<soap12:address location="http://localhost:2401/QuoteServiceBase.asmx" />
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
4、生成Web Service接口类型
通过Visual Studio.Net自带的Wsdl.exe进行如下操作。
这样就生成了报价Web Service的抽象接口:wsdl /n:DemoService /serverinterface /o:IQuoteStub.cs Quote.wsdl Quote.xsd
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Web.Services.Description;
using System.Xml.Serialization;
namespace DemoService
{
[WebServiceBindingAttribute(
Name = "QuoteServiceSoap", Namespace = "http://www.visionlogic.com/trade")]
public interface IQuoteServiceSoap
{
[WebMethodAttribute()]
[SoapDocumentMethodAttribute(
"http://www.visionlogic.com/trade/GetQuote",
RequestNamespace = "http://www.visionlogic.com/trade",
ResponseNamespace = "http://www.visionlogic.com/trade",
Use = SoapBindingUse.Literal,
ParameterStyle = SoapParameterStyle.Wrapped)]
[return: XmlElementAttribute("Quote",
Namespace = "http://www.visoinlogic.com/trade")]
Quote GetQuote(string id);
}
}
5、生成具体的报价Web Service
为了示例的方便,IntranetQuoteService自己“手捏”了一票测试报价数据,至此服务端Web Service工作基本完成,如果需要使用UDDI则还需要把这个具体服务publish出来。
using System;
using System.Web.Services;
using System.Web.Services.Protocols;
namespace DemoService
{
/// <summary>
/// 具体的报价Web Service 功能实现
/// </summary>
[WebService(Namespace = "http://www.visionlogic.com/trade")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class IntranetQuoteService : WebService, IQuoteServiceSoap
{
/// <summary>
/// 实现抽象的Web Service调用
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[WebMethod]
public Quote GetQuote(string id)
{
#region "手捏"出来的测试数据
Quote quote = new Quote();
quote.Id = id;
quote.Company = "deluxe";
QuoteItem[] items = new QuoteItem[2];
items[0] = new QuoteItem();
items[0].QuantitiveInStockSpecified = true;
items[0].ProductId = "Note Bulletin";
items[0].Price = 220;
items[0].QuantitiveInStock = 10;
items[1] = new QuoteItem();
items[1].QuantitiveInStockSpecified = true;
items[1].ProductId = "Pen";
items[1].Price = 3.4;
items[1].QuantitiveInStock = 3000;
quote.QuoteItem = items;
#endregion
return quote;
}
}
}
6、生成客户端Proxy
通过Visual Studio.Net自带的Wsdl.exe进行如下操作。
这样就生成了报价Web Service的客户端Proxy,它仅通过最初抽象Web Service的WSDL调用服务端Web Service。实际运行过程中,它并不了解真正使用的时候是由哪个服务提供WSDL中声明到的“GetQuote”方法。wsdl /n:Test.Client /o:QuoteProxy.cs Quote.wsdl Quote.xsd
using System.Web.Services;
using System.Threading;
using System.Web.Services.Protocols;
using System.Web.Services.Description;
using System.Xml.Serialization;
using DemoService;
namespace Test.Client
{
/// <summary>
/// Web Service 的客户端 Proxy
/// </summary>
[WebServiceBindingAttribute(
Name="QuoteServiceSoap",
Namespace="http://www.visionlogic.com/trade")]
public class QuoteService : SoapHttpClientProtocol
{
/// <summary>
/// 借助 SOAP 消息调用 Web Service 服务端
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[SoapDocumentMethodAttribute(
"http://www.visionlogic.com/trade/GetQuote",
RequestNamespace="http://www.visionlogic.com/trade",
ResponseNamespace="http://www.visionlogic.com/trade",
Use=SoapBindingUse.Literal,
ParameterStyle=SoapParameterStyle.Wrapped)]
[return: XmlElementAttribute("Quote",
Namespace="http://www.visoinlogic.com/trade")]
public Quote GetQuote(string id)
{
object[] results = this.Invoke("GetQuote", new object[] {id});
return ((Quote)(results[0]));
}
}
}
7、客户程序
最后,通过单元测试工具检查的客户程序如下:
using System;
using DemoService;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Test.Client
{
/// <summary>
/// 测试用客户程序
/// </summary>
[TestClass]
public class Client
{
/// <summary>
/// 为了简化,这里在客户程序中直接定义了具体报价Web Service的Uri.
/// 实际开发中该信息应该作为服务端的一个配置项登记在Directory之中,
/// 客户程序仅仅通过抽象的服务逻辑名称从Directory中获得。)
/// </summary>
[TestMethod]
public void Test()
{
QuoteService service = new QuoteService();
service.Url = "http://localhost:2401/IntranetQuoteService.asmx";
Quote quote = service.GetQuote("quote:2007-07-15");
Assert.AreEqual<string>("quote:2007-07-15", quote.Id);
Assert.AreEqual<string>("deluxe", quote.Company);
Assert.AreEqual<int>(2, quote.QuoteItem.Length);
Assert.IsNotNull(quote.QuoteItem[0]);
}
}
}
为了使用方便,本系列所有示例都没有直接采用IIS作为Web Server宿主,而是采用Visual Studio.Net自带的临时服务进程,因此WSDL和Proxy的使用上,相关端口可能会变化。