随笔 - 181, 文章 - 2, 评论 - 85, 引用 - 0
数据加载中……

SOA 探索,第 1 部分: 通过动态解耦来简化 Web 服务调用-----执行稳定的 Web 服务调用的解决方案

学习如何使用应用动态代理模式(Dynamic Proxy Patterns)来进行动态解耦的 Web 服务适配器(Web Service Adapters)。通过适当地使用这种机制,您可以提供所需要的抽象级别,这样有助于适当的面向服务的体系结构(Service-Oriented Architecture,SOA)的实现与服务重用。

引言

理想的基于面向服务的体系结构系统设计的需求是在服务消费者与服务提供者之间进行解耦。解耦可采用多种形式,其范围从静态(在编译时通过请求端存根来解耦)到全动态解耦(在所有服务调用的运行时完全封装并建立解耦)。显然,全动态解耦是一个严格的要求。如果一个服务被动态解耦,然后服务特性中的更改导致了服务实现中的修正,但是所有其它的系统元素,特别是调用机制,仍旧保持不变。这种设计的优点显而易见。

对于 SOA 与 Web 服务的更强大功能与性能的日益增长的期望与需求经常导致显著增长的软件复杂性。今天,SOA 实现的实际情况是,尽管规划了服务的功能模块化,但是它们仍旧经常与独立的中间件或通信协议(如基于 MOM 的 JMS、HTTP/SOAP 等等)耦合。在大多数组织中,当应用程序扩展功能时,开发者关注的,在某种程度上,是静态解耦(通常是指绑定),并强制为每一个 Web 服务创建独立的客户端,而不是单独的能够同时访问多个服务的客户端。

静态绑定的局限性是非常明显的——代码中的静态绑定阻止了不同应用程序中贯穿整个企业的服务的重用。简而言之,即使需要提取并封装所用的不同中间件及协议来执行服务组件之间的交互,这些组件被包括在服务消费者与服务提供者的交互中,这样的需求非常普遍,但是封装了 Web 服务接口的体系结构还未得到广泛应用。造成这种状态的其中一个主要原因就在于,尽管动态绑定的几种机制已被设计用来适应这种局限性,但是对动态绑定相关联的实际技术依然存在很多的误解。





Web 服务调用的剖析

已经有许多关于 SOA 最重要的概念——服务接口(换句话说,调用功能)及在运行时如何定位并调用它们的文章。换句话说, Web 服务调用应该在后期绑定。需要指明的是 Web 服务调用已经被设计为传统的 Remote Procedure Call(RPC)方法,该方法准许一个应用程序调用由第二个应用程序发布的功能。RPC 工作方式如下(如图 1 所示):

  • 被调用的应用程序的功能是以本地功能的形式展示给调用的应用程序的。
  • 通过向调用的应用程序提供功能存根达到定位透明性。
  • 功能存根,当它被调用时,访问一个中间件层,该层向被调用的应用程序传输调用及其相关的数据。

图 1. 基于 RPC 的 Web 服务调用
基于 RPC 的 Web 服务器调用

在 Web 服务中,存根代码通常是自动产生的,并且是基于使用被调用的功能的接口描述,它通过 Web 服务描述语言(Web Services Description Language,WSDL)来表示,并创建了存根 shell。此外,存根通常是在应用程序开发的编码阶段产生,因为存根是主要是从应用程序代码中直接调用,而且,结果是必须在编译时解决的(换句话说,即所谓的前期绑定)。

大多数应用程序开发者,主要是 Java 开发者,认为使用这样的存根是必要的,因为对他们来说,如何处理从调用服务返回的复杂数据类型是不清楚的。当然,来自被调用应用程序功能的数据必须同 SOAP 消息相分离,而且,当使用 Java 的时候,必须有一个相对应的(兼容的)实现序列化接口的类。显然,同样的存根产生工具被用于产生那些所需求的类。

本文的首要思想就是,一般来说,通常自动化是双刃剑。一方面,自动化准许行业内 Web 服务的快速适应,其原因是,有了自动化工具,大多数应用程序开发者的行为不再发生在 WSDL 自身的级别上。另一方面,不幸的是,市场仍需要一种主要的工具来选择需要使用的调用形式——显式的存根(存根在编译时解析)或隐式的存根(存根在运行时解析)。隐式的存根通常被当成 Web 服务的无存根调用。

大多数基于 Java 的 Web 服务工具提供了一些 Web 服务接口的运行时后期绑定功能。来自 webMethods 的 Glue 产品与 J2EE Web Services Developer 组件的 JAX-RPC 包就是两个例子。然而,这些尝试大多导致了软件深受供应商特定逻辑的影响。而且,问题并不仅仅存在于供应商指定的代码。在许多情况下,供应商专有的解决方案产生了实际的可管理性与可维护性的问题。

