您的位置:首页>软件开发>.NET>

从 COM 客户端获得连续的 .NET 异常日志记录并且无需修改代码

[ 来源:ccid | 更新日期:2007-7-15 20:19:37 | 评论 0 条 | 我要投稿 ]
本文讨论:

? 为什么异常日志记录很重要

? 异常日志记录在 .NET 中是如何工作的

? 记录所有被引发到 COM 客户端的 .NET 异常

本文使用下列技术:
.NET、COM、C++ 和 C#

代码下载可从以下位置获得:
ExceptionLogging.exe (267KB)




本页内容
为什么记录 .NET 异常?
当前记录 .NET 异常的方式
异常信息丢失
解决办法
挂钩
并发问题
异常队列
异常日志记录线程
ExceptionPublisher 类
回退日志记录
ASP
示例项目
小结

许多仍然使用 ASP 页来提供演示服务的开发人员正在集成 COM 和 .NET 对象,以提供应用程序的业务服务。从 ASP 中使用 .NET 对象可以帮助开发人员在迁移到 ASP.NET 之前获得有关 .NET Framework 的体验。他们的 ASP 页通过 COM 可调用包装 (CCW) 利用新的 .NET 组件。但是,如何处理异常呢? 字串7

每当 .NET 异常从 .NET 组件引发到 ASP 页时,它都被转换为一个包含有关错误的信息的 Err 对象。遗憾的是,诸如堆栈跟踪、所有内部异常和 .NET 异常内部的其他属性之类的大量信息都会丢失;只有字符串说明和一个数字被返回到 ASP 页。为了将引发到 COM 客户端的 .NET 异常添加到事件日志中,我编写了一个使用诸如挂钩之类技术的组件。

为什么记录 .NET 异常?


大多数应用程序都会记录意外的异常,在某些情况下,还会记录预期的异常。异常管理应用程序块 (EMAB) 提供了一个非常有用的框架,用于发布异常。您只需用 Exception 对象调用发布方法。这可以从集中式 catch 处理程序内部操作,也可以从使用 AppDomain 类中 UnhandledException 事件的未处理异常处理程序中操作(参见图 1)。

在诊断问题时,所记录的异常可以提供大量信息。例如,如果所引发的异常包装了另一个异常,则内层异常通常会包含大量详细信息,以便建立错误的上下文。最内层的异常通常包含导致失败的实际错误,例如,数据库架构过时或 SQL Server_ 数据库不可用。 字串6

其他有用的功能包括能够查找与特定异常条件相关的特定信息。例如,ArgumentOutOfRange 异常包含无效的参数值。

运行库能够在引发异常时捕获堆栈跟踪意味着,您可以确定失败的特定函数。例如,如果诸如 IsUserInRole 之类的函数失败并且可以明显地在调用堆栈中看到,请首先查看用户的安全权利或与安全相关的代码。

正是因为有了上述优势,所以能够记录所引发的任何 .NET 异常(不管是使用 .NET 还是 COM 客户端)是有价值的。

返回页首
当前记录 .NET 异常的方式


大多数 ASP 开发人员都使用某种形式的日志记录组件将错误记录到特定的目的地,例如,事件日志或平面文件。当错误条件出现时,通常会设置一个标志,以指示该页已经失败并且应当记录相关信息。有关错误的唯一信息存在于 Err 对象中。Err 对象提供了对 Number 和 Description 属性的访问,并且 Description 属性包含有关错误的最详细信息。下面显示了示例 ASP 代码:

On Error Resume Next 字串6
Dim oSupplierInvoice
Set oSupplierInvoice = Server.CreateObject("ECommerce.SupplierInvoice")
oSupplierInvoice.GenerateFromOrder(oPurchaseOrder)
If Err.Number <_ _0="_0" then="then" br="br" /> LogPageError "Failed to generate purchase order for supplier invoice,
SupplierInvoiceID=" & oSupplierInvoice.ID
End If

LogPageError 是一个虚构的方法,它包装了对日志记录组件的访问,并且将其他上下文添加到错误日志中,例如,被调用的页、会话中的用户以及来自 Err 对象的 Description 和 Number。如果 ECommerce.SupplierInvoice 组件被编写为 COM 对象,则 Description 和 Number 将是该组件产生的唯一信息,因此不会丢失任何信息。但是,如果该组件是用 .NET 语言编写的,则会引发 CLR 异常并将其转换为 Err 对象,从而使 CLR 异常中的大量有用信息丢失。

