错误处理:从托管的 COM+ 服务器应用中抛出自定义异常类型 原文出处:MSDN Magazine Mar 2004(Error Handling) 原代码下载 ExceptionsinCOM.exe (179KB) 这篇文章假设你熟悉 COM+ 和 C# ![]() .Net中的异常处理语义是基于类型的,所以你可以创建具有其自己的属性和方法的自定义异常。异常是 .Net 中头等重要的一个元素,因为它是内建的错误处理机制,因此所有 .NET 兼容的语言都必须支持异常。另外, .Net 代码可以将 COM+ 服务作为 Enterprise Services(企业服务)来获得,所以你可以在 Enterprise Services 设计中定制异常。 本文作者描述了自定义异常——跨 COM 互用性( COM Interop )边界抛出异常以及使用 Enterprise Services。 如果你正在开发一个系统,它涉及多个 .Net Framework 成分,作为设计的一部分,你还要结合定制异常类型层次给客户提供一个强类型的健壮的异常信息。考虑到你的各个组件要被远程访问 ,你要确保开发定制的异常类型以便能被完全串行化,到目前为止,一切都还顺利。但系统单元测试进行到中间阶段时你才会注意一个问题。客户端测试程序的 Catch 块代码段捕获不到预先定制的异常类型,而是收到一个熟悉得不能再熟悉的未处理异常对话框。 现在该怎么办?首先让我们看看抛出的地方在哪里。查看后,看起来很正常: throw new CustomException ("Throwing CustomException type ");下面看看客户端的代码 ,看上去也没什么问题。 try { ExceptionGenerator x_gen = new ExceptionGenerator (); x_gen.GenerateCustomException (); } catch (CustomException x) { MessageBox.Show (x.Message); }你可能和我第一次碰到这种事情时所想的一样——我的定制异常到哪里去了? 本文专注的是从服务性组件产生定制异常,该组件被配置在一个 COM+ 服务器应用环境中执行(相对于库应用环境)。我将向你示范如何获取自定义异常并将它传回到客户端。我还要讲述基本的体系架构问题以及在 服务性组件体系中设计决定对实现异常处理策略所起的重要作用。 ![]() 让我们看一下创建一个自定义异常需要什么?我们不会涉及太多有关异常的细节,因为这方面的资源太多太多,但我 会对建立自定义异常类型的基本概念进行一个回顾。在例子中,我会建立一个叫做 CustomAppException 的异常类型(参见 Figure 1)。对于创建自定义异常类型,微软有两个建议:一是类型名要以“Exception”结尾, 二是异常类型要从 ApplicationException 派生。遵循这这些规则,使得这个类的使用者明白这是一个异常类型,并且它是一个定制的应用程序异常,而不是内建的系统异常类型。 ![]() 这个异常类型还没有完成。我仍然必须保证异常数据在被抛出时可以跨应用程序(AppDomain)边界被串行化。因此,现在必须添加 [Serializeabe] 属性到这个类中,以便通知串行处理例程这个类型可以被串行。如果你不用这个属性标记异常,那么就会抛出 SerializationException 异常 。该错误信息表示这个类型没有没被标记为可串行。第二、如果具备了定制属性。则必须实现 ISerializable 接口。这样便可以定制自己的串行化/反串行 化处理。幸运的是,顶层 Exception 类已经实现了这个接口,所以只要改写基类现有的实现即可。我还必须实现一个特殊的构造 函数(它被用于异常的反串行)并改写 GetObjectData 方法,该方法被用于将任何属性数据串行成流数据。(Figure 2) 正象你所见到的那样,我的自定义类型有三个可被串行的定制属性:ThreadID、ProcessID 和 UserID 。完整的解决方案包括该定制的异常类型 ,加上远程服务器和客户端程序跨越单远程边界抛出的一个文本异常。从本文顶部链接处可以下载到源代码。我鼓励用这个项目进行实验,删掉 Serializable 属性或注释掉重写的 ISerializable,当你检查被抛出的与不同异常类型关联的返回堆栈数据时,你就会看到一些有趣的事情。 你可能很想知道为什么要讨论这些关于串行化的问题。而理解对象被串行化的整个过程对理解为什么你在上述情况下得不到期望的异常类型是至关重要的。这对于你在必须排除这方面故障时也是有所裨益的。在深入探讨企业服务和 服务性组件体系结构之前,我将回顾另外一个叫做 COM 互用性(COM interoperability)或简称为互用性( interop )的 .Net Framework 技术。 ![]() COM 互用性是 .Net Framework 提供的一个关键技术,它使得 COM 组件能使用托管代码,反之亦然。interop 的一个主要的目标是让托管和非托管代码 之间的转化尽可能无缝。这是个很复杂的任务,因为 .Net 的编程模型和 COM 模型之间存在明显差异。 你可能已经注意到,COM 错误处理的核心是 HRESULT。HRESULT是个 32 位的值,由四部分比特位组成。其中最重要的位叫做 severity 位,被用来表示成功(severity = 0)或者失败( severity = 1)。COM 的客户端负责检查每次 COM 调用的 HRESULT 值 以判断调用是成功还是失败。COM 组件还可以通过实现 IErrorInfo 接口来支持更详细的出错信息。但这在编程模型中不是必须的,所以客户端不能 保证总是能捕获到这种类型的错误信息。 相比之下,.Net FrameWork 的错误处理模型是基于异常的 。异常是面向对象的,强类型的用于通知调用代码有一个错误或者未曾预料到的事件发生了。异常提供丰富的出错信息,并且与HRESULTS 和 IErrorInfo 不同,异常是可扩展的。这样使你能从现有的异常类型中派生自己的异常类型——根据需要添加额外的属性和方法。定制异常的其它好处还包括:可以通过类型名来区分,改进了代码的可读性和易于维护。 当我们必须从托管代码向非托管代码传递出错信息时,对我们来说是个挑战,反之亦然。interop 将使得这种转换尽可能平滑,它将充当桥梁来连接两大错误处理模式的鸿沟。 ![]() 虽然 Interop 做了大量的工作来掩饰编程模型间的差异,但它无法隐藏所有的东西。情况一旦复杂化,interop 便无法平滑地弥补两者的鸿沟。比如,当 异常要从托管代码转换到非托管代码,最终又要转换回到托管代码时便是如此。 当我回顾 Enterprise Services 的内部机制时你将会看到,COM+ 充当了(DLLHOST.EXE)代理并且它提供的服务仍然用非托管代码写成。因此,跨 Interop 边界隐含转换在你不知不觉的情况下时有发生,因为有这种秘密行动,我首先要考察如果我的 CustomAppException 跨托管/非托管边界被抛出将会发生什么。 让我们回头使用前面的远程解决方案。如果你检查调用 GenerateException 的客户端代码,你会发现它仅仅是实例化 ExceptionGenerator 类,然后象下面这样调用方法: 其实企业服务的内部 DLLHOST.exe 和它的提供者依然是非托管的代码.对应的模糊的传递通过 Interop 绑订会出现在没有你的知识的时候 因为这种隐式的行为,我门先看看如果我的 CustomAppException 被抛出后会怎么样? 你可以看到客户端只是简单的调用了 GenerateException : try { ExceptionGenerator generator = new ExceptionGenerator (); generator.GenerateException ("testing remote exception generation..."); }现在假设 GenerateException 调用没有直接到达服务器上的宿主对象实例。而是被拦截并被重定向到某些非托管代码。当从 ExceptionGenerator 跑出某个异常时,返回路径将如 Figure 3 所示,结果是一个 ApplicationException,而非 CustomAppException。之所以发生这样的事情是因为托管到非托管的转换保留了 HRESULT 和 错误信息,但没有保留该异常的类型。当该异常从非托管转换到托管时,公共语言运行时(CLR)必须确定如何做。CLR 处理的不再某个类型,而只有一个 HRESULT。 ![]() Figure 3 跨 Interop 边界的异常 然而,一切并没有丢失,CLR有一个巨大的 HRESULTS 列表,知道如何将它转换为特定的异常类型。因为我是从 ApplicaionException 派生了 CustomAppException,我继承了基本的 HRESULT 值。这就是为什么我的类型通过非托管代码的传递后变成了 ApplicationException, 我未曾从 ApplicationException 或另一个 CLR 在其中内建有 HRESULT 的异常类型来派生我的异常类型,返回的类型已经变成一个 COMException (当 CLR 获得一个不可识别的跨 interop 边界 HRESULT,它便创建一个 COMException)。 以上讨论中,我展示了当定制异常跨越托管和非托管边界时所发生的一切,接下来让我们看看企业服务和服务性组件。 ![]() 众所周知,COM+是继承 MTS 而来的,并提供企业级服务,如分布式事务处理、对象池、JIT 名字激活等。COM+ 的目标之一是提供一个体系结构 ,使人们能在该体系结构中开发出具备可伸缩性的并能应付越来越大的吞吐量需求的基于组件的,分布式的企业级系统。当从托管代码中存取这些服务时,这些服务都是同等的,并被称为企业服务——Enterprise Services。 最新版本的 COM+ ( Windows Server 2003 和 Windows XP 上的 Version 1.5) 依然是用非托管的代码 编写的。因此,如果托管的代码想利用 COM+ 提供的服务,Interop 将充当一个重要角色。你将会看到,interop 是唯一的解决途径。具体信息参见 System.EnterpriseServices 名字空间和 COM+ 基础结构的整合细节,该整合的核心部分即 ServicedComponent 类。 ![]() 服务性组件体系结构依赖于大量基于.NET Framework 的技术,其中最主要的一个便是 .NET Remoting。 Remoting 提供了一个分层的可括展的底层结构,允许各种服务被穿插到调用序列中。此外,ProxyAttribute 和 RealPRoxy 类 在活化服务性组件方面起着重要的作用。 在我们深究细节之前,我们回顾一下 ExceptionGenerator 示例。我打算将 ExceptionGenerator 转换成一个服务性组件 。修改后的版本(现在叫做 EnterpriseExceptionGenerator )如 Figure 4 所示 。代码中有一些重要事项需要注意:使用 using 引用 System.EnterpriseServices,ServicedComponent 基类 和 IExceptionGenerator .Net 接口声明。 现在,让我们看看 Figure 5 中的例子,这里有几个问题要注意。首先,没有导入 EnterpriseServices 名字空间,编译器不能识别我的基类 (ServicedComponent),也不能识别我用于声明 COM+ 应用程序设置 的各种定制属性以及任何组件可能需要的服务。第二,如果类不是从 ServicedComponent 派生的。将不能作为 Enterprise Services 应用程序,并不能使用 COM+ 提供的服务。最后,注意我选择了在我的服务性组件中实现 .Net 接口,因为它将被 COM 和 .Net 两者的客户端程序使用。在本文后面的部分,你将看到接口的实现是一种根据你的组件当前和未来的需要必须考虑的设计决定。 Enterprise Services 的核心是 ServicedComponent 类,它从 ContextBoundObject 派生,而ContextBoundObject 又派生于 MarshalByRefObject。MarshalByRefObject 是任何可以被远程访问的 对象的基类,它通过使用一个代理对象来实现。如 Figure 6 所示,你可以看到客户端和 Remoting 体系结构的服务器端有很多层。客户端的代理对象被称为透明代理,该对象的行为有点象实际的对象,但在现实应用中,它前向调用到下一层——Remoting 代理。Remoting 代理是一个定制的 RealProxy。 ![]() Figure 6 Remoting 体系结构 Figure 7 中显示了服务性组件体系结构中主要的组件,很多地方类似 Remoting 体系结构,因为Remoting 体系结构是实现 Enterprise Services 的基础。第一,对象激活必须被定制,所以某个定制的 ProxyAttribute 被用于截获一个新的调用 。通过截获对象的激活,COM+ 可以根据 COM+ 目录中的信息建立起关联的非托管上下文对象。 ![]() Figure 7 服务性组件体系结构 第二,被截获的激活序列还扩展了 RealProxy 类,创建了两个定制的 RealProxy 对象,称为服务性组件代理和 对应的远程服务性组件代理。服务性组件代理负责准备并布置方法调用拦截。这就是自动事物处理的实现方式。远程服务性组件代理主要负责掌握与各自 ServicedComponent 对象关联的上下文ID。我鼓励你追踪客户和服务器堆栈痕迹以考察相应的 RemotingExceptions 和 EnterpriseExceptions 的解决方法。 当调试 EnterpriseExceptions 程序的时候,别忘了设置 DLLHOST.EXE 为启动程序 以及相应的命令行参数,以便你能在派生于 ServicedComponent 的类中设置断点。另外.如果你在追踪堆栈中看到 一行:“[non-user code]”,只要在单击右键,在 Options 弹出式菜单中打开显示 non-user 代码的选项。 一个 COM+服务程序 最常用的部署场景之一是生成一个 COM+ 代理安装程序,它意味着进程内通讯是通过使用客户端和服务器 .NET Remoting 完成的,中间是 DCOM 通讯通道。使用 COM+ 库体系结构的应用是不同的,因为对象是建立在客户端的进程中,所以没有 DCOM IPC 存取访问,不需要 构造远程服务性组件代理。因此,两者方法方法调用机制是不同的。 从具体细节中,你会看到(尤其是你检查堆栈痕迹的时候)体系结构是相当复杂的,很多地方都有托管代码和非托管代码之间的交互。有一些已写进文档,而有些 则没有写入文档。 此外,COM+ 体系结构中有些关于不同对象和接口的细节没有公开,原因是这些东西目前只被 COM+ 内部实现使用。那么, CustomAppException 是在哪里被改变为一个基类类型(例如:ApplicationException,COMException)的呢?与你期望的类型相反,答案在某些服务 性组件体系结构的优化实现中。 ![]() Enterprise Services(COM+)被设计用来帮助你构建可伸缩的,高性能的企业系统。所以,当存取运行于 COM+ 服务器应用中的服务性组件时要做一些优化。这些优化尽可能利用 COM,以避免远程串行逻辑带来的额外开销。不管怎样,这种优化成为定制异常被转换成更通用的异常类型(如:ApplicationException 和 COMException)的原因。有一工作 是必须做的,那就是需要你有意识的决定如何公开服务性组件的公共接口。下一部分,我将说明你必须理解的设计决定以及它们对这种优化有何影响。 ![]() 任何软件工程。当你设计服务组件的时候你必须考虑你的客户端是谁。特别要考虑你打算支持的客户端的类型。你必须了解你的组件是否要需要支持 .Net 客户 端,或者它是否要同时支持 .Net 和 COM 客户端,如果你有幸做一个项目不用考虑遗留产品或者现有的使用基于 COM 的软件用户,那么你可以先入为 COM 客户端提供支持,否则,你必须设计一个服务组件,并且能从 COM 和 .NET Framework 两者中轻松访问这个组件。这种交互 是 interop 的核心,其所涉及的完整内容大大超出了本文讨论的范围。 有足够的理由让你将组件必须设计成 COM 友好的。 COM 友好意味着要选择适当的数据类型作为方法参数。要意识到版本问题以及它们对 COM 编程模型的影响,决定是否用 .NET 类或 .NET 接口来公开你的组件,要指定几个标准。至于 .NET 类接口与 .NET 接口 以及方法署名,它们是实现上述优化的根源。 当某个服务组件用 COM+ 注册时,程序集中相关的元数据可以通过 System.EnterpriseServices.RegistrationHelper 名字空间 中的函数 获得。其中包括生成类型库并将该信息注入 COM+ 的目录中。默认情况下,.Net 托管代码不通过接口公开函数。而是从类定义中直接 公开。因此,在默认情况下 COM 客户端不能早期绑定 .Net 类。 如果你计划要支持 COM 客户端,那么有两个选择,一是利用 .Net Framework 的默认行为,它公开 .Net 类接口。.Net 类接口是 一个派生于 IDispatch 的接口,由 CLR 产生以使 COM 客户端存取某个类的公有成员。该元数据的生成是由 ClassInterfaceType 类控制,它以 ClassInterfaceType 枚举类型制值作为参数, Figure 8 列出了 ClassInterfaceType 的取值及其每个选项的描述。AutoDispatch 是默认选项,它只允许后期绑定,所以 COM 客户端被从底层类设计中屏蔽掉了, 不幸的是,它 还招致与后期绑定有关的性能问题,当你想用 CLR 生成的类接口的话,这是推荐的选项。 我推荐第二种选择,需要你使用 .Net 接口提供功能。这样做的主要原因是为了将 COM 客户端与底层类设计的变化隔离开。从而避免由于 .Net 类的改动可能造成客户端无法正常工作。这种方法将导致接口被包含在在所生成的类型库中,COM+ 目录亦然。这样也使得 COM 客户端与该接口实现早绑定,与派生于 IDispatch 的 .Net 类接口相比,这样带来的开销更低。发布 .Net 接口的一个额外的好处是 COM 和 .Net 的使用者都能使用你的组件,因为两种编程模型都理解某个接口的含义。缺点是公开 .Net 接口的服务性组件可能被以不同方式隐蔽存取。当通过接口进行远程调用时,COM+ 将试图 通过与 Remoting 序列化相对应的 DCOM 序列化来优化此调用。结果,在这种情况下丢出的异常将经历前述 Interop 的转换过程。 虽然你可以用自动生成 .Net 类接口的方法。但我推荐你明确声明某个接口。即使要做很多前期工作,但你会事半功倍。下面让我来展示两种实现 .Net 接口并仍能保留异常类型的解决方案。 ![]() 这个方案需要接口的方法被设计成这样一种方式:COM+ 不会尝试优化经由 DCOM 的调用。为此我需要调用一个复杂的方法署名——即包含一个不能被标准 interop 封送机制(marshaling)序列化的类型,如 Object。它将强制这个方法调用使用 Remoting 序列化。下面的代码片断展示了该方案的接口声明并包含由简单的和复杂的两种方法署名,比如 StringBuilder 参数: public interface IExceptionGenerator { // 简单方法署名 void GenerateException (string message); // 复杂的方法署名 void GenerateException_ComplexSignature (string message, StringBuilder ComplexParam); }当我执行客户端的时候,会产生一个ApplicationException 和一个 CustomAppException,它们都有各自自己的 堆栈痕迹。如图 Figure 9 和 Figure 10 所示。根据不同的方法署名,我会得到不同的异常类型,并且采用不同的路径。Figure 11 是客户端应用程序在收到原来在 DLLHOST.EXE 运行进程中引发的定制异常属性之后的情形。完整的细节请参考下载的例子代码 EnterpriseExceptions_1 目录。 ![]() Figure 11 定制的异常数据 ![]() 第二种方案是一种二尖分支的方法。它利用在一个瘦托管包装类中实现的正式接口声明来公开函数。然后该包装类将所有的调用委派进主要 .Net 类。它也 是使得托管客户端可存取的类(参见Figure 12 )。这也是需要做最多工作的地方,因为它要实现一个包装类,同时这里也是最通用的地方,因为它为 COM 客户端公开了一个正式的接口以及为托管客户端提供了一个灵活的,强类型的 .NET 类。另外,我还为CustomAppException 类添加了一个 ToXML 方法,使得包装类能捕捉到 CustomAppException 并将它存储在一个 ApplicationException 的 Message 属性中。然后 CLR 将它序列化,在 IErrorinfo 描述中越过 COM 客户端。完整的细节参见 EnterpriseExceptions_2 目录中的实例代码。 ![]() 本文被设计用来帮助你更好的理解服务组件体系结构上下文中的异常概念,以及跨 interop 边界的异常传播。我 呈现了两种解决问题的方案,如果你还知道其它的方法,我愿洗耳恭听。 如你所见,编写一个定制异常是很直接的。只是在通过不同的技术进行传播时,实现上有些细微的差别,它需要细心的观察。同样,决定是否声明一个正式接口也是很有挑战的 事情。我相信一个好的设计会恰到好处地利用类继承和接口实现两者。 ![]() ![]() |
![]() Bob DeRemer 是 Lighthammer Software Development Corporation 资深软件开发工程师 ,专门从事企业应用软件的架构设计和开发。当他外出不在家或者飞行时,你将发现他埋头于由关 .NET 技术的最新出版物里。他的 e-mail 联系方式:bob.deremer@lighthammer.com。 |
本文出自 MSDN Magazine 的 March 2004 期刊,可通过当地报摊获得,或者最好是 订阅 |
![](https://i-blog.csdnimg.cn/blog_migrate/c029e5b2a9dcb8cda92a70c75efd6ade.png)