现在让我们回顾提到的问题点,例如用于后期绑定的 webMethods Glue 方式。目前,Glue 是广泛使用的工具。由于使用了一个代理用于 Web 服务的接口,Glue 看起来好象是无所不能。然而,仅仅通过自身使用一个代理并不能消除所有的问题。下面的 Java 代码片断描述了在它们的应用中的一个典型的负面情形:


清单 1. lateBindingGlue
												
														public class lateBindingGlue 
{ 
public Document serviceInvocationGlue() 
throws Throwable { 
String wsdlName = "http://…../Service.wsdl?WSDL"; 
String operation = "…….."; 
String args[] = {  }; 
/* first, Glue creates a SOAP interceptor */ 
SOAPInterceptor responseHandler = new SOAPInterceptor(); 
/* second, Glue registers the SOAP interceptor to catch incoming responses */ 
ApplicationContext.addInboundSoapResponseInterceptor( 
(ISOAPInterceptor)responseHandler ); 
try { 
/* third, Glue gets a proxy to the Web service through its WSDL */ 
IProxy proxy = Registry.bind( wsdlName ); 
/* here, service's operation is invoked through a proxy */ 
proxy.invoke( operation, args ); 
} catch( java.rmi.UnmarshalException e ) { 
// Glue is catching the UnmarshalException that is perfectly expected 
} 
/* forth, Glue generates an XML document containing the SOAP body, */
/* and passes the whole document for parsing  */ 
 return new Document( responseHandler.getResponse() ); 
} 
} 
class SOAPInterceptor implements ISOAPInterceptor { 
private Element soapBody; 
public void intercept( SOAPMessage message, 
Context messageContext ) { 
try { 
soapBody = message.getBody(); 
} 
catch( Exception e ){ 
 System.err.println( e.toString()); 
} 
} 
public Element getResponse(){ 
 return soapBody; 
} 
} 

       

												
										

您能从上面的例子中学到很多东西。首先,由 Glue 引入的代码是一个两层代码。一层“消除”了 SOAP 分离的负面效应,通过使用 Java 异常处理程序和一个专门的组件——SOAP 拦截器来拦截这种特定的应用程序异常而实现。第二层将服务调用的结果作为完整的 XML 文档来提交给调用应用程序。然后,XML 文档通过使用一个诸如 DOM 结构的工具进行分离。显然,这样的方法不能“安全地”移植到其它 Web 服务工具中。但更重要的是,尽管拦截应用程序异常的技术广为人知,但它真的适合于 Web 服务调用吗?

一般而言,异常是 Java 语言极为有用的特性,特别是,这意味着一种检测错误位置、无计划的编程操作(如使用空指针)的简单方式。使用这种技术作为一个计划的控制操作,而不是写一个 if-then 逻辑段来测试一个指针是否真正为空,使代码的可管理性与可维护性显著地复杂化。显然,SOAP 分离并不同于空指针。您不能通过一个简单的 if-then 代码测试出来。不过,它们的负面影响是相同的——抛出异常并捕获它,这是一项代价非常高昂的技术,特别是如果您需要获得每秒数千次的 Web 服务调用的话。换句话说,运行时异常应该专门针对意外情况作为“防御线”而保留,以应对软件的错误。

我进一步指出为什么分离 SOAP 的问题不能通过一个 Java Exception API 进行处理的更多原因。但是,让我们先来看看引用的范例所显现的另外一个问题。由于调用操作需要直接分析 XML,它不得不访问响应信息的 XSD 定义,而且必须依次成为提供的 WSDL 文件的一部分(且被存根产生工具所使用)。这里的最终结果就是甚至一些后期绑定的形式也存在于引例之中;在 Web 服务伙伴之间的紧耦合依旧存在。换句话说,Web 服务的客户端必须访问 WSDL 文件,并且在大多数情况下,它必须是一个完整(而不是部分)的 WSDL 文件。

还存在一个与使用 JAX-RPC API 相关的问题。记住这是非常必要的:在 Java 语言中,由于在 XML schema 中的数据类型不能直接准确地映射到 Java 语言的数据类型当中,所以在调用的应用程序端 WSDL 中产生的服务终端接口将与 JAX-RPC 编译器在调用端产生的 WSDL 的形式不同。而且,生成的服务接口也将依赖于 SOAP 采用的编码体制。例如,使用文档文字编码利用了每个方法类的封装版本,并造成了额外的可管理性与可维护性的问题。





Web 服务编程构件的自由应用程序逻辑:最佳实践设计模式