返回页首
异常信息丢失


当 COM 客户端使用 .NET 组件时,将创建 COM 可调用包装。CCW 是实际 .NET 对象的代理。该对象在 mscorwks.dll 或 mscorsvr.dll 中实现,具体取决于诸如系统具有多少个处理器或环境变量设置等因素。这两个 DLL 之一由 mscoree.dll 加载,后者充当代理以加载模块 mscorwks 和 mscorsvr 中的一个。
字串1


当 .NET 异常被引发时,CCW 将检索 .NET 异常接口并基于它包含的信息创建一个 COM 错误对象。通常可以查询该错误对象以获得说明和编号。说明取自该异常的 Message 属性,而编号则取自该异常的 HResult 属性(如果可用)。

Adam Nathan 在他的优秀著作 .NET and COM: The Complete Interoperability Guide (Sams Publishing, 2002) 中介绍了一种技术,使用该技术,可以通过调用 GetErrorInfo 为 COM 客户端获得 .NET 异常。该方案之所以有效是因为,在 CCW 内部捕获 .NET 异常之后,mscorsvr/mscorwks DLL 会对 SetErrorInfo 进行调用。这会检索当前线程上设置的最后一个错误对象。然后,可以为 .NET 异常接口 _Exception 查询返回的 IErrorInfo 接口指针。以下为完成该工作的 C++ 代码:

try
{
... // Work which throws a .NET exception through a CCW
}
catch(_com_error &err)
{
_ExceptionPtr spException = err.ErrorInfo();
}
字串6


这里您可以看到,只需查询在当前逻辑线程上设置的 IErrorInfo 指针,就可以检索到 .NET 异常。遗憾的是,该方案只对使用为该组件定义的特定接口的早绑定客户端有效。晚绑定脚本客户端(例如,ASP 页)总是使用 IDispatch Invoke 方法来调用在 .NET 组件上定义的方法和属性。Invoke 方法调用 GetErrorInfo 来检索丰富的错误信息,以便设置 EXCEPINFO 结构的相关字段。然后,脚本引擎使用该结构生成可供 ASP 页查询的 Err 对象。

由于 GetErrorInfo 调用还会清除错误信息,因此在晚绑定调用完成之后,该对象将不可用。如果不会发生这种情况,则理论上可以在另一个 C++ COM 服务器中编写一个方法,以便简单地调用 GetErrorInfo 并检索 .NET 异常接口指针。



图 2 通过 CCW 进行的 .NET 调用



为了解该方案对于晚绑定客户端失败的原因,我将演练从晚绑定 COM 客户端进行的托管方法调用。.NET 方法调用是通过由 asp.dll 承载的脚本引擎(VBScript 或 JScript_)中的晚绑定启动的。这会导致在 CCW 上调用 IDispatch::Invoke(传递一个 EXCEPINFO 结构)。接下来,CCW 调用托管对象上的实际托管方法。在我们的方案中,该方法会引发异常。CCW 捕捉该异常并调用 SetErrorInfo 来存储该异常的信息。IDispatch::Invoke 的实现看到发生了异常,并且调用 GetErrorInfo 以检索它填充 EXCEPINFO 结构所需要的信息。由于 GetErrorInfo 清除了错误信息,因此当 IDispatch::Invoke 返回时,.NET 异常会丢失。该过程在图 2 中进行了概述。 字串2

返回页首
解决办法


使异常得以发布的一种解决办法是修改托管代码组件中的所有属性/方法,以捕捉所有类型的异常,并且在重新引发之前发布它们:

public void DoSomethingUseful()
{
try
{
... // Do some useful work
}
catch(Exception exception)
{
ExceptionManager.Publish(exception);
throw;
}
}

显然,该方法具有严重缺陷。那就是存在重大的开发开销:所有可以由 COM 客户端调用的 .NET 方法/属性都需要修改,以包含 catch 处理程序。实际上,这将意味着要修改所有公共方法/属性,因为预测组件将来的用途是不可能的。

该方法的另一个问题是,如果 .NET 组件正在由 .NET 客户端使用,则该客户端也可能发布异常,从而最终导致该异常被发布两次。如果该组件上的某个公共方法调用该组件上的另一个公共方法,则也会出现这一问题。
字串3


