COM+ 集成:.NET 企业服务如何帮助您构建分布式应用程序
Tim Ewald
本文假设您熟悉 COM 与 C++
Level of Difficulty 1 2 3
下载本文代码:ComPlus.exe
摘要 .NET 公共语言运行库 (CLR) 是 Microsoft 的下一代组件技术。CLR 将替代 COM,但不会替代 COM+。COM+,现在称为 .NET 企业服务,是用于可伸缩系统开发的 Microsoft 对象运行时环境。
本文解释了如何使用 CLR 实现与部署 COM+ 已配置类,如何访问对象上下文与调用上下文,以及管理与上下文相关的对象引用的规则。同时还讨论了管理宝贵资源(例如数据连接与共享池的对象)的一些方法,以及 COM+ 和新的 .NET 远程处理结构之间的关系。
2000 年 7 月,Microsoft 正式宣布新的以 Web 为中心的开发平台 — Microsoft .NET。.NET 的核心是公共语言运行库 (CLR),它是继承了 COM (并且与其向后兼容)的下一代组件技术。CLR 从许多方面对 COM 进行了改进,最重要的改进是通过提供元数据来完整描述一个组件实现的所有类型,包括它们所依赖的其他组件。所有这些类型信息都可以通过反射访问,通过自定义属性扩展。这使得创建各种复杂基础结构(例如对象序列化的自动化支持)变得非常容易,而这些 COM 开发人员已经期望多年了。
CLR 将替代 COM,但不会替代 COM+。COM+ 是一个对象运行时环境,提供了一套服务旨在简化可伸缩分布式系统的创建过程。如果在使用 COM 时发现 COM+ 很有用,那么开始使用 CLR 时也会发现 COM+ 同样有用。本文讲述的就是关于使用 CLR 的 COM+ 编程。我将首先简单回顾一下 COM+,它现在是 .NET 企业服务的一部分。
COM+ 的快速回顾
所有的 COM+ 服务都是依据上下文概念实现的。上下文是指一个进程中的空间,可以为驻留在其中的一个或者多个对象提供运行时服务。当位于一个上下文中的对象(或者线程)调用另一个上下文中的对象时,方法将被一个代理侦听,如图 1 所示。代理为 COM+ 运行时提供了机会,可以对调用进行预处理和后处理,并执行必要的服务代码以满足目标对象的需要,例如调用序列化、声明性事务管理等等。
图 1 上下文与侦听
COM+ 为需要的类提供运行时服务。如果类需要服务,COM+ 要确保类的实例总是驻留在提供该服务的上下文中。例如,如果类使用声明性事务,COM+ 要确保每个实例都是生存在一个可以提供分布式事务给对象使用的上下文中。类通过用一个声明性属性标记自己,表示自己希望使用一个 COM+ 运行时服务。每个服务定义了一个或者多个用于控制自己行为的声明性属性。
那些使用 COM+ 运行时服务并且被一个或者多个声明性属性标记的类,被称为已配置类。那些没有使用 COM+ 运行时服务,且没有被任何声明性属性标记的类,被称为未配置类。COM+ 在一个被称为编录的元数据库中存储已配置类的声明性属性值。编录被划分为若干应用程序,这些应用程序控制已配置类实例映射到进程的方式。有两类应用程序 — 库应用程序与服务器应用程序。每个已配置类属于一个应用程序。如果已配置类位于一个库应用程序中,该类型的新对象总是在它们的客户端进程中创建。如果一个已配置类位于服务器应用程序中,该类型的新对象总是在专用代理项进程 — dllhost.exe 的一个实例中创建。
当一个已配置类被实例化时,对象创建基础结构必须确保新对象最终在提供了它所需的运行时服务的上下文中。当创建新的对象时,对象创建基础结构将根据存储在 COM+ 编录中的类的声明性属性值所表示的内容,检查其服务需求。如果对象的需求能够被其创建者的上下文所满足,那么新的对象将生存在该上下文中,对该对象的调用将不被侦听。如果新对象的需求不能被其创建者上下文满足,那么将创建一个可以提供该对象所需运行时服务的新上下文。新对象生存在新的上下文中,对该对象的调用将被侦听以确保对象总是可以访问它所需的服务。当一个未配置类被实例化时,对象创建基础结构总是将新对象放入其创建者的上下文中。
位于一个特定上下文内的对象也许希望与其上下文提供的运行时服务交互。COM+ 通过将每个上下文建模成被称为对象上下文对象(简称对象上下文)的 COM 对象来促进这种交互。它也将每一个执行的逻辑顺序(因果关系)建模成被称为调用上下文对象(简称调用上下文)的 COM 对象。在上下文内执行的任何代码都能够通过分别调用 CoGetObjectContext 与 CoGetCallContext(或者等效的 Visual Basic 6.0 调用,GetObjectContext 与 GetSecurityCallContext)来获取对这些对象的引用。图 2 说明了这种结构。
用 CLR 实现 COM+ 已配置类
如同 COM 一样,CLR 依赖 COM+ 提供了对构建可伸缩应用程序的开发人员有用的运行时服务。用 CLR 来实现 COM+ 已配置类比使用 COM 来实现它们更容易,并且在某些情况下更有效。这两种技术的集成并不只是通过与 COM 的互操作性才能达到,理解这一点非常重要。也就是说,在可以使用 CLR 实现使用 COM+ 的传统 COM 组件时,CLR 与 COM+ 之间的集成程度实际上已经深入多了,这产生了一种可以与其他 CLR 技术(如远程处理和代码访问安全性)更好地集成的编程模型。COM+ 的 CLR 托管 API 通过 System.EnterpriseServices 命名空间的类型定义。依赖于 COM+ 的 CLR 类使用这些类型定义它们的声明性属性,同对象和调用上下文交互。
System.EnterpriseService 命名空间中最重要的类型是 ServicedComponent。所有使用 COM+ 运行时服务的 CLR 类必须扩展 ServicedComponent,如下所示:
using System.EnterpriseServices; namespace ESExample{public classMyCfgClass :ServicedComponent{public MyCfgClass() {}•••}}
一个类的继承层次结构中存在 ServicedComponent,这一点告诉了公共语言运行库的对象创建基础结构,该类已经被配置了,并且它的实例也许需要生存在一个新的 COM+ 上下文中。派生自 ServicedComponent 的类必须是公共的和具体的,并且必须提供一个公共默认构造函数。
声明性属性
决定一个派生自 ServicedComponent 的已配置类的新实例是否需要一个新的上下文,取决于这个类的声明性属性。请牢记已配置类的声明性属性存储在 COM+ 编录中。对于一个基于 COM 的已配置类,将适当的信息填充进编录的任务将由您来完成。要么编写脚本以编程的方式访问编录,要么通过使用 Component Services Explorer 管理控制台来手动安装已配置类
对于一个基于 CLR 的已配置类,可以利用 CLR 对于可扩展元数据的内部支持,在一个类的定义中直接嵌入相关的声明性属性。这类信息可以在安装时从一个已配置类的定义中提取,并且可以被用于填充 COM+ 编录。System.EnterpriseServices 命名空间定义了一组属性类,这些属性类可用于描述一个已配置类的 COM+ 运行时服务需求。最重要的属性类列于图 3 中(还有其他的一些属性用于控制其他的 COM+ 服务,如队列组件)
using System.EnterpriseServices;namespaceESExample{[Transaction(TransactionOption.Required)]public class MyTxCfgClass :ServicedComponent{public MyTxCfgClass() {}???}}
在这段代码中,Transaction 属性的存在指示了 MyTxCfgClass 需要使用一个 COM+ 托管的分布式事务。
当关于一个已配置类的信息被添加到 COM+ 编录中时,该类总是被添加到一个 COM+ 应用程序中。对于传统的基于 COM 的已配置类,没有办法在类的代码中建立这种关联。对于基于 CLR 的已配置类,CLR 元数据的可扩展性也会对此有所帮助。System.EnterpriseServices 命名空间定义了一组属性类,可用来描述一个程序集中的已配置类属于哪个应用程序。图 4 列出了最重要的程序集级别的属性(还有其他的一些属性用于控制其他的 COM+ 服务,如队列组件)。
下面的代码显示了应用程序属性如何被使用。
using System.EnterpriseServices; [assembly:ApplicationName("MyApp")] [assembly:ApplicationActivation(ActivationOption.Library)]namespace ESExample{[ Transaction(TransactionOption.Required) ] public class MyTxCfgClass :ServicedComponent{public MyTxCfgClass() {}???}}
在这种情况下,MyTxCfgClass 将会被编译到一个程序集中,该程序集被标记为属于一个名为 MyApp 的库应用程序的实现类。如果 ApplicationActivation 属性的构造函数参数被修改成 ActivationOption.Server,则该程序集将被标记为属于一个服务器应用程序的实现类。注意这些应用程序属性并不向 COM+ 提供的所有应用程序设置提供完全控制。例如,并没有办法使用一个应用程序属性来指定一个服务器应用程序应该作为何种安全性主体运行。这是有意义的,因为在一个 CLR 程序集中将用户名和密码作为元数据编码并不是一个好主意。
实现 IObjectConstruct 与 IObjectControl
一些 COM+ 运行时服务,包括对象构造、对象池、以及实时 (JIT) 激活,都需要调用使用它们的对象上的标准方法。具体来说,对象构造服务调用由 IObjectConstruct 接口定义的 Construct 方法,向每一个新实例提供类的构造函数字符串。对象池与 JIT 激活服务则调用由 IObjectControl 接口定义的 Activate、Deactivate、以及 CanBePooled 方法来通知一个对象关于其生命周期中的事件。您也许认为为了使用这些服务,必须显式地在派生自 ServicedComponent的已配置类上实现 IObjectConstruct 与 IObjectControl 接口,但实际情况并非如此。ServicedComponent 类已经提供了所有这四种方法的默认实现,一个类可以只需重写它感兴趣的任何一种方法。
例如,下面的代码显示了一个共享池的对象可以如何有选择地实现 IObjectControl 的方法。
namespace ESExample{[ ObjectPooling(true, 5, 20) ] public class MyPooledCfgClass :ServicedComponent{ public MyPooledCfgClass() {}// override CanBePooled so that instances can be// reused, use default implementations of Activate// and Deactivate provided by ServicedComponent public override bool CanBePooled() { return true>
在本例中,用 ObjectPooling 属性标记了 MyPooledCfgClass 类,其最小的池大小为 5,而最大的池大小为 20。该类重写了 CanBePooled 的定义,并且提供了返回为真的实现,这样类的实例就可以被重用。默认的实现返回为假,意味着类的实例不应被重用。这个类并没有重写 Activate 或者 Deactivate;它依赖于由 ServicedComponent 提供的默认实现。Activate 的默认实现成功地返回,这样就允许激活继续进行。Deactivate 的默认实现不做任何事情。
非常值得注意的是,虽然 ServicedComponent 类为 IObjectConstruct 与 IObjectControl 提供了方法的默认实现,但它实际上并没有实现任何一个接口。相反,这些接口是由一个内部类 (ProxyTearoff) 实现的,它将调用直接转发给您的对象。这似乎是一种不重要的实现细节,但马上您将会看到这种实现方式的重要性
ContextUtil 与 SecurityCallContext
已配置类经常需要与它们所使用的运行时服务交互。COM+ 通过侦听实现其服务,对象通过对象上下文与它们间接使用的服务进行交互。System.EnterpriseServices.ContextUtil 类包装 CoGetObjectContext,后者是用于检索对象上下文的 COM+ API。ContextUtil 使用图 5 中列出的一组静态方法和属性公开 COM+ 对象上下文对象的功能。
位于图 6 中的代码显示了 ContextUtil 类是如何被用来操作 "happy" 与 "done" 位的,它们用于控制一个 COM+ 声明性事务的输出。具体说来,它设置 ContextUtil.DeactivateOnReturn 属性为真,打开 "done" 位,指示声明性事务将在方法调用结束时完成。然后它设置 ContextUtil.MyTransactionVote 属性为 TransactionVote.Abort,关闭 "happy" 位,指示如果方法没有成功完成 — 换句话说,如果发生一个异常时,则声明性事务应该中止。接着,在完成一些数据库处理之后,方法设置 ContextUtil.MyTransactionVote 属性为 TransactionVote.Commit,将 "happy" 位重新打开,指示 COM+ 侦听基础结构应该提交此声明性事务。
COM+ 安全性服务通过调用上下文,而不是对象上下文公开其行为。 System.EnterpriseServices.SecurityCallContext 类包装 CoGetCallContext,后者是用于检索调用上下文的 COM+ API。SecurityCallContext 使用一组图 7 列出的方法和属性公开调用上下文对象的功能。
静态属性 CurrentCall 为当前的调用返回指向一个 SecurityCallContext 对象的引用。图 8 提供了一个示例,其中 GetCallerName 方法使用 SecurityCallContext 类来检索以及返回当前调用者的名称。(注意 ApplicationAccessControl 属性被用于打开在 MyApp 库应用程序中对基于角色安全性的支持。)
编译一个已配置类
当已配置类实现后,它必须被编译。编译代码是容易的,但需要牢记两件事情。首先,COM+ 集成基础结构要求被编译的程序集具有强名称。为了创建一个具有强名称的程序集,必须通过运行强名称实用工具 sn.exe,生成一个密钥。接着必须在您的组件代码中使用一个来自于 System.Reflection 命名空间称为 AssemblyKeyFileAttribute 的程序集级别属性来引用该密钥,它被存储在一个文件中,如下面的代码所示:
using System.EnterpriseServices; using System.Reflection; [assembly:ApplicationName("MyApp")][assembly:ApplicationActivation(ActivationOption.Library) // AssemblyKeyFile attribute references keyfile generated// by sn.exe - assembly will have strong name[assembly:AssemblyKeyFile("keyfile")] namespace ESExample] {???}
其次,在编译具有强名称的程序集时,必须引用导出 System.EnterpriseServices 命名空间中类型的程序集 System.EnterpriseServices.dll。下面给出的是生成一个密钥以及编译一个已配置类所需要的命令:
sn -k keyfile csc /out:ESExample.dll /t:library/r:System.EnterpriseServices.dll MyCfgClass.cs
部署一个已配置类
在一个基于 CLR 的已配置类已经编译后,就需要部署它的程序集了。可以通过从命令行运行服务安装实用工具 regsvcs.exe 来完成,如下所示:
regsvcs ESExample.dll
该工具完成三件事情。首先,它将 CLR 程序集作为一个 COM 组件注册(如同已经运行了程序集注册实用工具 regasm.exe)。其次,它生成一个 COM 类型库(如同已经运行了程序集到类型库转换器 tlbexp.exe)并且使用它来部署在 COM+ 编录中程序集实现的已配置类。 Regsvcs.exe 在默认情况下将创建程序集的 ApplicationName 与 ApplicationActivation 属性所描述的目标应用程序。(也可以使用命令行开关重写这种行为。)第三,它使用 .NET 反射 API 来询问程序集实现的已配置类的元数据,并使用该信息编程更新 COM+ 编录,使每个类都将有相应的声明性属性设置
如果已配置类没有特定的声明性属性,regsvcs.exe 将使用一个默认值来代替。一般情况下,默认的选项将会关闭运行时服务。这是一种好的做法,因为通过确保已配置类的实例只使用它们显式说明其需要的服务,可以最小化系统开销。但是,在一些情况下,regsvcs.exe 将根据其他属性的设置来推断属性的值。例如,图 6 中的 MyTxCfgClass 并没有用 Synchronization 或者 JustInTimeActivation 属性标记。但是因为 COM+ 声明性事务服务依赖于上述这些服务,那么当类注册在 COM+ 编录中时,无论如何它们都将被启用。如果 MyTxCfgClass 用这些附加的属性进行了标记,它们的值将必须与 Transaction 属性指定的值兼容,如下所示
// all these attributes// are compatible[ Transaction(TransactionOption.Required) ] [ Synchronization(SynchronizationOption.Required) ] [ JustInTimeActivation(true) ] public class MyTxCfgClass :ServicedComponent public MyTxCfgClass() {{}???}
那么,这些修饰 ServicedComponent 派生类的声明性属性是否就是用正确的信息来初始化 COM+ 编录的方式呢?也许这里还执行更多的操作?经证明在许多情况下,托管 COM+ API基础结构并不反射 COM+ 编录,而是反射在类的代码中的声明性属性,以了解类使用了哪些服务。它使用这类信息来调整其行为。
例如,如果用来自 System.EnterpriseServices 命名空间的 ObjectPooling 或者 JustInTimeActivation 属性标记了您的类,则我前面提到的 ProxyTearoff 类将对 IObjectControl 方法的调用只转发给您的类的实例。如果没有标记,则对象将不会获得通知,无论 COM+ 编录的内容如何。
实际上这意味着 COM+ 编录应该被看成类的声明性属性的一个副本,而不是真正的来源。在 COM+ 编录中一直应该修改的属性只是那些特定于部署的属性,如对象结构字符串以及安全性设置。这是一种时代的标志。COM+ 编录之所以存在,是因为 COM 并没有二进制组件的可扩展元数据模型。CLR 却有这样的模型,因此可以预期 COM+ 运行时服务的将来版本将会完全依赖于内嵌 CLR 元数据,从而彻底地抛弃编录。
在默认情况下,类的公共方法并没有显示在 COM+ 编录中,这一点值得注意。这是因为它们没有显示在 regsvcs.exe 为进行安装而生成并使用的类型库中。无论何时为了从 COM 使用而导出一个 CLR 类型时,这都是一种默认的行为。您可以通过定义并实现已配置类的接口,或者通过 System.ClassInterfaceAttribute 属性标记您的类来改变这种行为。
除了开发一个在 COM+ 中实现了基于 CLR 的已配置类的程序集之外,还必须部署它,从而使其能够被基于 CLR 的客户端加载。您可以私下部署程序集,通过将其复制到客户端的目录(或者配置为该客户端可搜索的任意目录)中,使其只对单个客户端可用。您也可以公开部署程序集,通过将其安装在全局程序集缓存 (GAC) 中,使得它对机器中的所有客户端都可用。您可以通过运行命令行全局程序集缓存实用工具 gacutil.exe,并且使用 -i 开关来完成部署。
gacutil -i ESExample.dll
如果基于 CLR 的已配置类被部署在一个 COM+ 服务器应用程序中,COM+ 基础结构需要能够独立于客户端实际运行所在的目录来加载程序集。在这种情况下,程序集必须部署在 GAC 中,否则尝试实例化已配置类将会失败。
.NET 框架的目标之一就是通过支持 XCOPY 部署来简化系统的安装。术语 XCOPY 指的是一个 MS-DOS 命令行实用工具,用于从一个位置向另一个位置复制文件和目录。XCOPY 部署的目的是使在远程服务器上不执行任何代码就可以在该服务器上安装应用程序成为可能。为了支持 XCOPY 部署,CLR/COM+ 集成基础结构允许您在安装时提前执行 regsvcs.exe,并在第一次使用基于 CLR 的已配置类时处理注册。
有一些关于基于 CLR 的已配置类的 XCOPY 部署的缺点应该注意。首先,COM+ 编录只能由加入到 COM+ 系统应用程序定义的 Administrators 角色中的用户来更新。默认情况下,只有本地管理员才会被加入到这种角色中。因此,为了成功地注册已配置的组件,首次使用基于 CLR 的已配置类的代码必须拥有管理权限。如果没有,则注册将失败。(当然,任何运行 regsvcs.exe 工具的人也必须拥有此权限。)
第二个缺点,虽然可只需通过将程序集复制到适当的目录中,即可在 GAC 中对该程序集进行部署,但是如果在该处部署程序集是想要使已配置类可被安装在一个 COM+ 服务器应用程序中,则也许还有一些额外的工作要做。说得具体一些,您也许需要设置 COM+ 服务器应用程序的安全主要对象,出于我刚刚提到过的原因它不能自动地使用元数据来完成。如果正在将您的已配置类部署在一个 COM+ 库服务器中,只要程序集被部署在需要它的客户端可以发现的位置,那就不会有问题了。例如,如果正在使用 CLR 来实现一个只能被 ASP .NET 应用程序使用的已配置类,可以使用 XCOPY 部署在 COM+ 库应用程序中安装该类,以及在 ASP .NET 应用程序的 ./bin 子目录中安装该程序集,这样它就可以被 ASP .NET 代码发现。
实现一个客户端
一旦一个基于 CLR 的已配置类编译和部署完成,就可以使用它了。从客户端的视角看,已配置类并没有什么特殊的地方;它使用 COM+ 运行时服务这一事实是无关紧要的。下面的代码显示了一个使用前面所定义的 MyTxCfgClass 类的简单客户端:
using ESExample; public class Client{ public static void Main(string[] args) { MyTxCfgClass tcc = new MyTxCfgClass();... // use object as desired }}
当然,对于所有的 CLR 代码,当编译客户端时必须提供一个指向已配置类程序集的引用。
csc /r:ESExample.dll Client.cs
微妙的复杂性
此时可以看出,用 .NET CLR 实现 COM+ 已配置类是相当简单的。System.EnterpriseServices 命名空间中的类型提供了 COM+ 的一个托管 API,从而简化了运行时服务的使用。运行时服务自身并没有改变;它们仍然严格地按照有经验的 COM+ 开发人员习惯的方式来工作。虽然话是这么说,但是集成 COM+ 与 CLR 并不是一点微妙的复杂之处都没有了,充分认识到这一点很重要。.在使用基于 CLR 的语言编写 COM+ 代码时,有几个问题将使情况比我迄今为止所暗示的还要复杂。这些问题中绝大多数之所以会出现,是由于 CLR 并不是 COM,而且它可以完成 COM 无法完成的事情。所有这些问题都是可以克服的;只需要理解一些 CLR 特性(如静态方法以及垃圾回收)对 COM+ 编程的影响。
在我深入研究这些问题之前,您需要了解两个有关 CLR 类与 COM+ 上下文之间关系的细节问题。首先,对派生自 ServicedComponent 的类实例的调用将在 COM+ 上下文边界被侦听。这些对象被称为上下文绑定。对非派生自 ServicedComponent 的类实例的调用将不在 COM+ 上下文边界被侦听。这些对象被称为上下文灵活 (context-agile)。CLR 对象默认情况下总是上下文灵活的。它们只有从 ServicedComponent 派生时才是上下文绑定的。(这仍然与 .NET 远程处理上下文无关,我等一下将对此进行讨论。)
图 9 说明了这种结构。
图 9 上下文对象
注意到没有?对于 COM+ 上下文而言,CLR 对象的行为恰好与 COM 对象相反,这真有意思。在 COM 中,对一个对象的调用在默认情况下总是会被侦听;所有对象都是上下文绑定的。一个 COM 对象只有在聚合了自由线程封送拆收器 (FTM) 并且不是新的 COM+ 上下文中的第一个创建的对象(也就是说,它不是识别对象)时才是上下文灵活的,在这种情况下,对该对象的调用是不可侦听的。这种新方式的好处是,它通过确保调用只有当绝对必要时才进行预处理和后处理,降低了侦听开销。说得具体一些,就是如果一个已配置类的实例返回一个指向未配置类实例的引用(例如,一个 ADO.NET DataSet 对象),则对该对象的调用将不会被侦听。DataSet 对象不必做任何特殊的事情,它只是按照这种方式执行。
您需要知道的第二件事情是,除了将跨上下文侦听减少到只在真正需要的情况下才进行,CLR/COM+ 集成基础结构还会在可能的时候避免在托管的类型和本机类型之间进行转换。我们都已经知道,从托管的 CLR 代码向外调用本机 COM 代码的开销是相当昂贵的。这种开销的很大一部分是花在类型的来回转换上 — 绝大多数情况是将 CLR 字符串与 COM BSTR 来回转换。当跨越 COM+ 上下文边界的调用确实需要调用一些本机代码时,只要调用者与被调用者都是使用 CLR 实现的,并且都是在同一个进程中,基础结构是足够智能的,会避免数据类型的转换。总有一天 COM+ 运行时服务自身也会作为托管代码被重新实现,那时,到本机代码的转换将不会是一个问题。不过,到那时,这种优化将会有助于 COM+ 侦听变得更快一些。
静态方法与属性
既然您已经对 CLR 代码如何与上下文联系了解得不少了,现在考虑如下问题:如果一个基于 CLR 的已配置类有静态方法或者属性访问器,那么它们应该在什么样的上下文中执行呢?答案是在调用者的上下文中。这也许不太直观,但是考虑到静态成员并不是特定于对象的,因此不必从一个给定对象驻留的特定上下文中进行访问,就可以理解了。例如,位于图 10 中的已配置类有一个静态方法 Two,以及一个静态属性 Four。这些方法的代码将总是在调用者的上下文中执行。对于实例方法 One 或者属性 Three 的调用将总是在对象的上下文中执行。
此时您也许会想知道,如果在一个上下文中执行的时候,在静态属性中存储对象的引用,然后试图从另一个上下文中使用该引用,会发生什么情况?在传统的基于 COM 的 COM+ 编程中,如果在一个全局变量中存储一个原始对象引用(这就是静态属性的作用所在),这将会引发各种类型的灾难,因为您不能将一个对象的引用不加封送地从其原上下文中取出。对于基于 CLR 的 COM+ 代码,这就不再是一个问题了。CLR 使用一个非常“瘦”的代理包装每一个已配置类的实例,这样即使在相同上下文中的其他对象都不会有指向该类实例的直接引用。如果在一个上下文中执行的代码将一个指向基于 CLR 的已配置类的引用存储到一个静态属性中,那么它就会实际存储一个指向这个特定代理的引用。如果在另一个上下文中执行的代码从静态属性获得该引用,并且开始使用它,特定的代理会检测到这种变化,并且封送它所拥有的引用,这样就会进行正确的侦听。这是一种非常优秀的解决方案,实质上,这意味着您可以在任何地方存储指向基于 CLR 的已配置类实例的引用,从任何地方都可以使用它们,并且可进行正确的侦听。
管理宝贵的资源
在讨论有关管理对象引用的主题时,还有其他的问题需要您注意。CLR 的主要特性之一是它依赖垃圾回收机制来回收内存。这非常重要,因为这种回收通过自动检测和释放不再使用的内存块区,有助于避免内存泄漏。不幸的是,虽然垃圾回收使得管理内存更为容易,但是它同时也使得管理其他类型的资源(如对象)变得更困难。因为有效的资源管理是构建可伸缩系统的关键,这也是设计 COM+ 的目的,您需要理解如何正确清理宝贵的基于 CLR 的资源。
考虑一个 CLR 类,它使用一个 SqlConnection 对象与数据库通信,如下所示
public void UseADatabase(void) { System.Data.SqlClient.SqlConnection= new System.Data.SqlClient.SqlConnection(...);... // use connection to execute statements}
在 UseADatabase 方法的最后,SqlConnection 对象的引用被释放了。SqlConnection 对象包装了底层的数据库连接,并且出于效率考虑被 ADO .NET 基础结构池化 (pooled) 了(在 Beta 1 版本中这是通过使用 COM+ 对象池实现的;在 Beta 2 版本中,这是通过使用方式类似于 COM+ 对象池的一种内部机制实现的)。问题是,什么时候 SqlConnection 对象所持有的连接会返回到池中?直到垃圾回收器介入,才能认识到 SqlConnection 对象不再被使用,并且调用它的终结器。终结器实现将底层连接返回到池中,然后 SqlConnection 对象的内存被回收。
这里有一个问题是,数据库连接是非常宝贵的资源,让它们在被垃圾回收器回收之前,在内存中飘浮不定,游离于我们的控制之外,这是我们无法承受的。需要有某种方法来立即回收这种类型的资源。一种完成这种功能的方式是强制垃圾回收器来执行回收,如下所示:
public void UseADatabase(void) { System.Data.SqlClient.SqlConnection conn = new System.Data.SqlClient.SqlConnection(...);... // use connection to execute statements conn = null; System.GC.Collect();}
但是,这种方式非常笨拙,而且存在各种其他的隐患。
一种更好的方式是调用 SqlConnection 对象的 IDisposable.Dispose 的实现。System 命名空间中的 IDisposable 接口为拥有宝贵资源的对象行为进行了建模,其中的资源应该由它的客户端显式地清理而不是由垃圾回收器隐式地清理。SqlConnection 类的 Dispose 实现立即向池返回底层数据库连接(实际上,类的终结器所调用的正是同一方法)。
下面的代码显示了一个新版本的 UseADatabase 方法,它显式地清理其 SqlConnection。
public void UseADatabase(void) { System.Data.SqlClient.SqlConnection conn = new System.Data.SqlClient.SqlConnection(...);try {... // use connection to execute statements } finally { conn.Dispose(); }}
注意对 Dispose 的调用出现在一个 finally 代码块中,这确保了调用总是会执行,即使发生异常。
那么这同 COM+ 与已配置类有什么关系呢?首先,您的已配置类可能会使用数据库连接和其他的宝贵资源,所以如果希望您的系统可伸缩,就必须知道如何正确清理这些资源。其次,所有基于 CLR 的已配置类拜它们的基类 ServicedComponent 所赐,都已经实现了 IDisposable.Dispose。您需要知道当一个客户端调用 Dispose 时会发生什么。默认实现的行为取决于已配置类所使用的是何种运行时服务。如果它没有使用对象池激活,Dispose 只需立即调用一个对象的终结器(当该对象的内存被垃圾回收器回收时,它将不会被再次调用)。如果已配置类使用对象池而没有使用 JIT 激活,Dispose 调用 Deactivate 来通知对象它正在离开其当前的上下文。然后它会调用 CanBePooled 来询问对象是希望被重用还是被销毁。如果 CanBePooled 返回真,该对象将被返回到其类的池中。如果它返回假,该对象的终结器将被调用(当该对象的内存被垃圾回收器回收时,它将不会被再次调用)。客户端为共享池的对象调用 Dispose 特别重要,如下所示。
public void UseAPooledObject(void) { MyPooledCfgClass pcc = new MyPooledCfgClass();try {... // use pooled object to do something }finally { pcc.Dispose(); }}
如果它们不调用 Dispose,对象(例如前面描述过的数据库连接)直到垃圾回收器回收它们时才会返回到它们的池中。最后,如果出于某种原因您选择在一个已配置类中实现您自己的 Dispose 版本,如果您希望有前面描述的这种行为,必须调用基类的实现。
[ Synchronization(SynchronizedOption.Required) ] public class MyCfgClass :ServicedComponent{ publi ncew void Dispose() {... // do whatever base.Dispose(); // call base to clean up }}
此时您也许想知道 JIT 激活以及在每个方法调用的最后停用对象所发生的情况。这解决了前面那些资源管理问题了吗?一般性的回答是没有。在每个调用的最后停用对象,将强制该对象释放它的数据成员中所持有的所有资源,但是它同样强制一个希望再次使用相同对象的客户端要为了每次方法调用重复获取一个对象。您只需通过在数据成员中不存储任何资源,即可从这种模式中获取好处而无需系统开销。如果您确实选择使用 JIT 激活以及在每个方法调用的最后停用对象,要明白一个对 Dispose 的客户端调用实际上在再一次停用和终结对象时将强制对象的重新激活。一般说来,转向 CLR 并没有改变使用 JIT 激活的规则。除非某些其他的运行时服务(如声明性事务服务)确实需要,否则您应该避免使用它,因为它从来都不会提高效率,而且经常比我们自己直接小心管理宝贵资源更低效。
将来
ServicedComponent 类派生自 System.ContextBoundObject,而后者又派生自 System.MarshalByRefObject,System.MarshalByRefObject 是使得对象可以通过 .NET 远程处理基础结构访问的基类。在新的远程处理层上直接建立 CLR/COM+ 集成基础结构有两个好处。首先,现在您能够使用远程处理层内置的对 SOAP 的支持,访问使用 COM+ 运行时服务的远程对象。但是,您无法使用 SOAP 从一个进程向另一个进程中传播声明性事务(这也许并不是您希望的,但事情的原理就是这样)。其次,它为 COM+ 运行时服务完全根据 CLR 远程处理层崭新的上下文基础结构实现新版本提供了一种途径,而这是目前的 COM+ 无法使用的。这在将来的某个时候是会实现的。当实现了这一步时,服务将与 SOAP 或者任何其他的传送机制无缝集成,上下文结构将是完全可扩展的。到那时,CLR 将使得 COM+ 编程的许多细节变得更简单,这总可算得上是朝着正确方向迈出了一步。
有关相关文章,请参阅
Windows XP:Make Your Components More Robust with COM+ 1.5 Innovations
House of COM:Migrating Native Code to the .NET CLR
.NET 框架 SDK 的 /samples/technologies/componentservices 子目录
Tim Ewald 的 Transactional COM+:Building Scalable Applications一书(Addison-Wesley,2001 年)
Tim Ewald 是 DevelopMentor 的主任科学家,并且是最近出版的 Transactional COM+:Building Scalable Applications(Addison-Wesley,2001 年)一书的作者
摘自 MSDN Magazine 的 2001 年 10 月一期。
此杂志可通过各地的报摊购买,也可以订阅。