根据到目前为止的讲述,您容易想到使用任何种类的动态调用技术(如后期绑定的动态代理),不幸的是,许多开发者都是这样做的。关于 Web 服务其中一个最常见的误解就是,使用动态调用取代静态存根的好处并没有那么大,而且最好是坚持用生成的存根作为 Web 服务调用的主要方法。

最后,我们的讨论转到第二个,而且是最重要的原则——它不是真正的编程技术,它确保不依赖服务编程构件(如 WSDL),但是取而代之的是,它的价值体现在高级设计中。这对于获得 SOA 的真正利益是必要的,因为随着服务的发展,服务消费者的代码将完全不受影响。无论如何,基于 SOA 的应用程序必须注重于提供弹性体系结构,且更少的关注于公共通信协议(比如 SOAP)。服务与应用之间不应该具有严格的界限。一个终端用户的应用程序可以被看作是其它终端用户的服务。整个企业 IT 环境应该被设计成高度模块化,允许开发者能够挑选适合他们需要的服务与应用组合。

在良好的 SOA 设计中,Web 服务消费者的应用逻辑能够使用两个基础体系结构原则来从服务构件完全解耦:

  • “保持可适应性”——创建一种方法来支持多接口继承(理论上,这将不受限制),在开发有限数量的具体实现(在面向对象的设计当中,经常将匿名的内部类作为对象适配器使用,考虑上下文,允许工作行为的定制,实际上是嵌入在子类之中)。
  • “使用所谓的好莱坞原则:不要调用我们,我们将调用您”——Web 服务调用模型应该只通过动态代理框架来指定发现,并假定客户端事先对服务一无所知;因此,Web 服务提供方应该能够通过将存根类信息声明到 XML 文档,从而在运行时宣传他们的服务。那实际上就以一种开放且可执行的方式,提供了发现之外的另一种调用方法(换句话说,优于 UDDI)。

最终设计目标是,为 Web 服务消费者应用提供一组类,这些类根据上面引述的原则组成了可重用适配器层。这种适配器层封装了使用存根及其生成的类的代码。适配器的公共 API 并不公开任何存根类;而是将其映射到 Web 服务消费者应用可以理解的类。

为了得到最好的适配器灵活性与可扩展性,适配器的总体类结构均通过使用下列设计模式的组合来构建:

  • 动态代理是一种结构模式,它定义了一组类与方法调用,这些类和调用被分派给调用句柄(在动态代理类生成的时候指定)。使用动态代理是 Java 编程中模拟多实现继承的关键。通过动态代理,定制的 InvocationHandler 能够由一组表示合成子类的超类来构建;该子类的接口将是这些超类实现的接口的联合。
  • 适配器是一种结构模式,它通过对实现的抽象进行解耦来影响类层次(比如继承层次)的创建,因此两者可以单独改变。这种允许在运行时选择实现的解耦避免了抽象与其实现间的永久绑定。作为 Web 服务客户端的调用应用的类仅处理抽象。
  • 服务配置器是一种行为模式,它使您可以改变适配器超时性能,并添加或去除附加功能,这样就改变了调用框架的规范。例如,如果一个 Web 服务提供方引入了一个新的协议(如在 RMI 上的 SOAP),那么它只需要“宣传”新的传输性能。因此,适配器演示了这种新服务能力与这种调用功能的任何实现。使用一些元数据表示作为结果,必须具备一种方法能够引用提供了独立于特定规范与实现的功能类的接口。
  • 工厂方法(Factory Method )是一种结构模式,它使一个类延迟实例化到子类。在 Web 服务调用的情况下,本地与远程实现类都必须是子类,以实现它们的特定于服务的实现。需要访问服务的调用应用将得到这个工厂的一个句柄,并给服务一个调用。因此,根据从服务配置器得来的信息,工厂封装了访问服务必须使用的实现知识。
  • 装饰(Decorator)模式是一种结构模式,它定义了缓存、发布与交换服务声明的封装器,用于连接合适服务与可重用的代理类。 通过使用装饰模式,实现执行调用的代码与提供缓存的代码相分离,将非常简单。




关键实现指南

图 2 演示了使用上面引用的设计原则而设计的“好莱坞”类型适配器的概念模型。 图 3 展示了使用适配器时的基本交互的序列图。


图 2. Web 服务适配器的概念模型
Web 服务适配器的概念模型

图 3. 包含 Web 服务适配器的序列图
包括 Web 服务适配器的序列图

创建一个使用适配器的服务,包括创建一个使用服务描述符(代表 SOAP 服务)的服务类。这个描述符被包括在服务类成员之中。服务类被作为 SOAP 来部署,例如,使用来自 Open Source Foundation 的 Axis。