此外,由于 Jeffrey Richter 的优秀著作 Applied Microsoft .NET Framework Programming (Microsoft Press_, 2002) 中提到的各种原因,捕捉 Exception 类型的异常被视为一种不好的做法。例如,可能会引发 StackOverFlow 异常,在这种情况下,catch 处理程序不应当再完成其他任何可能使堆栈进一步增加的工作(例如,发布异常)。

即使上述理由并不充分,但有时 .NET 组件(例如,第三方组件)的源代码不可用。唯一的解决方案是为该组件实现 .NET 代理,这只需委托给由 try/catch 块围绕的对象。

现在,让我们考察一下如何创建相应的组件,以便在 .NET 异常被引发到 COM 客户端时为这些异常提供连续的日志记录,同时无需对 .NET 组件进行任何修改。通过 CCW 引发到 COM 客户端的任何 .NET 异常都会被 EMAB 自动记录下来。

返回页首
挂钩


在 Windows? 中,DLL 所导出的函数可以替换为其他可提供相同签名的自定义函数。该技术称为“挂钩”。它通过更改 DLL 的导入地址表 (IAT) 内部的函数地址指针来工作。每个被导入的 API 都在 IAT 中具有它自己的保留位置。在 IAT 中,导入函数的地址由 Windows 加载程序写入。在模块加载以后,IAT 就包含在调用导入的 API 时调用的地址。 字串3

我的挂钩函数包含在 HookSupport 类中,该类作为与本文关联的代码下载的一部分,可在 MSDN_Magazine Web 站点上得到。该类上最重要的方法是 HookImportFunctionsByName,它采用将在其中挂钩导入函数的模块句柄、要挂钩的模块的名称、包含要挂钩的函数名称的结构数组,以及新的自定义函数过程地址。John Robbins(MSDN Magazine 的 Bugslayer 专栏作家)在使用 Win32_ 的时期经常说明如何挂钩函数。我将使用 John 在他的调试著作 Debugging Applications for Microsoft .NET and Microsoft Windows (Microsoft Press, 2003) 中介绍的技术。

正如前面提到的那样,ASP 客户端无法使用 .NET 异常的原因是,该异常最初被使用 SetErrorInfo 设置为线程的错误对象;随后,当脚本运行库调用 GetErrorInfo 以生成 IDispatch Invoke 方法所必需的 EXCEPINFO 结构时,该异常被清除。在理论上,如果可以在另一个自定义函数中挂钩和替换 SetErrorInfo 函数,则在脚本运行库调用 GetErrorInfo 以清除 .NET 异常接口指针之前,可以使用 EMAB 提取和发布它。

SetErrorInfo 函数定义在 OLEAUT32 模块中。遗憾的是,尽管该模块是从 mscorwks/mscorsvr 中加载的,但无法容易地挂钩它,因为 OLEAUT32 模块在 mscorwks/mscorsvr 的导入地址表中不存在(该模块只存在于单独的延迟加载 IAT 中)。这是因为该模块被延迟加载:在运行时,LoadLibrary 和 GetProcAddress 被调用以解析 SetErrorInfo 的函数地址。得到的地址随后被存储在延迟加载 IAT 中,以便将来的调用能够直接转向 API。
字串2


尽管挂钩依赖于对模块的导入表进行修改,但对于延迟加载的 DLL 而言,在模块内部对该函数进行首次调用之前,必须完成这一工作。由于您不显式加载 mscorwks.dll,因此当您开始挂钩时,您无法确保尚未对 SetErrorInfo 进行首次调用。

解决方案要求依次挂钩不同模块中的多种函数。首先,我为 OLE32 模块中的 LoadLibrary 调用设置了初始挂钩。当模块 mscoree.dll 正在由挂钩安装程序中的 OLE32 加载时,将为来自 mscoree.dll 模块的 LoadLibrary 调用设置其他挂钩。当模块 mscorwks.dll 正在由第二个挂钩安装程序中的 mscoree.dll 加载时,将为来自 mscorwks.dll 模块的 GetProcAddress 调用设置其他挂钩。(请注意,有时还可以使用 mscorsvr,具体取决于各种因素,包括处理器的数量。)在上述挂钩就绪以后,当使用 GetProcAddress 挂钩的 mscorwks.dll 模块进行 ErrorInfo 调用(按名称或序号值)时,将返回指向我的自定义 SetErrorInfo 的函数指针,这将会发布异常。图 3 对此进行了说明。


