JException - 在 C++ 中获得有意义的异常





5.00/5 (9投票s)
带有嵌入式堆栈跟踪和原因的 C++ 异常。
引言
作为开发人员,我最沮丧的情景之一是收到用户关于某个错误的投诉,却不知道如何重现它。Java 的一个强大贡献(C# 紧随其后)是拥有功能齐全的异常,这些异常维护了大量关于其发生情况的数据。本文介绍了一个类似的 C++ 异常基类,其主要功能包括:
- 拥有抛出异常的调用堆栈跟踪。
- 在使用 DCOM 时,能够在客户端和服务器进程之间传播,同时保持客户端和服务器的堆栈跟踪。
- 便捷的方法,用于在异常发生时添加关于上下文的文字描述。
JException(J 代表 Java,其内置堆栈跟踪的异常启发了我进行此实现)是使用 Visual Studio 2008 (VC9) 编译器编写的;但是,此概念也可以由 Visual Studio 的早期版本和其他编译器实现。
运行演示
演示展示了两种情况:第一种,异常在同一进程中抛出并捕获。第二种,调用服务器,服务器抛出的异常由客户端处理。执行演示的最佳方式是运行 Start.bat 批处理文件,该文件随后也会终止服务器。程序的输出同时发送到标准输出和调试输出。要查看它,可以运行 Debug View for Windows,或者直接从命令行窗口或 shell 调用批处理文件。演示中的代码片段:
void Func3()
{
throw JException("Demo of a failure. Error %d", 67);
}
void Func2()
{
Func3();
}
void DoSomething()
{
Func2();
}
void ActOnTheServer(IDemoDCOM* p_demoCOM)
{
// Use COM_ACTION macro to handle exceptions
// thrown by the server and DCOM errors
COM_ACTION(p_demoCOM->DoSomethingRemote());
}
int _tmain(int argc, _TCHAR* argv[])
{
try
{
Trace("[Info] An example of throwing an exception " +
"within the same process and thread\n");
DoSomething();
}
catch (JException& e)
{
e.PrependToCause("Failed to Do Something due to: ");
Trace("[Error] %s", e.GetCause().c_str());
Trace("[Error] Exception stack trace: \n%s\n", e.GetStackTrace().c_str());
}
try
{
Trace("[Info] An example of handling an exception " +
"that is thrown by a remote server\n");
// Create the server locally
IDemoDCOM* p_demoCOM = CreateDemoProxy("localhost");
ActOnTheServer(p_demoCOM);
p_demoCOM->Release();
CoUninitialize();
}
catch (const JException& e)
{
Trace("[Error] %s", e.AsString().c_str());
}
return 0;
}
输出结果如下:
[Info] An example of throwing an exception within the same process and thread
[Error] Failed to Do Something due to: Demo of a failure. Error 67
[Error] Exception stack trace:
f:\exceptions\sandbox\exceptiondemo\exceptiondemo.cpp (17): Func3
f:\exceptions\sandbox\exceptiondemo\exceptiondemo.cpp (23): Func2
f:\exceptions\sandbox\exceptiondemo\exceptiondemo.cpp (28): DoSomething
f:\exceptions\sandbox\exceptiondemo\exceptiondemo.cpp (42): main
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c (586): __tmainCRTStartup
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c (403): mainCRTStartup
7C817077 (kernel32): (filename not available): RegisterWaitForInputIdle
[Info] An example of handling an exception that is thrown by a remote server
Attempt to create an instance of DemoServer on host: localhost
[Error] Demo of a failure on the server side
f:\exceptions\sandbox\demoserver\demodcom.cpp (12): ServerFunc3
f:\exceptions\sandbox\demoserver\demodcom.cpp (14): ServerFunc2
f:\exceptions\sandbox\demoserver\demodcom.cpp (16): SomeApplicationLogic
f:\exceptions\sandbox\demoserver\demodcom.cpp (26): CDemoDCOM::DoSomethingRemote
77E799F4 (RPCRT4): (filename not available): CheckVerificationTrailer
77EF421A (RPCRT4): (filename not available): NdrStubCall2
77EF5EA5 (RPCRT4): (filename not available): NdrCStdStubBuffer2_Release
771329AF (OLEAUT32): (filename not available): DllGetClassObject
77600C15 (ole32): (filename not available): StgGetIFillLockBytesOnFile
77600BBF (ole32): (filename not available): StgGetIFillLockBytesOnFile
7752AD31 (ole32): (filename not available): CoRevokeClassObject
7752AC56 (ole32): (filename not available): CoRevokeClassObject
7752B771 (ole32): (filename not available): DcomChannelSetHResult
77600E1F (ole32): (filename not available): StgGetIFillLockBytesOnFile
77602DF3 (ole32): (filename not available): WdtpInterfacePointer_UserMarshal
77600DD6 (ole32): (filename not available): StgGetIFillLockBytesOnFile
7752B7AB (ole32): (filename not available): DcomChannelSetHResult
7752B5E1 (ole32): (filename not available): DcomChannelSetHResult
7E418734 (USER32): (filename not available): GetDC
7E418816 (USER32): (filename not available): GetDC
7E4189CD (USER32): (filename not available): GetWindowLongW
7E4196C7 (USER32): (filename not available): DispatchMessageA
c:\program files\microsoft visual studio 9.0\vc\atlmfc\include\atlbase.h (3534):
ATL::CAtlExeModuleT<CDemoServerModule>::RunMessageLoop
c:\program files\microsoft visual studio 9.0\vc\atlmfc\include\atlbase.h (3552):
ATL::CAtlExeModuleT<CDemoServerModule>::Run
c:\program files\microsoft visual studio 9.0\vc\atlmfc\include\atlbase.h (3364):
ATL::CAtlExeModuleT<CDemoServerModule>::WinMain
f:\exceptions\sandbox\demoserver\demoserver.cpp (26): WinMain
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c (578): __tmainCRTStartup
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c (403): WinMainCRTStartup
7C817077 (kernel32): (filename not available): RegisterWaitForInputIdle
-------------------------
f:\exceptions\sandbox\exceptiondemo\exceptiondemo.cpp (33): ActOnTheServer
f:\exceptions\sandbox\exceptiondemo\exceptiondemo.cpp (56): main
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c (586): __tmainCRTStartup
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c (403): mainCRTStartup
7C817077 (kernel32): (filename not available): RegisterWaitForInputIdle
在异常中嵌入堆栈跟踪
我们通过抛出异常来通知意外情况。例如,假设我们编写了一个处理图像的库,其中一个方法 DoSomething(image)
在执行其操作之前检查图像是否非空:
if (image.GetSize() == 0)
throw ("Failed to Do Something, since image is empty");
假设我们的图像库很有用,它将由我们的团队广泛使用,并且在我们的软件产品经过调试和测试后,它将作为我们产品的一部分交付给我们的客户。在那里,我们可能会遇到一个不愉快的意外:客户找到了一个导致空图像作为参数传递给 DoSomething
并导致失败的路径。听起来熟悉吗?DoSomething
抛出异常,然后呢?在某个地方捕获了异常,并可能向客户显示错误消息。然而,客户通常无法分析他做错了什么或如何修复。他抱怨,我们尝试找出对 DoSomething
的调用堆栈跟踪。不幸的是,没有办法。请注意,当这种情况发生在调试环境中时,没有问题:VC 为您中断,您可以检查堆栈。但是,在运行时环境中,无论是在 Alpha、Beta 还是客户现场,它都不会那样工作。假设我们有某种日志记录,这就是 JException 变得有用的地方。此异常在其构造函数中记录堆栈跟踪并维护它以备将来调查。在典型用法中,异常的原因显示给用户,堆栈跟踪写入日志文件。
try
{
DoSomething(image);
...
} catch (const JException& e)
{
AfxMessageBox( e.GetCause(), MB_ICONEXCLAMATION);
Trace(e.GetStackTrace());
}
通过 DCOM 在进程之间传播异常
Windows 基于 C++ 的“默认”进程间通信机制是 DCOM。与其他协议(例如 CORBA)相比,它的一个缺点是服务器端抛出的异常无法到达客户端。DCOM 使用 IErrorInfo
机制,该机制仅限于在发生错误时将字符串从服务器传输到客户端。但是,使用 JException,我们可以利用此功能将异常信息从服务器传输到客户端,如下所示:
- 将异常序列化为 XML 字符串;
- 将字符串作为
IErrorInfo
传递给客户端; - 在客户端重建等效异常;
- 抛出重建的异常。
现在,重新抛出的异常的堆栈跟踪可以由服务器和客户端的堆栈跟踪(以文本方式分隔)组合而成。这为程序员提供了引发异常的事件序列的完整图景。
用法
每次调用服务器时,都应该捕获它抛出的任何异常,并使用序列化的异常设置 IErrorInfo
接口。当然,这应该在服务器端完成,方法是用 try catch
子句将业务逻辑代码括起来,如下所示:
STDMETHODIMP CServer::DoSomething(const long aParameter)
{
try
{
... // Do your business logic
}
catch (const JException& e)
{
return CComCoClass::Error(e.ToXml());
}
return S_OK;
}
客户端应该执行相反的操作。此代码处理由服务器抛出的异常以及 DCOM 本身固有的错误,例如通信故障。后者也被转换为 JException。
HRESULT hr;
if ( ( hr = server->DoSomething(anArgument) ) < 0 )
{
USES_CONVERSION;
/* Smart pointer to release IErrorInfo when done with it */
CComPtr<IErrorInfo> pError = NULL;
JException e;
if ( GetErrorInfo( 0, &pError ) == S_OK && pError != NULL ) {
/* Smart pointer wrapper to release strError when done with it */
CComBSTR strError;
pError->GetDescription( &strError );
/* Parse the string to form a JException */
e = JException::FromXml( strError );
} else {
/* Error info is missing, try to figure out what went wrong from the hr */
string cause = TranslateCOMException( hr );
e = JException( cause );
}
throw e;
}
此代码应在每次调用服务器时重复。它不能转换为函数,因为服务器的每个函数可能具有不同的签名。相反,我们可以使用宏。文件 errorHandling.h 包含一个 COM_ACTION
宏,它正是这样做的。使用此宏时,代码变为:
COM_ACTION(server->DoSomething(anArgument))
实现
堆栈跟踪
JException 在构造时使用 StackWalker64(由 Jochen Kalmbach 贡献)的轻微修改版本生成堆栈跟踪。这些修改使其输出更适合此用法,并允许一个参数来丢弃前给定数量的帧。它用于从堆栈跟踪中丢弃 JException 本身的构造。当从 XML 在客户端重建异常时,它也很有用。解析和重建本身不是异常情况的一部分,因此被丢弃。如果堆栈跟踪功能可用(例如,Linux 的 pstack),则此概念可以适用于 Windows 以外的平台。
通过 DCOM 传播异常
一个 简单的 XML(由 Dr. Ir. Frank Vanden Berghen 贡献)用于序列化和解析。它可以很容易地替换为任何其他适合自己喜好的解析器。
从 JException 派生
本段仅适用于 DCOM 用户。要从 IErrorInfo
字符串重现适当的异常,需要使用静态方法 JException::FromXml
,该方法又使用 ExceptionFactory
。从 JException
派生的异常应重写 GetClassName()
以返回一个唯一标识其类型的字符串。然后,ExceptionFactory
可以使用此字符串来实例化适当的 JException
派生类型。对从 JException
派生的异常的另一个限制是它们应该有一个带有签名 SomeException(const int skipFrames, const string& cause, const string& preStackTrace)
的 protected
或 private
构造函数,仅供 ExceptionFactory
使用。例如,请参见 TimeoutException
。不使用 DCOM(因此不使用 FromXML
)的项目可以忽略这些要求。
JException 的更多好处
- 一个基本构造函数接受可变数量的参数,以便以
printf
方式轻松格式化原因。例如,可以:
throw JException("Given value: %d is out of range. "
"Value should be between %d and %d", value, min, max);
GetByte()
方法可能会抛出异常,其原因为:“尝试获取字节时超时”。这是我们可以在低级别提供的全部信息,但在更高级别,捕获异常、提供更多上下文信息并重新抛出异常会很有用,如本例所示:try
{
b = socket.GetByte();
} catch (JException& e)
{
e.PrependToCause("Failed to read daily report "
"from transactions server %s due to: ", serverName);
throw e;
}
详细原因:“由于尝试获取字节时超时,未能从交易服务器 Server15 读取每日报告”,这比仅仅“尝试获取字节时超时”要全面得多。
JException
继承自 std::exception
,后者仅提供 what()
方法以 C 字符串而不是 std::string
获取原因。摘要
JException 维护调用堆栈跟踪和原因描述,这对于分析异常发生的环境很有用。它还允许异常跨越 DCOM 边界,DCOM 缺乏异常处理功能。
修订历史
- 2009 年 8 月 28 日
- 原始文章。