1、了解 .NET 中的企业服务 (COM+)Shannon PahlMicrosoft Corporation 2002 年 4 月摘要:本文提供了蕴含在 Microsoft .NET 与 COM+ 服务集成中的详细技术信息,并介绍了可用于托管代码的服务。 COM 提供了一种编写基于组件的应用程序的方法。众所周知,编写 COM 组件需要进行大量重复的琐碎工作。而 COM+ 并不完全是 COM 的新版本,实际上,COM+ 为组件提供了一个服务基础结构。组件在构建后安装到 COM+ 应用程序中,可以建立易于部署、吞吐量高、可缩放的服务器应用程序。(如果组件不需要使用任何服务,则不应放到 COM+ 应
2、用程序中。)为了达到可缩放性和吞吐量目标,需要从一开始就使用事务、对象池和活动语义等服务来设计应用程序。.NET 框架提供了另一种编写基于组件的应用程序的方法,与 COM 编程模型相比,它具有更好的工具支持、公共语言运行时 (CLR) 和更简单的编码语法等优势。COM+ 服务基础结构可以从托管和非托管代码进行访问。非托管代码中的服务称为 COM+ 服务。在 .NET 中,这些服务被称为企业服务。从 ServicedComponent 派生的类表明某个组件将需要服务。(如果组件不需要使用任何服务,则不应从 ServicedComponent 派生。)改进的工具支持使编程人员能够编写基于服务器的应
3、用程序,而可缩放性和吞吐量问题仍需要通过良好的编程实践来实现。服务背后的基本理念是,从一开始就考虑吞吐量和可缩放性的设计,并利用企业服务在适当的位置轻松地实现那些设计模式。有人可能会提出异议,服务基础结构设计实际上与 COM 甚至组件都没有多大关系:现在,COM+ 服务可以用于 COM 组件、.NET 组件、甚至其他不能称为组件的实体,如 ASP 页或任意代码块等(请参阅 Microsoft Windows XP 上的无组件服务 COM+ 功能)。今天,所有可用的 COM+ 服务都可以用于 .NET 和 COM 对象。其中一些服务包括:事务、对象池和构造字符串、JIT、同步、基于角色的安全性、
4、CRM 和 BYOT 等。有关 Microsoft Windows 2000 上的服务的完整列表,请参阅 Platform SDK 中的“COM+ 提供的服务” 。Microsoft Windows XP 包括 COM+ 的一个新版本,称为 COM+ 1.5,它具有一些附加服务,也可以用于 .NET 组件。事务为编写使用服务的托管应用程序,必须从 ServicedComponent 中派生需要服务的类,并使用各种自定义属性来指定所需的实际服务。本节介绍这些概念以及它们如何影响托管代码的编写。后面几节将进行详细说明。假设编写了一个 Account 类(其实际代码将在后面列出)并放置在 BankC
5、omponent 程序集中。则可以按照以下方法使用该类:BankComponent 客户端using system;using BankComponent;namespace BankComponentClientclass Clientpublic static int Main() Account act = new Account();act.Post(5, 100);act.Dispose();return 0;要建立该客户端,必须向 BankComponent 命名空间添加引用。此外,还必须为 System.EnterpriseServices 程序集添加引用 - 在 BankCom
6、ponentClient 命名空间中,客户端将调用 Dispose() 和 ServicedComponent 构造函数,它们是在 System.EnterpriseServices 中定义的方法,而不是在包含 BankComponent 的程序集中定义的。当派生类没有重载所有基类方法时,.NET 通常要求使用这种处理方式。BankComponent 服务器代码显示了在 .NET 中使用事务的 Account 类的实现。Account 类是从 System.EnterpriseServices.ServicedComponent 类中派生的。Transaction 属性将该类标记为需要一个事务
7、。由于使用了 Transaction 属性,所以将自动配置同步和 JIT 服务。AutoComplete 属性用于指定:如果在方法执行过程中出现未处理的异常,运行时必须为该事务自动调用 SetAbort 函数,否则,将调用 SetComplete 函数。ApplicationName 属性将此程序集与为此应用程序存储服务配置数据的 COM+ 应用程序关联起来。该类所需的进一步修改已在代码中标明。BankComponent 服务器using System.EnterpriseServices;assembly: ApplicationName(“BankComponent“)assembly:
8、AssemblyKeyFileAttribute(“Demos.snk“)namespace BankComponentServerTransaction(TransactionOption.Required)public class Account : ServicedComponentAutoCompletepublic bool Post(int accountNum, double amount)/ 更新数据库,不必调用 SetComplete。/ 如果没有出现异常,则自动调用 SetComplete。从 BankComponent 服务器命名空间中的代码可以看出,在 .NET 中使用
9、 COM+ 服务是很容易的。下面简单列出了从编码到部署的整个过程: 1. 编写服务器程序集。 2. 建立程序集: a. 对程序集签名。可以为项目一次性地生成密钥文件,而不必为每次编译都生成密钥文件。要创建密钥,可以在 Microsoft .NET 命令提示下使用 sn.exe:b. sn k Demos.snkc. 编译代码。必须为 System.EnterpriseServices 添加引用。 3. 部署应用程序。 必须在 COM+ 目录中注册使用受服务组件的程序集。 ServicedComponent 类和自定义属性是从托管代码访问 COM+ 服务的两个关键概念。服务的配置存储在 COM+
10、 目录中。对象在 CLR 中驻留和执行。图 1 显示了托管对象及其相关联的 COM+ 上下文,在下面的两节中会更清楚。 图 1:与托管组件相关联的服务 使用 COM+ 组件时需要手动配置目录,而使用受服务组件时,可以根据代码中的属性来更新目录。使用命令行工具 regsvcs.exe 或通过编写访问托管 API 的脚本可以显式注册程序集。后面的“ 部署” 一节提供了详细的信息。在开发过程中,为方便起见,提供了 XCopy 部署,即简单地将程序集复制到应用程序目录中。每当客户端应用程序为从 ServicedComponent 中派生的类创建实例时,运行时都将检测是否已在 COM+ 应用程序中注册了
11、该程序集。如果没有注册,则在本地目录中搜索程序集,如果找到了,该程序集中所有受服务组件都将在 COM+ 应用程序中注册,然后激活。这一过程称为迟缓注册,但并不适合所有情况。例如,标记为 COM+ 服务器应用程序的程序集需要显式注册(如下所示),迟缓注册不适合调用托管受服务组件的非托管客户端。迟缓注册在开发时很有用,因为如果没有它,就需要使用脚本、代码或 RegSvcs 来注册程序集。 4. 可以将程序集放到 GAC 中。有关详细信息,请参阅“部署”一节。 5. 运行客户端。 部署自定义属性是从托管代码访问 COM+ 服务的两个关键概念之一。自定义属性用于指定所需的服务,如上述代码中的 Tran
12、saction 自定义属性。这些属性在程序集的元数据中存储了服务的配置选项。自定义属性的使用方式是:让某段代码加载程序集,然后使用反射来创建属性的实例并对其调用方法,从而提取存储在属性中的服务配置。然后,可以将该信息写入到 COM+ 目录中。执行这些步骤和其他步骤的代码包含在 EnterpriseServices.RegistrationHelper 中。为使注册过程更简单,所有注册窗体都使用了 EnterpriseServices.RegistrationHelper 组件。该组件可以作为托管类和 COM 对象来访问。图 2:注册受服务组件从概念上讲,RegistrationHelper 执
13、行了以下步骤: 使用 RegistrationServices.RegisterAssembly 在注册表中注册程序集。因此,在注册表中,类是作为以托管代码编写的 COM 组件出现的,并且具有指向 mscoree.dll 的 InprocServer32 键。如果托管类没有实现任何接口,则除非使用 ClassInterfaceAttribute,否则该类的公共方法不会出现在 COM+ 目录中。这意味着,与方法级相关联的服务配置不能存储在目录中。但是,某些 COM+ 服务可以在方法级上进行配置,并要求组件象在 COM+ 目录中显示的那样公开接口。例如,方法级上的 COM+ 基于角色的安全性要求组
14、件实现一个接口以配置服务。此问题将在“安全性”一节中进行详细讨论。 使用 TypeLibConverter.ConvertAssemblyToTypeLib 从程序集生成 COM 类型库。 注册类型库。到目前为止,它与 RegAsm.exe /tlb 完全相同。 查找或创建 COM+ 应用程序。其名称是从 ApplicationName 属性、程序集名称或所提供的应用程序的名称/GUID 中提取的。 用类型库配置使用 COM+ admin API 的 COM+ 应用程序。 检查所有自定义属性,并使用 IConfigurationAttribute 将特定服务的配置数据写入到 COM+ 目录中。
15、 RegistrationHelper 将试图使用 RegistrationHelperTx 在事务中执行这些步骤。RegistrationHelperTx 是在安装 .NET 时创建的 COM+ 应用程序中的一个类。因此,如果注册失败,COM+ 目录和注册表将恢复到其原始状态。但目前,生成的类型库将仍保留在磁盘上(或者在 GAC 中,如果程序集在 GAC 中)。如果正在注册的程序集引用了同样使用 COM+ 服务的其他程序集,则相关图中的所有程序集将执行上述相同的步骤。由于 RegistrationHelper 要访问 COM+ 目录,因而需要具有计算机上的非托管代码权限和管理权限。因此,对于
16、 RegistrationHelper 的客户端也是一样,如迟缓注册、RegSvcs 或您的脚本/代码。这还意味着从 Internet 上下载的代码或存储在网络共享上的代码将不能进行注册。可以编写不兼容的属性组合,例如,请求一个 Transaction 并将 Synchronization 设置为禁用。当前,这些组合是在注册时检测的(当把它们写入到 COM+ 目录中时),而不是在编译时检测的。某些属性具有与其他属性的相关性,例如,当只使用 Transaction 属性时,相当于使用 Transaction、JustInTimeActivation 和 Synchronization 属性。注册
17、托管组件时,如果不使用属性覆盖“未配置的” 默认值,将使用 COM+ 目录的默认值。例如,如果注册一个组件并且没有指定 Transaction 属性,则目录中事务设置的未配置默认值将设置为 TransactionOption.Disabled。这种方法使开发人员在组件不再需要某个属性时,可以从代码中删除它,然后,当再次注册程序集时,再适当地重新设置事务的目录条目。联机文档中给出了这些未配置默认值的详细列表。默认的配置值是属性参数中的默认值,例如,只使用属性 Transaction 表示 TransactionOption.Required。由于托管类上的服务的配置数据存储在 COM+ 目录中,
18、因此在注册程序集后,也可以通过管理的方式修改某些目录条目。但某些服务不能以这种方式修改。例如,在目录中禁用事务服务可能导致代码运行不正常。部署特有的设置(如对象构造字符串和安全性角色)可以在注册后进行处理。但如果在注册后设置,对于包含受服务组件的程序集的 XCopy 部署可能是不够的。COM+ 应用程序的导入和导出功能可以帮助分配应用程序的当前状态。有关导入和导出的进一步信息将在“远程组件”一节中介绍。在某些情况下,配置数据并不参考目录,而只是从程序集元数据中提取。这些情况包括自动完成、JIT、对象池(尽管池的大小是从目录中提取的)和安全方法属性。有关此问题的详细信息将在各个服务的相关章节中讨
19、论。注册程序集的进程将自动生成 COM+ 所需的 GUID。如果程序集没有签名,则只根据类型和命名空间的名称生成 GUID。因此,如果程序集没有签名,则可能生成非唯一的 GUID。.NET 程序集可能遇到类似的情况,它甚至没有使用 COM+ 服务,但是需要唯一的类型名称。因此,必须对使用 COM+ 服务的程序集签名。如果程序集没有签名,注册将失败。注册还意味着使用 COM+ 服务的 .NET 类具有一个全局配置数据存储。尽管有可能将专用程序集复制到多个应用程序目录中,但最终所有这些应用程序都引用受服务组件的一个配置数据。因此,更改 COM+ 目录中的配置数据将影响使用该类的所有应用程序。这一点
20、对于 Microsoft ASP.NET 配置中的多个 vroot 是很显然的,这些 vroot 都包括使用受服务组件的相同程序集的副本。使相同的 COM+ 应用程序具有多个配置的一种方法是在 Microsoft Windows .NET 上使用 COM+ 分区。要在 .NET 中使用 COM+ 分区服务,请不要使用 ApplicationID 属性 - 为在多个分区内安装相同的组件,COM+ 需要唯一的应用程序 ID。通常,当客户端需要访问不在客户端应用程序目录中的程序集时,或者如果程序集被加载到不在客户端目录中的其他进程中时,请使用 GAC。从概念上讲,使用受服务组件的专用程序集实际上是共
21、享程序集 - 它们使用共享的配置数据。如果 ApplicationActivationOption 被设置为库,则有可能在程序集中的类上使用事务,并且,如果所有程序集都加载自同一个目录,则可以在一个客户端使用该程序集。当使用 ApplicationActivationOption 的程序集被设置为服务器时,该程序集将由 dllhost.exe(通常不在客户端目录中)加载。使用 COM+ 服务器应用程序中的受服务组件的程序集应放置在 GAC 中。使用 COM+ 库应用程序中的受服务组件的程序集则不必放置在 GAC 中(除非它们位于不同的目录)。唯一的例外是组件保留在 ASP.NET 中时 - 程
22、序集不应放置到 GAC 中,这样才能使阴影副本能够正常运行。要删除使用受服务组件的 .NET 应用程序,应先从 GAC 中删除程序集(如果它是使用 GAC 注册的),使用 regsvcs.exe 从 COM+ 中取消程序集的注册,然后删除程序集及相关联的类型库。版本控制使用 GUID 属性可以确定 COM+ 所需的 GUID。但建议使用版本控制,而不是明确地使用 GUID。当创建新的方法签名,或当类具有不同的服务属性时,应递增程序集的主版本号或次版本号。每个版本都应进行一次注册。注册程序集的新版本时,将为该版本生成新的 GUID,而且组件将使用相同的组件名称在相同的 COM+ 应用程序中注册。
23、因此,组件会在 COM+ 应用程序中多次出现。但每个组件都有由 GUID 给定的唯一 ID。每个实例都引用该组件的一个特定版本。当使用 Microsoft Visual Studio .NET 建立 .NET 应用程序时,经常会遇到类似情况。环境将属性 assembly:AssemblyVersion(“1.0.*“) 添加到项目上。每次新建项目时都将生成新的内部版本号,因此,当重新注册程序集时,将生成新的 GUID。所以,最好在适当的时候手动递增内部版本号。客户端将使用 CLR 版本策略绑定到程序集上,从而能够使用 COM+ 应用程序中的类的正确版本。编写使用受服务组件的程序集(托管服务器)
24、时的一些类似情况包括:(下面使用了激活的某些方面,这些内容将在下一节中介绍) 托管客户端,托管服务器,在程序集中没有使用确定的 GUID。 客户端将加载由版本策略指定的程序集。 托管客户端,托管服务器,使用确定的 GUID。 如果客户端激活了一个类并使用版本策略获取程序集的早期版本,则在激活过程中将使用代码中的确定的 GUID 从目录中提取服务信息。因此,来自使用此 GUID 的上次注册的程序集的信息将被用于创建对象,而实际上,该对象可能是一个新版本,因此,当试图从实际创建的对象 (v2) 向代码中的引用 (v1) 进行强制类型转换时,可能会出现类型转换异常。 托管客户端,托管服务器,没有确定
25、的 GUID,只更改内部版本号。 尽管将生成新的 GUID,但由于类型库只有两个版本号,所以它将仍保留相同的版本号。但是如果版本 2 安装在版本 1 之上,则卸载版本 1 时,版本 2 的类型库将取消注册,从而无法工作。解决方案 1:.NET 框架的下一个版本 (V1.1) 通过启用独立于程序集的类型库版本控制,解决了这个问题。这意味着,更改程序集的版本号时,也应更改类型库的版本。解决方案 2:只使用主版本号和次版本号。 非托管客户端,托管服务器,没有使用确定的 GUID。 客户端将使用 GUID 创建组件。互操作性将把 GUID 解析为一个名称,然后应用版本策略。如果计算机上具有程序集的版本
26、 1 和版本 2,并使用策略获取版本 2,则非托管客户端将获取版本 2。 安装版本 1,安装版本 2,卸载版本 1。现在,除非版本策略重新指向版本 2,否则客户端将不能创建组件。此外,还必须存在包含版本 1 的注册信息的注册表项。要为卸载的版本 1 创建注册表信息,可以使用 Windows XP 上的 COM+ 别名功能。 版本控制应用于同一 COM+ 应用程序中的所有组件,也就是说,不能为应用程序自动添加版本。例如,不能使用版本策略为应用程序上的角色添加版本。要为应用程序添加版本,请使用应用程序名称属性。受服务组件激活企业服务基础结构是建立在上下文概念的基础之上的。上下文是具有类似执行要求的
27、对象的环境。可以在激活和/ 或方法调用截取的过程中实施服务。尽管 COM+ 服务是用非托管代码编写的,但 COM+ 服务与 .NET 的集成要比仅在 .NET 中使用 COM 互操作技术强大得多。如果不从 ServicedComponent 中派生,注册进程将不会获得预期的效果。受服务组件可以用各种组合方式予以激活和保留。如图 3 所示,这里将讨论三种情况:进程内(相同的应用程序域)、应用程序域间(相同的进程)和进程间的激活。这些情况的重点在于调用组件时所跨越的边界。进程内激活可能会跨越上下文边界,在应用程序域间的情况下会跨越上下文边界和应用程序域的边界,而在进程间情况下将处理跨计算机、跨进程
28、和跨上下文的边界。图 3:受服务组件的激活宿主受服务组件的实现依赖于 .NET Remoting,它为插入以非托管或托管代码编写的服务提供了可扩展的机制。受服务组件是从 ContextBoundObject 中派生的,并能实现诸如 IDisposable 等各种接口。使用 ProxyAttribute 派生的自定义属性可以轻易地自定义 CLR 中的激活链。通过编写自定义的真正代理可以自定义截取。当需要新的受服务组件派生类时,可以自定义激活链以使激活调用真正地调用 CoCreateInstance 的托管 C+ 包装程序。这将使 COM+ 能够根据存储在以前注册的程序集的 COM+ 目录中的信息
29、设置非托管的上下文和服务。这也是实现迟缓注册的阶段。在程序集的注册过程中,InprocServer32 键指向 mscoree.dll,因此最终将 COM+ CreateInstance 重新定向到运行时,以创建真正的托管对象。因此,在激活过程中,将创建一个自定义的真正代理对象。此代理的进程内版本被称作受服务组件代理或 SCP,如图 4 所示。图 4:激活路径从激活调用返回的路径将封送托管代码中的托管引用,通过非托管的 COM+,返回到托管代码中(图 4 中线路 1 的相反路径)。根据真正对象的创建位置,客户端将引用拆封到相关窗体中。在进程内激活的情况下,引用被拆封为对透明代理 (TP) 的直
30、接引用,如图 5 所示。应用程序域间的引用被拆封为 .NET Remoting 代理。进程间或计算机间的引用(图 6)需要更多的拆封处理:COM 互操作调用由 ServicedComponent 在激活和拆封过程中实现的 IManagedObject。远程受服务组件代理 (RSCP) 在激活过程中调用 IServicedComponentInfo 以获取服务器对象的 URI,这意味着在激活过程中进行了两次远程调用。当方法级上需要 COM+ 基于角色的安全性时,这些接口需要与一个角色相关联,以便在基础结构调用这些接口时成功地进行拆封处理。“ 安全性” 一节将讨论进程间激活和拆封处理对配置基于角色
31、的安全性的影响。图 5:进程内调用的基础结构图 6:进程外调用的基础结构因此,激活链已被自定义,以创建自定义的真正代理(用于截取)和创建非托管的上下文,只给 COM+ 留下了执行截取服务的语义所需的上下文基础结构。现在,COM+ 上下文与托管对象相关联,而不是与 COM 对象相关联。截取图 7 显示了进程内方法调用的基础结构。自定义代理 (SCP) 使得能够截取托管调用。在激活过程中,COM+ 上下文 ID 存储在 SCP 中。当一个托管对象调用受服务组件时,存储在目标 SCP 中的上下文 ID 将与当前的上下文 ID 进行比较。如果上下文 ID 相同,则直接在真正的对象上执行调用。如果上下文
32、 ID 不同,SCP 将调用 COM+ 以切换上下文并呈现输入方法调用的服务。对于进程内的调用,与 AppDomain.DoCallBack 类似,只是 AppDomain 为 COM+。DoCallBack 函数首先进入 COM+(图 7 中的步骤 2),它将切换上下文并呈现服务,然后回调 SCP 上的函数调用。SCP 进行数据封送并在真正对象上调用方法。当方法退出时,返回路径允许 COM+ 呈现离开方法调用的语义(图 7 中的步骤 5)。COM+ 仅用于呈现服务。数据封送和方法调用是在 .NET 运行时中进行的,这样在调用方法时就不必进行类型转换(如从 String 转换到 BSTR)。如
33、果进程内调用使用了 COM 互操作,则需要进行数据封送。因此,以非托管代码呈现服务的调用不是进程内调用的 COM 互操作调用。图 7:进程内调用的基础结构对静态方法的调用不转发到透明和真正的代理。因此,静态方法不能使用截取服务,它们是在客户端的上下文中被调用的。内部方法将在正确的上下文中被调用,这意味着,在为新事务配置的对象上调用内部方法的客户端将参与到新事务中。但是,由于方法级服务需要 COM+ 目录中的一个接口(在下一节和 “安全性” 一节中将详细介绍该主题),因此不能为方法级服务配置内部方法。服务可以应用到属性上,但方法级属性(如 AutoComplete)必须分别放在获得者和设置者方法
34、上。AutoComplete 属性是使用事务的一种方便方法,无需编写任何代码以访问该服务。此外,还可以使用 ContextUtil.SetAbort 或 ContextUtil.SetComplete。该服务可以在 COM+ 资源管理器中通过设置方法属性的复选框进行配置。但托管对象不需要实现接口。受服务组件也是这样。如果接口上没有声明方法,方法级服务的配置将不能写入到注册的目录中,配置只能存储在元数据中。如果方法没有接口,上下文切换将从 SCP 中进行,使用存储在 IRemoteDispatch.RemoteDispatchAutoDone 上的配置信息(如果存在 AutoComplete 属
35、性)。如果不存在 AutoComplete,则使用 IRemoteDispatch.RemoteDispatchNotAutoDone。IRemoteDispatch 是由 ServicedComponent 实现的一个接口。非托管客户端只能使用 IDispatch(后期绑定)调用没有接口的受服务组件,因此,由于在那种情况下没有真正的代理,因而不能实施 AutoComplete 语义。即使在使用接口时,AutoComplete 的配置仍然由托管客户端的元数据进行驱动。只有在进程外的情况下,才在 RemoteDispatchAutoDone 上进行 DCOM 方法调用。进程外组件不使用 DoCa
36、llBack 机制,而使用 DCOM 来发送调用和呈现服务。如果方法在接口上,则使用 DCOM 调用远程受服务组件上的接口方法,否则,调用将被调度到 ServicedComponent 上的 IRemoteDispatch 接口。这意味着即使象 Dispose() 这样的调用也是通过 DCOM 调用的,其含义将在以后讨论。上下文ContextUtil 类用于访问相关联的 COM+ 对象的上下文及其属性。它与由 CoGetObjectContext 以非托管代码方式返回的对象的功能类似。与受服务组件相关联的托管对象的上下文与相关联的非托管对象的上下文的用途不同。通过编写三个托管对象可以证明这一点
37、,一个带有所需的事务(作为根),另外两个不从受服务组件中派生(作为上下文托管对象的子对象示例)。非受服务组件的行为将与带有事务支持的受服务组件一样,也就是说,它们可以调用资源管理器,而且必要时还可以使用 ContextUtil.SetAbort。创建根对象后,将创建相关联的非托管上下文并与当前的线程相关联。当调用子对象时,由于它们与非托管的上下文不相关,不需要进行 COM+ 上下文的更改,因而线程仍保持根的非托管上下文 ID。当子对象调用资源管理器时,资源管理器将从执行该子对象的线程中提取非托管上下文,即根对象的非托管上下文。依赖于这种方法是危险的,并且在以后的版本中,非托管上下文可能与托管上
38、下文合并,因此,子对象将与潜在的、不同的托管上下文相关联,资源管理器将不再获得根对象的上下文。因此,升级到 .NET 的新版本可能会破坏依赖于这种行为的代码。性能结果在这一节中,将比较托管客户端、托管服务器服务的组件解决方案与非托管客户端/服务器解决方案的性能。下表介绍了进程内的情况。为事务配置的 ServicedComponent 是用 C# 编写的,带有简单添加数字的单一方法。用相应的 C+ 实现进行比较。这种比较在不做任何实际工作的情况下,显示出托管和非托管的解决方案之间的不同。在托管解决方案中,进程内激活的速度大约要慢上 3.5 倍,而且当有上下文切换时,方法调用的时间大约要多 2 倍
39、。但是,当比较需要上下文切换的受服务组件方法调用和不需要上下文切换的受服务组件方法调用时,它们大约相差 3 个数量级,这表明进程内受服务组件截取基础结构是非常成功的。对于进程外解决方案,激活的时间大约多出 2 倍,上下文间的方法调用大约多出 3 倍。表 1 显示了使用托管和非托管解决方案时进程内激活和方法调用的时间比较。表 1:进程内激活和方法调用托管解决方案 非托管解决方案激活 35 10上下文间无操作方法调用 2 1上下文间有操作方法调用 200 100激活比无操作的方法调用的时间高一个数量级。加入一些工作以简单地获得一个 DTC 事务(但不对其做任何操作)可以使激活和方法调用的时间达到相
40、同的数量级。当方法调用只简单地打开一个缓冲的数据库连接,其工作速度将比激活和无操作方法调用的结合高一个数量级,这就证明了当实验中加入实际工作时,受服务组件基础结构的系统开销只是理论值而已。对象的生存期实时激活一般来讲,实时 (JIT) 服务不单独使用。它隐式地与事务服务一起使用,而且多数是与对象池一起使用。但是,此示例显示出一些有趣的主题。在以下代码中,.NET 类仅使用 JIT 服务编写。using System;using System.EnterpriseServices;assembly: AssemblyKeyFile(“Demos.snk“)assembly: Applicatio
41、nName(“JITDemo“)namespace DemosJustInTimeActivationpublic class TestJIT : ServicedComponentpublic TestJIT() / 首先获得调用AutoCompletepublic void DoWork () / 用以下方法显示完成 ./ 1. autocomplete 属性或/ 2. ContextUtil.DeactivateOnReturn = true 或/ 3. ContextUtil.SetComplete(); public override void Dispose(bool b) / 有
42、选择地替换此方法并使用您自己的 / 自定义 Dispose 逻辑。如果 b=true,则从客户端调用 Dispose(),/ 如果等于 false,GC 将清除对象该类从 ServicedComponent 中派生,并使用 JIT 属性指明所需的特定服务。为重载非托管代码中的 Activate 和 Deactivate 方法,要求类实现 IObjectControl 接口。而 ServicedComponent 类则有虚方法,可被重载以处理 Activate 和 Deactivate 事件。但是,ServicedComponent 及其真正代理 SCP 都不能实现 IObjectControl
43、。相反,当 COM+ 请求 IObjectControl 接口时,SCP 将创建一个代理剥离程序。然后,在剥离程序上的 COM+ 的调用被发送到 ServicedComponent 的虚方法中。DeactivateOnReturn 位是通过使用方法上的 AutoComplete 属性、调用 ContextUtil.SetComplete()、ContextUtil.SetAbort() 或设置 ContextUtil.DeactivateOnReturn 来设置的。假定 DeactivateOnReturn 位是在每个方法调用的过程中设置的,则方法调用的顺序将为:类的构造函数、Activate
44、、实际方法调用、Deactivate 、Dispose(true),最后是类的终结器(如果存在)。进行另一个方法调用时,将重复相同的顺序。一个好的设计只需重载 Activate 和 Deactivate 方法即可知道对象何时被取出,何时又放回对象池。Activate 和 Deactivate 的其他逻辑应放置在类的构造函数和 Dispose(bool) 方法中。可以使用以下方法之一设置 DeactivateOnReturn 位: 1. 客户端使用对象状态只进行单一的方法调用。在进入方法时,将创建一个新的真正对象并附加到 SCP 上。在退出方法时,将首先调用 Dispose(true),然后调用
45、真正对象的终结器(如果存在),以此停用真正对象。但是,相关的 COM+ 上下文、SCP 和 TP 仍然存在。客户端代码将仍然保持引用它所认为的实际对象(透明代理)。客户端对同一个引用所做的下一个方法调用将创建新的真正对象并附加到 SCP 上,以便为方法调用提供服务(请参阅“对象池” 一节以删除创建新对象的要求)。要停用真正对象,当方法调用退出时,真正对象需要表明已完成。这可以通过使用以下方法实现: a. 类的方法上的 AutoComplete 属性 b. ContextUtil 类上的两个方法调用之一, DeactivateOnReturn 或 SetComplete 2. 客户端对相同的对象
46、进行多次方法调用,每次方法调用后并没有停用对象(通过在退出方法之前将完成位设置为 false)。例如,规范在窗体级上使用 JIT 的受服务组件,并让两个窗体按钮在相同的对象实例上调用方法(通过让方法显式地将完成位设置为 false)。在某些方面,完成位应设置为 true。这种方法意味着客户端和对象之间存在着联系。客户端可以隐式或显式地完成这一内容: a. 客户端知道当对象完成时在对象上调用某种方法以停用该对象。该方法的实现使用了选项 1 中的概念。还可以使用相同的调用顺序调用对象引用,这意味着将创建新的真正对象。 b. 当客户端在对象上调用 Dispose() 方法时,对象将被显式销毁。Dis
47、pose() 是在 ServicedComponent 上定义的方法,它依次调用 Dispose(true)、类的终结器(如果存在),然后剥离相关的 COM+ 上下文。在这种情况下,在对象引用上不能再进行进一步的方法调用。如果仍试图调用,将引发一个异常。如果很多客户端都使用相同的对象,则只有当最后一个客户端使用完该对象时才能调用 Dispose()。但是,JIT 对象的无状态特性导致这样一种设计模式,即每个客户端一个实例。 c. 对象从不将其完成位设置为 true,客户端也从不调用 Dispose()。当发生垃圾回收时,真正对象、代理和上下文将被销毁。由 GC 启动的方法调用顺序将为 Deac
48、tivate、Dispose(false) 和类的终结器(如果存在)。 所有受服务组件都有相关联的 COM+ 上下文,它作为引用存储在 SCP(在远程情况下,则为 RSCP)中。只有当发生 GC 或客户端调用 Dispose() 时,才释放该引用。最好不要依赖 GC 来清除上下文:COM+ 上下文束缚在一个 OS 句柄和一些内存上,从而可能会延迟这些句柄的释放,直至发生 GC。而且,尽管 ServicedComponent 没有终结器,但 SCP 实现了一个终结器,这意味着 COM+ 的上下文引用永远不能在第一次回收时被回收。实际上,当最终调用 SCP 上的终结器时,上下文还没有被终结器线程销
49、毁,相反,销毁上下文的工作已经从终结器中删除,而被放到了内部队列中。这样做是因为人们发现,在某种压力环境中工作时(即受服务组件被快速创建、使用和放到范围之外),终结器线程会消耗大量资源。因而使用内部线程为队列提供服务,销毁旧的上下文。此外,任何创建新的 ServicedComponent 的应用程序线程都将首先试图从队列中取出一个项目并销毁旧的上下文。因此,从客户端调用 Dispose() 将使用客户端线程更快地剥离 COM+ 上下文,而且,它将释放上下文所消耗的句柄和内存资源。有时 Dispose() 可能会引发异常。一种情况是,如果对象驻留在已经终止的非根事务上下文中,则 Dispose() 调用可能会引发 CONTEXT_E_ABORTED 异常。另一种情况将在对象池中说明。从性能角度来看,最好不要在 ServicedComponent 的派生类中实现终结器,而将此逻辑放到 Dispose(bool) 方法中。尽管 SCP 实现了终结器,但真正对象的终结器是使用反射调用的。使用 JIT 的一个较好的设计方法为: 在构造函数和 Dispose(bool) 方法中放置自定义的激活和终结代码,不