字串6

图 3 各种挂钩



在模块 OLE32.dll 和 mscoree.dll 的 LoadLibrary 挂钩函数中,总是首先进行 Win32 LoadLibrary 调用。这将确保模块请求被加载,并且可用来向来自这些模块的 LoadLibrary 和 GetProcAddress 调用安装其他挂钩。这里,需要解决的最重要的问题是确保在承载 .NET 组件的进程内部足够早地产生挂钩。

当 .NET 异常引发并且被 CCW 捕获时,将调用自定义的 SetErrorInfo 函数。然后,可以通过查询 _Exception 接口从该函数中提取 .NET 接口指针,如下所示:

HRESULT __stdcall SetErrorInfoWithExceptionLogging(
DWORD dwReserved, IErrorInfo *pErrInfo)
{
CComQIPtr<_exception /> spException(pErrInfo);
if (spException)
{
// Perform work with .NET exception interface
}
}

字串9


图 4 中显示了对各种挂钩函数的调用。

在将函数挂钩以后,它们将一直保持挂钩状态,直到进程终止。如果在 DLL 已经卸载之后,进程内部引发了错误,则这可能导致访问冲突,因为 SetErrorInfo 挂钩仍然指向该 DLL 中定义的函数,而该 DLL 在卸载后将指向无效地址。为了解决该问题,DllCanUnloadNow 函数返回 S_FALSE 以防止 DLL 卸载。替代方法是重置原始的挂钩函数指针。无论您选择哪一种方法都可以。

返回页首
并发问题


挂钩的 SetErrorInfoWithExceptionLogging 函数应当是线程安全。首先,您可能希望将 .NET 对象创建和日志记录放在自定义的 SetErrorInfoWithExceptionLogging 函数中,并且使用临界区来防止并发访问。但是,请考虑以下事件序列。首先,线程 A 导致错误并进入 SetErrorInfoWithExceptionLogging,从而获得一个临界区。当线程 A 执行时,它试图获得由线程 B 拥有的另一个资源,因此它等待该资源可用。当线程 B 拥有该资源时,它会生成错误。然后,在线程 B 上调用 SetErrorInfoWithExceptionLogging 函数,以阻止获得该临界区的企图。 字串6

结果:发生死锁!SetErrorInfoWithExceptionLogging 中的代码不会直接尝试获得可能被其他线程拥有的其他资源,但是,如果需要在它内部创建 .NET 对象,则会发生多个事件。例如,在创建 .NET 对象时,系统可能需要加载 DLL。这意味着在该时间间隔获得加载程序锁,这会造成发生死锁的可能。如果在 SetErrorInfoWithExceptionLogging 中创建了 .NET 对象并使用它们来发布异常,则会从单线程单元 (STA) 向多线程单元 (MTA) 进行跨单元调用。这可能造成出现重入情况,其中,SetErrorInfoWithExceptionLogging 被在某个 STA 中调用,并且挂钩函数对另一个单元进行传出 COM 调用,这会导致 COM 库进入 STA 消息循环。当另一个针对该 STA 中承载的对象的 COM 方法调用进入时,它将被接受,因为我们处于消息循环中。在处理该新请求期间,SetErrorInfoWithExceptionLogging 函数被再次调用。

由于存在上述并发问题,因此 SetErrorInfoWithExceptionLogging 函数尽可能少地完成工作。它只是向从 ManagedExceptionLoggerTask 辅助线程中访问的队列中添加异常。ManagedExceptionLoggerTask 对象执行异常的日志记录,这将完成创建托管的 ExceptionPublisher 对象和发布异常的重要工作。该线程不会遇到相同的锁定问题,从而使其成为发布异常的好地方。 字串3

返回页首
异常队列


为了尽可能减少阻塞问题,创建了一个新的队列对象。托管异常是在先入先出基础上添加和移除的。这些异常将由另一个辅助线程从该队列中移除,然后使用异常管理应用程序块发布。