服务描述符包括:

  • 类名:包含服务实现的全限定类名的元素。
  • 名称:包含服务名的元素。
  • 版本:包含服务版本名的元素。
  • 创建者:包含服务创建者名称的元素。
  • AssertURI:包含指向一个 XML 文件的统一资源标识符(URI)的元素。其中,XML 文件包括服务声明(规范)。
  • 描述:包含服务描述的元素。

现在,有一个关于 Axis 的耦合注意事项。使用 Axis 作为 SOAP 引擎非常有助于实现 Web 服务的松耦合,理由如下:

  1. 因为 Axis 定义了消息处理节点,该节点能够被封装以供服务请求者(客户端)与服务提供者(服务器)使用,适配器能够使用部署描述符作为一个 Web 服务部署描述符(WSDD),应用一个消息处理节点来部署 SOAP 服务。
  2. Axis 中的服务通常是 SOAP 服务的实例,它包含请求与响应链,但是必须包含实际服务类的提供方。结果是,通过将消息上下文传递给消息处理节点来完成 SOAP 服务的处理。
  3. Axis 客户端处理能够通过使用服务描述符和 XML 声明文件创建包含服务细节的调用工厂实例,从而构建调用对象。调用对象的属性被设定为使用相关的目标服务。然后,调用对象就通过调用 Service.createCall 工厂方法来创建。一旦建立起调用,Call.SetOperation 就通过被调用的方法名称来指定。然后,Call.invoke 通过相关的请求消息而被调用,它驱动 AxisClient.invoke,处理结果消息上下文,并向客户端返回响应消息。

在适配器的设计中,应该对性能进行特殊考虑。使用动态代理类具有性能含义。访问目标的直接方法快于访问代理类的方法。然而,这不应是在稳健体系结构与性能之间进行选择。这就是为什么目前适配器实现缓存封装器的原因。通过应用缓存,使用静态存根与基于适配器的解决方案之间的差异相对较小。根据如何实现缓存,必须提到可能的解决方案之一——记忆(Memoization)。 记忆是一种广泛使用的技术,它在 Lisp、Python 与 Perl 这样的功能编程语言中使用,给功能赋予预先计算的值。记忆一个功能将为功能添加一个透明的缓存封装,因此已经得到的值将从缓存中返回而不是每次都重建。记忆可以显著提高动态代理调用的性能。

总结我们关于此处描述的适配器的讨论,允许支持本地和远程服务实现的设计是非常重要的。服务类对适配器来说将是本地的,而远程 Web 服务则可以相互替换,因为服务类和代理类使用同样的接口来访问远程 Web 服务。本地服务类将实现与 Web 服务器远程实现相类似的方法 getFunction() 来返回功能的结果。下面的 Java 代码片断进一步说明了这一点:


清单 2. 本地服务类及其接口
												
														public class LocalServiceImpl implements IService
{
/* get the service results */
public ….. getFunction() 
{
………..
return …………;
}
}


												
										

实现了 IService 的代理类采用了方法 getFunction(),但并未考虑该方法需要访问远程 Web 服务的代码。这些代码代表了需要用来访问部署在适配器内的任意 Web 服务的代理代码。


清单 3. 远程服务
												
														public class RemoteServiceImpl implements IService 
{
/* get the service outputs */
/* outputs are from the web service running in the location */
/* mentioned in the serviceURI */

public ….. getFunction()
{
adapter.examples.Function service = null;
String serviceURI = "http://……../Function/";
String wsdlURI = "http://localhost:8080/Function/";
Try
{
// init the lookup
WebServiceLookup lookup = (WebServiceLookup)
……….
// get the instance of the Web services interface
// from the lookup
service = (adapter.examples.Function)
lookup.lookup(wsdlURI,
adapter.examples.Function.class,
serviceURI);
}
catch (Exception e) 
{
e.printStackTrace();
}
// now, call the methods on your Web services interface
return service.getFunction();
}



												
										





结束语

显然,使用 Web 服务的静态存根与前期绑定是 Web 服务调用的最简单的形式。但是简单化也带来了明显的缺点。与紧耦合方法不同,使用本文提出的适配器方法将给您留下可高度重用和可配置的 Web 服务代码。由于 Web 服务 调用全部通过部署了服务描述符的公共适配器来引导,因此您能够动态地决定调用什么服务——在部署代码时或在运行时。

同样,那些可能寻找商业产品作为 Web 服务松耦合调用定制解决方案的组织应该研究企业服务总线(Enterprise Service Bus,ESB)技术(参见参考资料),该技术提供了类似于上面所引述的功能的选项来连接企业内部及跨企业的可作为 Web 服务的应用程序,并具备一套功能来管理并监视已连接应用之间的交互。

posted on 2006-04-17 03:56 wsdfsdf 阅读(231) 评论(0)  编辑 收藏 引用 所属分类: 技术文章


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理