应该感谢那些指出你错误的人

借我三千虎骑,复我泱泱中华!

博客园 首页 新随笔 联系 订阅 管理

简介

在我以前的文章中,针对 UDDI 在运行时的使用引入了将运行时 UDDI 作为一种为 Web 服务提供故障转移和恢复的方法的概念。本文探究了如何将 UDDI 作为 Web 服务服务器与其客户端之间的抽象层插入(以使这两者之间能进行更多动态交互)的方法。构建完基本的设计模式以后,本文深入研究了该方法,并且介绍了在运行时使用 UDDI 的更复杂的设计模式。

在 Web 服务客户端和服务器之间放置一个运行时层极大地增多了用来编写更智能、更动态的客户端的选项。更重要的是,这种设计方法减少了 Web 服务客户端内对硬编码依赖项的需求。Web 服务的终结点可以从客户端的代码基中提取出来,而客户端可以查询 UDDI 来确定该终结点。移除这些依赖项可以启用许多吸引人的方案。客户端能够在运行时做出关于它们调用的终结点的决定。可以根据许多不同的变量做出这些决定,这取决于与不同服务关联的元数据。

请注意,可以使用其他方法(例如,配置文件和数据库查询)或通过对某个特定代理服务进行 Web 服务调用,来动态地为某个 Web 服务获取终结点。将 UDDI 用作此代理的优势在于 UDDI 具有全行业支持(与特定解决方案不同),并且支持一种基于分类的灵活查询机制,该机制使您可以根据分类方案(例如,物理位置、使用成本和服务质量保证)选择 URL。这种灵活性存在于较高的抽象层中,而不存在于较低层的、不单独提供基于应用程序约束的终结点选择的协议(例如,WS-Routing 和 WS-Referral)之中。

为了提供更具体的方案,请设想以下问题集。假设一家公司拥有在数年中收购的几家子公司和分公司。每家子公司都使用不同的技术平台,并以不同的格式报告销售数据。这意味着总部 IT 部门必须将几种不同的数据格式处理成某种通用格式,以使销售数据呈现在单一视图中。这不仅是一项费力耗时的过程,而且当有新的公司被收购时,此提供聚合销售报告的过程就必须处理更多新格式。总部 CIO 希望有一种方法可以获得跨所有子公司聚合的销售数据的实时视图,并且希望该解决方案能足够灵活,以便在有子公司加入或离开时,能够适应这些不同子公司的不同技术平台。

Web 服务和运行时 UDDI 能够解决这种问题,如下所示:总部 IT 部门能够定义一个所有分公司都必须实现的公共 Web 服务接口。此接口在 Microsoft Windows Server 2003 托管的私有 UDDI 服务器中注册为 tModel。分公司可以在任何技术平台上随意实现该服务,但必须遵从强类型 WSDL 接口定义。一旦某家分公司实现了它的销售数据报告服务,则该公司就将在私有 UDDI 服务中注册此 Web 服务,从而创建一个指向此 WSDL tModel 的绑定。此时,总部能够编写一个查询 UDDI 的客户端应用程序,以发现所有实现给定接口的分公司,然后再动态地轮询每家分公司,从而快速聚合结果。在添加或移除子公司时,客户端应用程序将不再需要更改,因为它将通过查询 UDDI 来动态地发现这些新的 Web 服务。

本文的余下部分将概述如何建立这样一个解决方案。为了生成此方案,需要完成以下步骤:

1.创建一个接口 WSDL 定义。

2.在 UDDI 中将该定义注册为 tModel。

3.创建一组实现抽象 WSDL 接口的 Web 服务。

4.在 UDDI 中注册那些实现,并且将它们正确标记为上面提到的接口的给定实现。

5. 编写一个在运行时查询 UDDI 的轮询应用程序,以动态绑定到支持该接口的一组 Web 服务。

通过完成以下将满足此方案的步骤,本文演示了 UDDI 如何能够在运行时用作抽象层,这是一种可以广泛应用于 Web 服务解决方案框架内的多种不同解决方案的设计方法。

步骤 1. 编写一个接口 WSDL 文档

在开始前,首先需要一个从实现信息中分离出来的 WSDL 文件。实际上,充当不可变接口的 WSDL 的概念(从其终结点中分离出来的)对于运行时 UDDI 方案的启用十分重要。将 WSDL 概念化为接口提升了 Web 服务之间面向对象的范例,并且也为采用重复使用接口的设计系统打开了方便之门。