该队列内部存储的对象的类型不能是 .NET 异常接口指针,因为该异常需要从辅助线程中跨单元访问。例如,自定义挂钩函数可以在 STA 上调用,但是辅助线程使用 MTA。在 ASP 应用程序中,每个客户端都将在它自己的 STA 上调用。

全局接口表 (GIT) 存储接口指针,以便可以跨单元安全地访问它们。在一个单元中添加的对象可以从 GIT 中移除,并且可以在另一个单元中访问。当接口被添加到 GIT 中的时候,将在 DWORD 中返回一个 Cookie。该 DWORD 需要存储在队列中,以便辅助线程可以简单地将该 DWORD 弹出队列,并且使用 GIT 提取与该 Cookie 相对应的 .NET 异常对象。

该异常的引发日期和时间也需要记录在队列中的元素内部。考虑以下情况:错误被引发,自定义的 SetErrorInfo 函数被调用,异常被添加到队列中,并且辅助线程被通知发布该异常。如果该辅助线程在一段时间内没有得到调度,则即使只过了几秒钟,其他异常也可能在第一个异常被实际发布之前被添加到队列中。因而,事件日志中实际记录的时间将与引发该错误的实际时间稍微不同步。因此,所记录的时间是至关重要的。所记录的时间和 .NET 异常 Cookie 将包装在结构内部,该结构将被添加到队列中。 字串7

因为 STL 队列类不是线程安全的,并且需要完成少量工作以便向队列中添加异常,所以创建了 ManagedExceptionQueue 类。这将使用类临界区来保护对队列进行的同时访问,将 GIT 的使用包装到该类上的 push 和 pop 函数中,并且为辅助线程和自定义挂钩函数提供了一个良好的接口,以便在队列中设置和检索异常信息。该过程显示在图 5 中。



图 5 两个通过 CCW 引发异常的客户端



在自定义 SetErrorInfo 函数将异常添加到队列中以后,就会在辅助线程内部产生一个事件,以通知它可以发布异常。队列类的代码存在于 MSDN Magazine Web 站点上的下载中的 ManagedExceptionQueue.cpp 中。

返回页首
异常日志记录线程


创建了一个新的 ManagedExceptionLoggerTask 类以充当辅助线程。该线程将一直处于空闲状态,以等待下列两个事件中的一个发生:新的异常已经添加到队列中,或者线程应当终止。当应用程序即将关闭时,通常应当请求任务完成。 字串2

在 ManagedExceptionInjector COM 对象上的 Inject 方法中设置了初始挂钩之后,将创建新线程。如果该线程因为另一个线程已经产生 Exception Added 事件而醒来,则将使用 ManagedExceptionQueue pop 方法从队列中检索每个异常。这将使用存储的 Cookie 从 GIT 中提取和返回接口指针。然后,将使用 ExceptionPublisher 类通过 CCW 发布每个异常。如果异常发布由于任何原因而失败,则会尝试从该异常中提取有用的信息(例如,堆栈跟踪和内层异常)并且将该信息记录到事件日志中。如果这种做法仍然失败,则会使用 OutputDebugString Win32 函数将异常信息输出到调试窗口。该异常记录过程显示在图 6 中。



图 6 异常记录过程



ManagedExceptionLoggerTask 加入一个 MTA;当创建 ExceptionPublisher 对象时,它将执行该 MTA 中的所有异常发布,以避免跨单元调用。向 COM 公开的托管对象(例如,ExceptionPublisher)的行为好像它们已经聚合了自由线程封送拆收器。换句话说,可以用自由线程方式从任何 COM 单元中调用它们。唯一没有展现这一自由线程行为的托管对象是那些派生自 ServicedComponent 的对象。

字串9



ExceptionInfo 结构实例是使用 ManagedExceptionQueue pop 方法从队列的顶部提取的。该结构包含异常的引发日期和时间。该日期和时间可用来创建一个托管的 NameValueCollection 对象(该对象带有一个包含日期值的名称值项)。该值的名称为 ExceptionRaisedDateTime;只有在显示事件日志中的异常时,才会使用它。

返回页首
ExceptionPublisher 类


需要将从挂钩的 SetErrorInfo 中提取的 .NET 异常中的信息记录在某处 ― 无论是文件、事件日志还是电子邮件。特别地,需要将前面提到的所有信息(例如,异常的属性、堆栈跟踪和任何内层异常)包含在日志中。

Microsoft 的 Patterns and Practices Group 已经专门针对此目的创建了 EMAB。如果您尚未完成有关工作,那么我强烈建议您查看其他所有可用的应用程序块,以便了解它们是否适合于在您的当前项目中使用。异常块是一种通过 ExceptionManager 类发布异常信息的简单而灵活的机制。它还使您可以创建自己的发布程序,以便向其他数据源(例如,XML 文件或数据库)发布数据。 字串2

需要调用该类中的 Publish 方法来发布异常。该方法被定义为静态方法,这将便于 .NET 客户端使用;但是,这对于 COM 客户端不够友好,因为它们无法调用静态方法。一种简单的解决方案是,使用门面模式将对该类的访问包装到另一个类中。为了使 .NET 组件更容易被 COM 客户端使用,开发人员应当遵守各种准则。其中一个准则是确保该类具有默认构造函数。创建了两个新的程序集,它们包含 ExceptionPublisher 类和 IExceptionPublisher 接口。以下代码显示了该类中的包装方法:

public void Publish(Exception exception, NameValueCollection nameValueItems)
{
ExceptionManager.Publish(exception, nameValueItems);
}

重载的 Publish 方法被调用,它不仅采用异常对象,而且还采用一个名-值项集合(这些项可以在发布异常时指定其他重要数据)。ManagedExceptionLoggerTask 辅助线程使用该方法来添加异常引发的日期和时间。

ExceptionPublisher 类还利用了其他一些最佳技术。这包括对类和接口使用显式 GUID,以及从自定义接口派生而不是使用默认的类接口。有关这些技术的详细信息,请参阅 .NET and COM: The Complete Interoperability Guide。通过提供默认构造函数和一个用于发布异常的公共非静态方法,我已经创建了一种容易的方式,以便 COM 客户端使用我的类来发布异常:
字串3


IExceptionPublisherPtr spExceptionPublisher(__uuidof(ExceptionPublisher));
spExceptionPublisher->Publish(spException, spNameValueItems);

返回页首
回退日志记录


如果无法使用异常块发布 .NET 异常(由于诸如无法创建对象或权限不足等原因),则应当尝试直接记录到事件日志中。如果这种办法也失败,则最后一个办法是将异常写到调试窗口中。有趣的是,如果 ExceptionPublisher 的 Publish 方法失败,则必须将两个异常记录到事件日志中。第一个异常是从 Publish 方法中引发的 .NET 异常(它包含有关 Publish 方法失败原因的信息),第二个异常是被记录的实际异常。

第一个异常实际上可以使用本文开头介绍的、Adam Nathan 的技术提取,因为这里使用了早绑定 COM 客户端来调用 .NET 组件。.NET 异常是通过查询 _Exception 接口直接从错误对象中提取的。

异常信息被在 CManagedExceptionLoggerTask::LogExceptionToEventLog 方法中添加到事件日志中。该方法的工作方式类似于 Publish 方法,即提取所有内层异常并且为每个异常写出堆栈跟踪。如果在记录到事件日志的过程中发生任何错误,则会将原因和异常信息写到调试窗口中。

字串1



返回页首
ASP


ASP 应用程序通常在 dllhost 或 inetinfo 进程中运行,具体取决于隔离级别。例如,中级(它在 Windows NT_ 中不可用)和高级使用 dllhost.exe,而低级总是使用 inetinfo。每个使用 ManagedExceptionInjector 组件的 ASP 应用程序都不仅必须创建该对象并调用 Inject,而且必须在 Application 变量内部存储对它的引用。以下为完成该工作的 ASP 代码(为了简洁起见,省略了错误处理):

Dim oInjector
Set oInjector = Server.CreateObject(
"NetInteropServicesEngine.ManagedExceptionInjector")
Set Application("ManagedInjector") = oInjector

需要在应用程序 OnStart 事件内部进行有关调用,以便在进程内部注入所有必需的挂钩。这可以确保在进程中的其他任何客户端导致对 CCW 调用 SetErrorInfo 之前进行有关调用;否则,无法设置挂钩。应当将该调用放置在该事件处理程序的开头以及其他任何代码之前。
字串9

通过在应用程序变量中存储对象,我们可以在应用程序 On End 事件中访问它,然后调用 UnInject 方法。该对象必须具有单元灵活性。换句话说,必须可以从进程内部的任何单元中访问它,以便能够将其存储在应用程序变量中。为此,该对象使用 CoCreateFreeThreadedMarshaler 函数聚合了自由线程封送拆收器。