概念化为接口的 WSDL 与由工具自动生成的 WSDL 文件略有不同。这种自动生成的 WSDL 文件可以描述为包含接口信息 终结点信息的“部署文档”。这种 WSDL 文件通常附加到一个给定终结点 — 实际上,在某些情况下,用户在 知道终结点的情况下不可能获得自动生成的 WSDL 文件。例如,在一个 .NET Web 服务中,可以通过将 ?WSDL 附加到 .asmx 页的 URL(即 http://localhost/Service1/HelloWorld.asmx?WSDL)来自动生成 WSDL 文件。如果自动生成的 WSDL 文件知道此 Web 服务自身的终结点,则它就允许客户端为此 Web 服务创建一个代理。

这种自动生成功能非常强大,并且也通过使创建客户端代理的功能非常易于使用而促进了 Web 服务的采用。然而,这种 WSDL 用法不一定改进 Web 服务中的接口重用。通过将 WSDL 概念化为与部署文档 相对的纯接口定义,可以使具有几种实现的单一 WSDL 文件的可能性一目了然。因为 WSDL 文件不要求包括 实体,所以不包含终结点信息的 WSDL 文件完全有效。因此,您可以将 WSDL 文件作为可以重用的纯接口定义来处理,就像处理 COM 中的 IDL 文件一样。

那么,该如何编写一个接口 WSDL 文件呢?第一种方法自然是打开 Notepad(或其他任何 XML 编辑器),并开始手工编写 WSDL。另一种方法是利用 .NET 框架中的 System.Web.Services.Description 类、以编程方式创建 WSDL,并将结果序列化为一个文件。第三种方法是调整已自动生成的 WSDL 文件,以生成一个可重用的接口文档。这种方法类似于使用活动模板库(ATL) 生成一个 IDL 文件,然后调整并修改此 IDL 文件。对于本文,我们将采用这种方法。我还会介绍 .NET Web 服务生成 WSDL 文件的方法,以及 Web.config 文件中可用来修改该行为的一些选项

首先,创建一个非常简单的 Web 服务。实际上,我们将使用以前的 UDDI 运行时文章中用过的同一 Web 服务来报告销售数据。它有一个 GetSalesTotalByRange 函数,该函数传递两个 DateTime 参数并返回两个参数。首先,您可以在 Visual Studio .NET 中创建一个新的名为 SalesReportUSA 的 Web 服务应用程序,并将以下代码作为 SalesReport.asmx 保存到此应用程序中。需要注意的是,此 Web 服务不使用代码隐藏功能,而是在 .asmx 文件中包含了 Web 服务的完整代码。以前的文章在编写代码示例时使用的是 C#,为和谐起见,本文将使用 Microsoft Visual Basic— .NET。

<%@ WebService Language="vb" Class="SalesReportUSA.SalesReport" %>
Imports System
Imports System.Web.Services
Namespace SalesReportUSA
<WebService([Namespace] := "urn:myCompany-com:SalesReport-Interface")> _
Public Class SalesReport
Inherits System.Web.Services.WebService
<WebMethod()> Public Function GetSalesTotalByRange _
(startDate As System.DateTime, endDate As System.DateTime) As Double
Return 5000.0
End Function
End Class
End Namespace

当您使用 Microsoft Internet Explorer 浏览此文件时,可以单击“服务说明”文件来定位到 http://localhost/SalesReportUSA/SalesReport.asmx?WSDL。自动生成的 WSDL 产生以下结果:

<?xml version="1.0" encoding="utf-8"?>
<definitions xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:s="http://www.w3.org/2001/XMLSchema"
xmlns:s0="urn:myCompany-com:SalesReport-Interface"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/"
xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/"
targetNamespace="urn:myCompany-com:SalesReport-Interface"
xmlns="http://schemas.xmlsoap.org/wsdl/">
<types>
<s:schema elementFormDefault="qualified"
targetNamespace="urn:myCompany-com:SalesReport-Interface">
<s:element name="GetSalesTotalByRange">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1" name="startDate"
type="s:dateTime" />
<s:element minOccurs="1" maxOccurs="1" name="endDate"
type="s:dateTime" />
</s:sequence>
</s:complexType>
</s:element>
<s:element name="GetSalesTotalByRangeResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1"
name="GetSalesTotalByRangeResult" type="s:double" />
</s:sequence>
</s:complexType>
</s:element>
<s:element name="double" type="s:double" />
</s:schema>
</types>
<message name="GetSalesTotalByRangeSoapIn">
<part name="parameters" element="s0:GetSalesTotalByRange" />
</message>
<message name="GetSalesTotalByRangeSoapOut">
<part name="parameters" element="s0:GetSalesTotalByRangeResponse" />
</message>
<message name="GetSalesTotalByRangeHttpGetIn">
<part name="startDate" type="s:string" />
<part name="endDate" type="s:string" />
</message>
<message name="GetSalesTotalByRangeHttpGetOut">
<part name="Body" element="s0:double" />
</message>
<message name="GetSalesTotalByRangeHttpPostIn">
<part name="startDate" type="s:string" />
<part name="endDate" type="s:string" />
</message>
<message name="GetSalesTotalByRangeHttpPostOut">
<part name="Body" element="s0:double" />
</message>
<portType name="SalesReportSoap">
<operation name="GetSalesTotalByRange">
<input message="s0:GetSalesTotalByRangeSoapIn" />
<output message="s0:GetSalesTotalByRangeSoapOut" />
</operation>
</portType>
<portType name="SalesReportHttpGet">
<operation name="GetSalesTotalByRange">
<input message="s0:GetSalesTotalByRangeHttpGetIn" />
<output message="s0:GetSalesTotalByRangeHttpGetOut" />
</operation>
</portType>
<portType name="SalesReportHttpPost">
<operation name="GetSalesTotalByRange">
<input message="s0:GetSalesTotalByRangeHttpPostIn" />
<output message="s0:GetSalesTotalByRangeHttpPostOut" />
</operation>
</portType>
<binding name="SalesReportSoap" type="s0:SalesReportSoap">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http"
style="document" />
<operation name="GetSalesTotalByRange">
<soap:operation soapAction="urn:myCompany-com:SalesReport-Interface
/GetSalesTotalByRange" style="document" />
<input>
<soap:body use="literal" />
</input>
<output>
<soap:body use="literal" />
</output>
</operation>
</binding>
<binding name="SalesReportHttpGet" type="s0:SalesReportHttpGet">
<http:binding verb="GET" />
<operation name="GetSalesTotalByRange">
<http:operation location="/GetSalesTotalByRange" />
<input>
<http:urlEncoded />
</input>
<output>
<mime:mimeXml part="Body" />
</output>
</operation>
</binding>
<binding name="SalesReportHttpPost" type="s0:SalesReportHttpPost">
<http:binding verb="POST" />
<operation name="GetSalesTotalByRange">
<http:operation location="/GetSalesTotalByRange" />
<input>
<mime:content type="application/x-www-form-urlencoded" />
</input>
<output>
<mime:mimeXml part="Body" />
</output>
</operation>
</binding>
<service name="SalesReport">
<port name="SalesReportSoap" binding="s0:SalesReportSoap">
<soap:address location="http://localhost/salesreportusa/salesreport.asmx" />
</port>
<port name="SalesReportHttpGet" binding="s0:SalesReportHttpGet">
<http:address location="http://localhost/salesreportusa/salesreport.asmx" />
</port>
<port name="SalesReportHttpPost" binding="s0:SalesReportHttpPost">
<http:address location="http://localhost/salesreportusa/salesreport.asmx" />
</port>
</service>
</definitions>

首先,请注意自动生成的 WSDL 如何分别针对 HTTP-POST、 HTTP-GET 和 SOAP 为 Web 服务创建了三种不同的绑定。为了提供在用户浏览至 Web 服务自身的访问点(在本例中是 SalesReport.asmx)时出现的自动生成的 HTML 格式,.NET 对此进行了改进。此 HTML 页使用户可以通过向查询字符串或 HTTP POST 中传递参数来调用 Web 服务,然后 .NET 呈现 Internet Explorer 中所显示的结果。这种通过浏览 Web 服务访问点来调用 Web 服务的功能是由 .NET 提供的一种非常方便的机制,旨在帮助用户了解 Web 服务及其工作原理。

然而,有些情况下,这种方法则不合适。也许该 Web 服务管理员只希望专门通过 SOAP 提供绑定。就我们的示例而言,我们的目标是编写一个只使用 SOAP 的接口 WSDL 文档,则其他的绑定就不适合。

如果用户希望禁用这种行为,则可以通过修改项目的 Web.config 文件来实现。或者,也可以通过修改 Machine.config 文件跨所有 Web 项目全局关闭此功能。通过在 实体内添加以下 XML,可以关闭 .asmx 文件上的 HTTP-POST 和 HTTP-GET:

<webServices>
<protocols>
<remove name="HttpPost" />
<remove name="HttpGet" />
</protocols>
</webServices>

通过做出此修改,.asmx 页仍将自动生成一个帮助页;但是,它将不再允许用户通过 HTTP GET 或 HTTP POST 调用该 Web 服务。自动生成的 WSDL 也将反映此修改的行为。让我们看看新生成的文件。请注意,现在只有一个操作和绑定,而不是三个。

<?xml version="1.0" encoding="utf-8"?>
<definitions xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:s="http://www.w3.org/2001/XMLSchema"
xmlns:s0="urn:myCompany-com:SalesReport-Interface"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/"
xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/"
targetNamespace="urn:myCompany-com:SalesReport-Interface"
xmlns="http://schemas.xmlsoap.org/wsdl/">
<types>
<s:schema elementFormDefault="qualified"
targetNamespace="urn:myCompany-com:SalesReport-Interface">
<s:element name="GetSalesTotalByRange">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1" name="startDate"
type="s:dateTime" />
<s:element minOccurs="1" maxOccurs="1" name="endDate"
type="s:dateTime" />
</s:sequence>
</s:complexType>
</s:element>
<s:element name="GetSalesTotalByRangeResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1"
name="GetSalesTotalByRangeResult" type="s:double" />
</s:sequence>
</s:complexType>
</s:element>
</s:schema>
</types>
<message name="GetSalesTotalByRangeSoapIn">
<part name="parameters" element="s0:GetSalesTotalByRange" />
</message>
<message name="GetSalesTotalByRangeSoapOut">
<part name="parameters" element="s0:GetSalesTotalByRangeResponse" />
</message>
<portType name="SalesReportSoap">
<operation name="GetSalesTotalByRange">
<input message="s0:GetSalesTotalByRangeSoapIn" />
<output message="s0:GetSalesTotalByRangeSoapOut" />
</operation>
</portType>
<binding name="SalesReportSoap" type="s0:SalesReportSoap">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http"
style="document" />
<operation name="GetSalesTotalByRange">
<soap:operation soapAction="urn:myCompany-com:SalesReport-Interface
/GetSalesTotalByRange" style="document" />
<input>
<soap:body use="literal" />
</input>
<output>
<soap:body use="literal" />
</output>
</operation>
</binding>
<service name="SalesReport">
<port name="SalesReportSoap" binding="s0:SalesReportSoap">
<soap:address location="http://localhost/salesreportusa
/salesreport.asmx" />
</port>
</service>
</definitions>

尽管此自动生成的 WSDL 是我们用以开始的清除程序,但它仍不能满足我们创建一个接口 WSDL 文件(该文件从实现细节中分离出来,并且重新使用已有的架构)的初始需求。请注意,该文档内仍然有一个 实体(已用粗体突出显示),该实体指定了从哪里调用服务。

此信息之所以保留下来,是因为 WSDL 文件被设计为包含两个主要方面的信息:Web 服务的接口签名(包括了 SOAP 负载的 XML 架构,以及所有消息都必须遵从的请求/响应行为)和 Web 服务自身的终结点。这样一个 WSDL 文件包含了客户端对此 Web 服务进行调用所需要的一切内容,并充当了部署文档,对此,上文已进行了讨论。

然而,我们的目的是编写一个纯接口 WSDL 文件。这种修改很容易进行:只要从 WSDL 文件中移除此部署信息便可。通过将自动生成的 WSDL 保存为名为 salesreport.wsdl 的文件,并通过删除整个 实体来调整该文件,就可以得到一个纯 Web 服务接口说明。

还需要注意此 Web 服务的架构是如何嵌入到 WSDL 文件中的。能够对 WSDL 文件做的另一个增强是将嵌入的架构(也是用粗体描述的)从 WSDL 中提取出来,并使用 WSDL 内的 指令从别处导入此架构。这种方式在几种情况下非常有用。架构可能拥有 Web 服务以外的用途。例如,架构可能用于数据库存储信息,或者用于验证通过 Web 服务以外的方法进入系统的 XML。通过将架构导入,而不是嵌入,就能够使架构拥有一个 WSDL 之外的生存期,并且也因此可以对其进行维护和重新使用。

通过移除 信息并导入架构信息,我们最终的接口 WSDL 文档看起来将如下所示:

<?xml version="1.0" encoding="utf-8"?>
<definitions xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:s="http://www.w3.org/2001/XMLSchema"
xmlns:s0="urn:myCompany-com:SalesReport-Interface"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/"
xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/"
targetNamespace="urn:myCompany-com:SalesReport-Interface"
xmlns="http://schemas.xmlsoap.org/wsdl/">
<import namespace="urn:myCompany-com:SalesReport-Interface"
location="salesreport.xsd"/>
<types/>
<message name="GetSalesTotalByRangeSoapIn">
<part name="parameters" element="s0:GetSalesTotalByRange" />
</message>
<message name="GetSalesTotalByRangeSoapOut">
<part name="parameters" element="s0:GetSalesTotalByRangeResponse" />
</message>
<portType name="SalesReportSoap">
<operation name="GetSalesTotalByRange">
<input message="s0:GetSalesTotalByRangeSoapIn" />
<output message="s0:GetSalesTotalByRangeSoapOut" />
</operation>
</portType>
<binding name="SalesReportSoap" type="s0:SalesReportSoap">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http"
style="document" />
<operation name="GetSalesTotalByRange">
<soap:operation soapAction="urn:myCompany-com:SalesReport-Interface
/GetSalesTotalByRange" style="document" />
<input>
<soap:body use="literal" />
</input>
<output>
<soap:body use="literal" />
</output>
</operation>
</binding>
</definitions>

我们将架构放在 WSDL 文件所处的目录中,以使导入命令知道从哪里获得该架构。相应的架构看起来将如下所示:

<?xml version="1.0" encoding="utf-8" ?>
<s:schema xmlns="urn:myCompany-com:SalesReport-Interface"
xmlns:s="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
targetNamespace="urn:myCompany-com:SalesReport-Interface">
<s:element name="GetSalesTotalByRange">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1" name="startDate"
type="s:dateTime" />
<s:element minOccurs="1" maxOccurs="1" name="endDate"
type="s:dateTime" />
</s:sequence>
</s:complexType>
</s:element>
<s:element name="GetSalesTotalByRangeResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1"
name="GetSalesTotalByRangeResult" type="s:double" />
</s:sequence>
</s:complexType>
</s:element>
</s:schema>
</types>

使用这个新的文档集,就可以生成一个供多个实现使用的纯接口。

随后产生的问题是:如何防止用户继续基于自动生成的 WSDL 生成客户端代理?如何将用户引导至这个自定义编写的接口 WSDL?考虑一下我们已有的 salesreport.asmx 文件。虽然它是作为一个 salesreport.wsdl 文件实现存在,但它仍生成自己的 WSDL。如果所需的行为是让 Web 服务访问点只作为 SOAP 侦听程序,而不提供任何文档信息,情况会怎样呢?或者,如果我们希望在用户试图浏览我们的 Web 服务访问点时编写我们自己的文档,情况又会怎样呢?

.NET 通过进一步调整 Web.config 文件对此进行处理。如果不需要文档,并且想要的行为是让访问点只作为 SOAP 访问点,则可以进行以下更改:

<customErrors mode="On" defaultRedirect="error.htm"/>
<webServices>
<protocols>
<remove name="HttpPost" />
<remove name="HttpGet" />
<remove name="Documentation" />
</protocols>
</webServices>

不需要打开自定义错误,但需要为用户显示界面友好的内容。在没有打开 customErrors 时,如果某位用户试图浏览 .asmx 文件,.NET 将会引发异常。

如果您希望无论用户何时试图通过 HTTP-GET 访问此 Web 服务终结点,都能显示一个自定义页,则可以按以下方式修改 Web.config 文件:

<webServices>
<protocols>
<remove name="HttpPost" />
<remove name="HttpGet" />
</protocols>
<wsdlHelpGenerator HREF="documentation.asp"/>
</webServices>

这样一个文档页可能讨论了如何实现此抽象接口、如何查找此接口的实现以及如何编写该接口的客户端。实际上,该文档页是一个将用户定位至 UDDI 的理想位置。

步骤 2:在 UDDI 中注册接口 WSDL 文档

至此,我们已完成了步骤 1,尽管过程十分曲折!下一步涉及的内容将少得多。请记住,就我们的方案而言,每家分公司都需要使用此 WSDL 文件来生成 Web 服务的实例。它们如何找到该文档?

现在是介绍在设计时使用 UDDI 的时候了。通过在 UDDI 中将此 WSDL 文件(以及组织内的所有接口 WSDL 文档)注册为 tModel,就可以使用接口的公共注册表。就我们的方案而言,将这些接口大规模公开使用并没有意义,所以将它们用作内部 UDDI 服务(而不是公共 UDDI 业务注册表)会更合适。UDDI 服务是在 Microsoft Windows Server 2003 中作为可选的外接程序提供的,并且很好地满足了我们的要求。更多关于 HYPERLINK "http://go.microsoft.com/fwlink/?LinkId=5202" UDDI Services in Windows Server 2003 的信息可供下载。

一旦在 Windows Server 2003 上安装了 UDDI 服务,用户就能够通过与 UDDI 服务一起提供的 Web 用户接口,或使用 UDDI .NET SDK、以编程方式来注册 WSDL 文件。有关在 UDDI 中注册信息的更多内容以及使用 Visual Basic .NET 和 C# 编写的一个代码示例,请参阅 Web Service Description and Discovery in UDDI, Part I

进行注册以后,Salesreport.wsdl 文件就有一个由 UDDI 生成的全局唯一标识符标记的 UDDI 条目。然后以编程方式使用该标识符来获取 WSDL 文件的 URL 位置,以及与 UDDI 中WSDL 文件相关的任意元数据(例如,说明信息和分类信息)。实际上,此标识符在下面HYPERLINK "../UsingUDDIatRunTime,PartII.xml" \l "runtimeuddi2_topic6" \t "_self"步骤 5 中编写的轮询应用程序中非常有用,因为我们将要用它来查找给定接口的所有实现。

至此,开发人员现在可以通过在 Web 服务接口中搜索 UDDI 并发布分类搜索或关键字搜索来发现该 WSDL 文件。这种 UDDI 的设计时用法体现了它作为查找和使用此 WSDL 文件的中心位置的重要性。

步骤 3:实现接口

回顾一下,为了生成 WSDL 文件,我们以一个 Web 服务(一个 .asmx 文件)开始,之后倒推出整个方式,最终以一个抽象 WSDL 接口文档结束。现在,让我们看看相反的方法:基于现有的 WSDL 文件实现一个 Web 服务。实际上,这种方法是我们方案的核心,在我们的方案中,每家分公司都必须实现由总部指定的接口。

为了提供此 Web 服务的一个 .NET 实现,我们将需要使用 .NET 框架 SDK 中可用的名为 WSDL.exe 的工具。不能通过 Visual Studio .NET 使用 Add Web Reference 来完成此任务,因为它只能生成客户端代理。在本例中,我们需要一个服务器 stub,将它用作我们实现的一个基础。从 C++ 的角度看,这就像使用 MIDL.exe 基于 IDL 文件生成类,而从 Visual Basic 的角度看,就像使用 Implements 指令基于类型库创建类。

这样,您就可以通过先单击 Program Files,然后单击 Tools 来启动 Visual Studio .NET 命令提示符(也可以通过打开一个命令行外壳程序并浏览至 \Microsoft Visual Studio .NET\FrameworkSDK\Bin 来进行启动)。在没有任何切换的情况下输入 wsdl,您将会看到一个可与此工具一起使用的选项列表。在本例中,我们将需要使用 /server 切换,它将生成一个与代理类相对的抽象 stub 类。通过为输出文件和编程语言首选项添加其他一些切换,我们将得出以下结果:

wsdl.exe /out:c:\salesreport_stub.vb /language:vb /server
http://localhost/salesreportusa/salesreport.wsdl

一旦拥有了这个 stub 类,我们就能够实现 Web 服务。若要实现此抽象类,首先需要创建一个新的 Web 服务项目。在本例中,虚拟目录命名为 SalesReportEurope,因为在我们的方案中,European 部门要实现在 UDDI 中发现的接口。一旦创建了新项目,抽象类 salesreport_stub.vb 就应该添加到此项目上。然后,可以编写一个从抽象类继承来的、名为 Service1.asmx 的新 Web 服务文件。然而,Service1 不是从 System.Web.Services.WebService 继承来的,我们将从由 WSDL.exe 生成的 stub 类 SalesReportSoap 继承:

Public Class Service1
Inherits SalesReportSoap

Visual Studio .NET 有助于确定需要重写的方法以及提供必需的属性和方法签名。

开发人员可以通过从下拉列表中选择需要重写的方法完成该 stub。在我们的示例中,我们将按以下方式完成它:

System.Web.Services.WebMethodAttribute(),
System.Web.Services.Protocols.SoapDocumentMethodAttribute
("urn:myCompany-com:SalesReport-Interface/GetSalesTotalByRange",
RequestNamespace:="urn:myCompany-com:SalesReport-Interface",
ResponseNamespace:="urn:myCompany-com:SalesReport-Interface",
Use:=System.Web.Services.Description.SoapBindingUse.Literal,
ParameterStyle:=System.Web.Services.Protocols.
SoapParameterStyle.Wrapped)> _
Public Overrides Function GetSalesTotalByRange _
(ByVal startDate As Date, ByVal endDate As Date) As Double
Return 8000.0
End Function

开发人员不必为正确设置此 Web 服务的属性而担心,因为一些工具提供了必需的 WebMethodAttribute。开发人员只需基于输入参数执行必需的操作来检索此数据。当然,我们的示例返回了静态数据,并且真实环境的应用程序将会进行某种数据库调用。还要注意到,我们可能希望根据本文前面所做的推荐修改这个新 Web 项目的 Web.config 文件。

步骤 4:在 UDDI 中注册实现

既然一个子公司已经编写了此接口 WSDL 文档的一个实现,那么还需要在 UDDI 中正确地注册该实现。正确注册此服务的首要条件是在 UDDI 中表明该服务确实符合 WSDL 接口。这是通过创建该服务绑定下的 tModelInstanceInfo 条目来实现的,该条目可以正确引用 WSDL 接口(该接口已在步骤 2 中注册过)的 tModel 关键字(有关在 UDDI 中对 Web 服务进行建模和注册的更多信息,请参阅 Web Service Description and Discovery in UDDI, Part I)

在 Windows .NET Server Beta 3 的 UDDI 服务中,这种条目看起来可能如图 1 所示:

runtimeuddi2_01

1. 一个 tModelInstanceInfo 条目示例

请注意,此 Web 服务的访问点拥有一个指向它所实现的 WSDL 的接口指针,该指针在图形上是用传统的 UML 接口棒糖符号外加一个指针箭头表示的。我们还可以看看 tModel 关键字,它是一个可以用来在 UDDI API 调用内表示接口的 GUID。

如果每家不同的子公司都注册了其实现,并且都正确指向了同一 tModel,则用户就可以发出一个 UDDI API 调用来查找某个给定接口定义的所有实现。

当 UDDI 在 find_business、find_service 或 find_binding API 调用中传递 tModelBag 中的 tModelKey 时,该调用将会被提供。这种 find_service 调用的 XML 表示形式如下所示:

<find_service businessKey="" generic="1.0" xmlns="urn:uddi-org:api">
<tModelBag>
<tModelKey>uuid:f1bff5d1-c53d-4279-ab13-02b51889c611</tModelKey>
<tModelBag>
</find_service>

请注意,我们正在传递一个空的 businessKey 属性,因为我们希望搜索所有的企业和提供商,以查找该 WSDL 文件的所有实现。我们可以传递一个实际的 businessKey 并缩小我们的搜索范围,以便只查询给定企业或提供商下的服务。

您也可以通过 Web 用户接口发出此查询。以这种方式进行查询,总部的 CIO 在设计时就能够发现哪些使用此销售报告接口 WSDL 文档的分公司正在采用并运行工作实现。

当使用 UDDI .NET SDK、以编程方式在客户端轮询应用程序中发出这类查询时,事情就变得非常有趣,这需要我们看一下步骤 5 了。

步骤 5:编写客户端轮询应用程序

现在我们可以梳理一下方案。我们需要编写一个具有以下两种功能的应用程序:(1). 能够查询 UDDI 以确定所有已实现此接口的分公司;(2). 能够生成一个调用此 Web 服务所有活动实例的报告。在本例中,我们将编写一个 Visual Basic .NET Windows 窗体,用以报告数据。

首先,在 Visual Basic .NET 中创建一个新的 Windows 应用程序。请下载 UDDI .NET SDK,并将它作为一个引用添加至项目。然后,将以下 Imports 指令添加至项目的顶端:

Imports Microsoft.Uddi
Imports Microsoft.Uddi.Binding
Imports Microsoft.Uddi.Business
Imports Microsoft.Uddi.Service
Imports Microsoft.Uddi.ServiceType
Imports System.Configuration

在进一步编码之前,我们将创建一个 App.config 文件来存储关于我们将访问的 UDDI 服务的信息,以及关于我们感兴趣的 tModelKey GUID 的信息。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="UDDI_URL" value="http://localhost/uddipublic/inquire.asmx" />
<add key="tModelKey" value="uuid:f1bff5d1-c53d-4279-ab13-02b51889c611" />
</appSettings>
</configuration>

接下来,我们使用 App.config 文件中的值初始化窗体的全局范围变量。我们还要初始化后面用来显示信息的 DataTable:

Private InquiryURL As String = ConfigurationSettings.AppSettings("UDDI_URL")
Private SalesReportTModel As String = ConfigurationSettings.AppSettings("tModelKey")
private DataTable dt = new DataTable("SalesReport");

我们可以在窗体上创建一些控件,包括两个 groupBox、一个 checkedListBox、一个 dataGrid、两个 dateTimePicker、一个标签以及两个按钮,这样,我们就得到如图 2 所示的结果:

runtimeuddi2_02

2. Windows 窗体上创建控件

接下来,我们将要创建一个自定义类,以用来在 checkedListBox 中存储由 UDDI 返回的信息。如下所示:

'storage class for UDDI data
Public Class UDDIInfo
Private m_name As String
Private m_accesspoint As String
Private m_serviceKey As String
Public Property Name() As String
Get
Return m_name
End Get
Set(ByVal Value As String)
m_name = value
End Set
End Property
Public Property Accesspoint() As String
Get
Return m_accesspoint
End Get
Set(ByVal Value As String)
m_accesspoint = value
End Set
End Property
Public Property ServiceKey() As String
Get
Return m_serviceKey
End Get
Set(ByVal Value As String)
m_serviceKey = value
End Set
End Property
End Class

在将代码添加至 On_Click 事件处理程序以前,我们首先要注意一下表明窗体何时加载的代码:

Public Sub New()
MyBase.New()
' This call is required by the Windows Form Designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call
' set UDDI inquiry location
Inquire.Url = InquiryURL
' set up data grid
dt.Columns.Add("Name")
dt.Columns("Name").Caption = "Name"
dt.Columns("Name").ColumnName = "Name"
dt.Columns.Add("Access Point")
dt.Columns("Access Point").Caption = "Access Point"
dt.Columns("Access Point").ColumnName = "Access Point"
dt.Columns.Add("Sales")
dt.Columns("Sales").Caption = "Sales"
dt.Columns("Sales").ColumnName = "Sales"
DataGrid1.DataSource = dt
DataGrid1.ReadOnly = True
' uses name property of UDDIInfo in display
CheckedListBox1.DisplayMember = "Name"
End Sub

然后再用代码填充 Button1 _Click 事件处理程序,该代码将查询 UDDI 以获得给定 tModelKey 实现的所有服务。首先,我们将使用包含表示此 WSDL 接口的 tModelKey 的 tModelBag 来调用 find_service API。这将为我们提供一个支持此接口的服务信息列表。在 UDDI 中,因为每个服务都可以包含多个访问点,所以我们随后需要调用每个服务上的 find_binding,然后再传递 tModelKey。find_binding 调用产生的结果将包含正确的访问点。我们会将访问点信息和一些其他显示信息存储到自定义 UDDI 对象中,然后将该对象添加至 checkedListBox

Private Sub Button1_Click(ByVal sender As System.Object,
ByVal e As System.EventArgs) Handles Button1.Click
Cursor.Current = Cursors.WaitCursor
'clear checkbox
CheckedListBox1.Items.Clear()
' do a findService with the tModelKey
' this will return a list of services that implement
' the tModel -- we will then have to iterate over
' that list and make additional calls to UDDI
Dim fs As New FindService()
' pass the interface tModel key
fs.TModelKeys.Add(SalesReportTModel)
' pass an empty BusinessKey so that we search across all providers
' in the UDDI Services registry
fs.BusinessKey = ""
Dim sl As New ServiceList()
Try
sl = fs.Send()
Catch ue As UddiException
MessageBox.Show(ue.StackTrace)
Catch ex As Exception
MessageBox.Show(ex.Message)
End Try
' now that we have the list of services that do in fact implement
' that tModel, we can issue a find_binding,
' passing the appropriate serviceKey
Dim fbind As New FindBinding()
Dim bindd As New BindingDetail()
fbind.TModelKeys.Add(SalesReportTModel)
Dim si As ServiceInfo
For Each si In sl.ServiceInfos
fbind.ServiceKey = si.ServiceKey
Try
bindd = fbind.Send()
Catch ue As UddiException
MessageBox.Show(ue.Message)
Catch ex As Exception
MessageBox.Show(ex.Message)
End Try
'grab the accessPoints
Dim bt As BindingTemplate
For Each bt In bindd.BindingTemplates
'create a UDDIInfo custom class
Dim ui As New UDDIInfo()
ui.Accesspoint = bt.AccessPoint.Text
ui.Name = si.Name
ui.ServiceKey = si.ServiceKey
'add it to the collection
CheckedListBox1.Items.Add(ui, True)
Next bt
Next si
Label1.Text = "UDDI Cache Updated With Latest Access Points From UDDI"
Cursor.Current = Cursors.Default
End Sub

至此,我们应用程序的一半已完成。至于缓存,我们使用一个内存中的缓存来存储从 UDDI API 查询中收集的信息。该缓存将存在于应用程序的整个生存期中。若要获得更持久的缓存,我们可以将此数据序列化到一个 XML 文件中,以便当用户启动应用程序时,可以从文件中取出这组访问点,而无需重新查询 UDDI。

不管使用什么技术,对 UDDI 的结果进行缓存对任何运行时 UDDI 方案来说都是至关重要 的。调用 UDDI 的开销相对较大,因此只应在绝对必要时进行调用。

现在,我们可以完成应用程序的另一半:基于一个公共接口调用 Web 服务。为了对这部分应用程序进行编码,我们首先需要使用 Add Web Reference 或 WSDL.exe 为应用程序生成一个代理类。通过浏览至 UDDI 以发现该文件的 URL,或者输入文件名本身,我们能够基于 http://localhost/SalesReportUSA/salesreport.wsdl 生成一个代理类,如果您看一看为此代理生成的代码,就会注意到没有该类的 URL 属性集。然而,我们将会在运行时设置 URL 属性。

既然我们已基于接口 WSDL 文档获得了一般代理类,我们就能够编写动态调用每个 Web 服务的代码。当用户单击 Generate Report 按钮时,代码将循环选中的 listbox(以便从每个 UddiInfo 对象中提取信息),并动态地将正确的访问点插入到代理类中。

Private Sub Button2_Click _
(ByVal sender As System.Object,
ByVal e As System.EventArgs) Handles Button2.Click
Cursor.Current = Cursors.WaitCursor
' clear the datagrid
dt.Clear()
' create an instance of our proxy class
Dim salesReport As New localhost.SalesReportSoap()
' create some variables for our report
Dim figure As Double = 0
Dim total As Double = 0
Dim arr(2) As String
'iterate through checked list box
Dim i As Integer
Dim ui As New UDDIInfo()
For i = 0 To CheckedListBox1.Items.Count - 1
' only use the checked boxes
If CheckedListBox1.GetItemChecked(i) Then
' extract UDDI object from list
ui = CType(CheckedListBox1.Items(i), UDDIInfo)
Try
' alter the proxy class, setting the URL property
salesReport.Url = ui.Accesspoint
' call the method to invoke the web service
figure = salesReport.GetSalesTotalByRange _
(DateTimePicker1.Value, DateTimePicker2.Value)
' update our total
total += figure
' add some properties to the array and insert the array into the datagrid
arr(0) = ui.Name
arr(1) = ui.Accesspoint
arr(2) = figure.ToString()
dt.Rows.Add(arr)
Catch err As Exception
MessageBox.Show _
(ui.Accesspoint + " failed:" + err.StackTrace + " ")
End Try
End If
Next i
' add a final row to the grid with the total aggregated information
arr(0) = "Total:"
arr(1) = ""
arr(2) = total.ToString()
dt.Rows.Add(arr)
End Sub

请注意我们是如何仅仅实例化代理一次便反复使用该对象的,只需在该代理上为每个访问点设置一个新的 URL 属性即可。

 

小结

本文描述了如何在运行时将 UDDI 作为 Web 服务客户端和服务器之间的抽象层使用。为了达到此目的,将 WSDL 文档看成是与部署文档相对的接口定义非常重要。通过将 WSDL 用作接口定义,我们可以创建多个共享一个公共接口的 Web 服务。然后,通过在 UDDI 中正确地注册那些 Web 服务,可以在运行时利用 UDDI,以基于 UDDI API 调用动态绑定至 Web。还要注意到,最小的 UDDI 查询是通过缓存查询的结果而生成的。

本文只是粗略讨论了将 UDDI 用作客户端和服务器之间的代理的可能性。以后的文章将会深入研究此概念,看看分类方案如何应用于 UDDI 数据,通过运行时 UDDI 查询在 UDDI 注册表中创建本体论,以用来具体化数据。在发出基于这些分类方案的运行时查询时,由此设计方法引出的可能性和方案是无穷的。

posted on 2006-04-26 08:46  落拓孤鸿  阅读(365)  评论(0编辑  收藏  举报