当 Web 应用程序结束(因为 Web 服务器已经重新启动,或者特定应用程序已经卸载)时,应用程序 On End 事件将被调用(这只对通过高隔离级别运行的应用程序可用)。UnInject 方法只是通知 ManagedExceptionLoggerTask 辅助线程完成并清理。

返回页首
示例项目


您可以从 MSDN Magazine Web 站点下载一个名为 ManagedExceptionWebApp 的示例 Web 项目。主页提供了指向其他三个页的超链接。其中,一个页只是显示文本,另一个页将几个数字加在一起,最后一个页调用 MSDN.ExceptionGenerator 命名空间中一个名为 PurchaseOrder 的 .NET 对象(这总是引发 .NET 异常)。

在 ThrowNetException ASP 页中,创建了一个托管的 PurchaseOrder 对象并且调用了 GenerateOrderForSupplier 方法。图 7 中显示了该方法的代码。该示例遵循一种常见的设计模式:使用数据访问类访问数据存储,同时让业务对象负责提供业务对象的功能。 字串8

PurchaseOrderData GenerateOrderForSupplier 试图使用显然不正确的连接登录数据库,如以下代码片段所示:

public void GenerateOrderForSupplier(String supplierName)
{
using(SqlConnection connection = new SqlConnection(
ConfigurationSettings.AppSettings["connStr"))
{
connection.Open();
... // Code to insert purchase order details into database
}
}

当从 PurchaseOrderData GenerateOrderForSupplier 方法中引发 SqlException 时,它被在 PurchaseOrder GenerateOrderForSupplier 方法中捕获。然后,该异常被使用自定义的异常类型 PurchaseOrderException 再次引发。该类型被用于在处理购买订单时引发的所有异常。它派生自 BaseException,这对于所有为在 EMAB 中使用而创建的异常类而言是一个必要条件。

要运行 Web 应用程序,需要将其安装在一个名为 ManagedExceptionWebApp 的新的虚拟目录中。需要生成 .NET 程序集,将其复制到应用程序文件夹中,并且使用 regasm 向 COM Interop 注册。还需要将异常块程序集 Microsoft.ApplicationBlocks.ExceptionManagement.dll 和 Microsoft.ApplicationBlocks.ExceptionManagement.Interfaces.dll 复制到该文件夹中。(请注意,需要使用 InstallUtil 实用工具安装 Microsoft.ApplicationBlocks.ExceptionManager.dll 程序集,以便在从 ASP 页中使用它之前,注册多个不同的事件源。)如果该操作未获执行,则 ASP 应用程序将尝试在事件源被首次引用时创建它们,并且在某些配置中,这可能导致安全异常,原因是匿名用户不具有创建事件源的权限。一个名为 RegisterTypeLibs 的文件从调试文件夹中复制这些程序集,并且向 COM Interop 注册它们。

字串3



创建 .NET 组件的 CLR 宿主的应用程序文件夹将是 system32(当 dllhost 被用于将保护级别设置为“中”或“高”的应用程序时)或 system32inetsrv(当应用程序的保护级别为“低”时)。相应的配置文件将因此称为 dllhost.exe.config 和 inetinfo.exe.config(如果需要运行库配置)。

返回页首
小结


在本文中,我已经讨论了能够在引发异常时截获 .NET 运行库 CCW 调用的对象的设计和实现。这可以提供连续的异常日志记录,而无需对 .NET 组件进行任何更改。我还分析了我在尝试生成能够在生产环境中正常工作的健壮解决方案时遇到的各种问题。

Framework 不仅向您展示了多种低级别技术(例如,将工作挂钩和调度到队列中),而且它还通过生成您可以直接在自己的应用程序中广泛应用的有用组件完成了相应工作。


Tags:
责任编辑:
您的评论
用户名: 新注册) 密码: 匿名评论 [所有评论]

·用户发表意见仅代表其个人意见,并且承担一切因发表内容引起的纠纷和责任
·本站管理人员有权在不通知用户的情况下删除不符合规定的评论信息或留做证据
·请客观的评价您所看到的资讯,提倡就事论事,杜绝漫骂和人身攻击等不文明行为