Exception Management Architecture Guide 异常管理框架指南1. 异常管理要架构一个结构良好、维护性高、富有弹性的应用系统就必须采用适当的异常管理策略。系统的异常管理必须包含以下功能:
l 探测异常
l 记录异常日志、发送信息
l 产生异常事件,使外部系统能够监测和作出判断
要架构一个结构良好、维护性高、富有弹性的应用系统就必须采用适当的异常管理策略。系统的异常管理必须包含以下功能
1.1 异常的层次结构异常通常由应用程序(用户程序等)或运行库(公共语言运行库和应用程序运行库) 引发的。Exception是所有异常类型的基类。当发生异常时,系统或当前正在执行的应用程序通过引发包含关于该错误的信息的异常来报告异常。异常发生后,将由该应用程序或默认异常处理程序进行处理。若干异常类都直接从Exception类继承,其中包括两种主要类型的异常类:
1. ApplicationException
用户定义的应用程序异常类型的基类。ApplicationException继承Exception,但是不提供扩展功能,必须开发ApplicationException的派生类,以实现自定义异常的功能。
2. SystemException
预定义的公共语言运行库异常类的基类。
这两个异常类构成了几乎所有的应用程序和运行库异常的基础。
1.1 异常处理过程异常处理过程主要由“分析异常”和“处理异常”组成。应用系统中的方法(过程)都必须按照下述流程以确保被处理的异常信息中包含当前方法(过程)的上下文。
注:在方法(过程)产生了异常,进入“异常探测”代码块。“异常探测”和“异常传播”代码块中论述了异常的细节。当异常被传播到应用系统的边界(需要将异常信息反馈给用户)时,异常管理进入异常处理阶段,见下图:
注:为了维持异常的信息(information),通告(nofice)和用户体验/用户提示(user experience),异常处理进入了信息收集、日志记录、通告异常过程。
1. 异常探测.Net的公共语言运行库提供一种异常处理模型,该模型基于对象形式的异常表示形式,将程序代码和异常处理代码分到try 块和 catch 块中。可以有一个或多个 catch块,每个块都设计为处理一种特定类型的异常,或者将一个块设计为捕捉比其他块更具体的异常。
try
{
// Some code that could throw an exception.
}
catch(SomeException exc)
{
// Code to react to the occurrence
// of the exception
}
finally
{
// Code that gets run always, whether or not
// an exception was thrown. This is usually
// clean up code that should be executed
// regardless of whether an exception has
// been thrown.
}
如果要处理在应用程序在执行期间某代码块发生的异常,则必须先该代码块放置在 try 块中。 (try 语句中的代码是try 块), 并将处理由try 块引发的异常的应用程序代码放在 catch语句中,称为catch块。零个或多个catch块与一个 try 块相关联,每个 catch块包含一个确定该块能够处理的异常类型的类型筛选器。不管异常产生与否,应用程序都将进入finally块,通常要要执行一些clean up代码。
注:在try 块中出现异常时,系统按所关联 catch块在应用程序代码中出现的顺序搜索它们,直到定位到处理该异常的catch块为止。如果某catch块的类型筛选器指定了异常类型 T或任何派生由异常类型T派生的异常类型,则该catch块处理 T类型及其派生类型的异常。系统在找到第一个处理该异常的catch 块后即停止搜索。因此,在应用程序代码中处理某类型异常的catch块必须在处理其基类型的catch块之前指定,所以通常处理System.Exception 的catch 块最后指定。如果与try块相关联的所有catch块均不处理该异常,且当前try 块嵌套在其他try 块中, 则搜索与上一级try块相关联的catch块。如果仍然没有找到用于该异常的catch 块,则将该异常沿调用堆栈向上传递,搜索上一个堆栈帧(当前方法的主调方法)来查找处理该异常的catch 块,并一直查找,直到该异常得到处理或调用堆栈中没有更多的帧为止。如果到达调用堆栈顶部却没有找到处理该异常的catch块,则由默认的异常处理程序处理该异常,然后应用程序终止。
当执行以下的特定操作时,应用程序需要捕获异常:
l 搜集信息并记录日志
l 为当前异常添加一些相关的信息
l 执行clean up代码
l 尝试将异常恢复(解决)
1.1 适当使用异常在应用程序设想以外出现的错误要使用异常,在应用程序设想内出现的错误一般不必使用异常”。
例如:某个用户登录应用系统,因为输入了错误的帐号或密码而无法登录,“登录失败”已经在系统的预料之中,在这种情况下,没有必要使用异常管理。但是一个未预料到的错误就要捕获异常,如用户登录失败是数据库连接失败造成的。
另外,抛出一个异常的资源开销要比返回一个简单结果给函数(过程)调用者大并且过度使用异常会产生难以阅读和管理的代码,所以对于可控制的执行流,不要使用异常管理。
1.2 运行时捕获异常在特定新的环境下,应用系统抛出的异常将在运行时截获,而这些异常会牵涉到堆栈资源。例如:调用一个ArrayList中的存放的Objects进行排序方法的代码里使用了Object的CompareTo方法,该方法抛出了System.InvalidOperationException异常,另外在调用“反射”方法时还可能产生System.Reflection.TargetInvocationException异常,可以把这些异常设置成InnerException(内部异常属性)在运行时抛出。必须处理好这些异常,使得它们对应用系统带来最小的影响。详细内容请点击以下链接地址:
l Base Exception Hierarchy
l Common Exception Classes
l System.Exception Class
2. 异常传播以下有三种途径来传播异常:
l 让异常自动传播
代码段可以故意忽略异常,当发生异常时,代码段停止执行,进入堆栈直至找到与当前异常符合的堆栈地址。
l 捕获抛出的异常
在代码段中捕获异常,然后在当前代码段中执行clean up或一些必要的过程代码。假如不能解决异常就将它抛给调用者。
l 捕获、包装、抛出已包装的异常
从堆栈中传播出来的异常,往往缺乏类型相关性。而经过包装的异常返回给调用者,将更加可读、更具相关性。下图解释了异常捕获、包装和抛出的过程。使用这种途径,应用程序可以捕获异常,然后执行clean up或一些必要的过程代码。假如异常无法解决,就重新包装异常,并抛给函数(过程)调用者。设置InnerException属性可以使异常源包装成“内部异常”和带有上下文相关性的“外部异常”的新异常。InnerException属性设置可以在构造异常时进行。
当异常传播时,catch代码段只能捕获“外部异常”,通过InnerException属性可以访问内部异常。下面的代码描述了实现过程:
try
{
// Some code that could throw an exception.
}
catch(TypeAException e)
{
// Code to do any processing needed.
// Rethrow the exception
throw;
}
catch(TypeBException e)
{
// Code to do any processing needed.
// Wrap the current exception in a more relevant
// outer exception and rethrow the new exception.
throw(new TypeCException(strMessage, e));
}
finally
{
// Code that gets executed regardless of whether
// an exception was thrown.
}
注:第一个catch代码段捕获了TypeAException异常,执行一些必要的过程代码,然后将本异常抛给调用者。第二个catch代码段将TypeBException包装成具有上下文相关性新异常TypeCException,并作为外部异常抛给调用者。在这个例子中代码块只关心异常TypeAException和TypeBException,其他异常将自动向上传播。
以下是异常三种传播途径的优缺点比较:
传播途径
是否允许处理
是否允许添加相关性
让异常自动传播
否
否
捕获抛出的异常
是
否
捕获、包装、抛出已包装的异常
是
是
1.1 何时使用内部异常 异常最初传播只提供异常产生的精确原因,当异常抛给调用者时,它带有很少量的上下文相关性,此时需要这行异常包装。
例如:调用LoadUserInfo方法需要读取服务器的本地文件,假如文件不存在,代码段将抛出FileNotFoundException异常给调用者。但是在LogonUser方法中出现“文件无法找到”的异常,显然很难读,假如将FileNotFoundException包装成自定义异常FailedToLoadUserInfoException,在FailedToLoadUserInfoException中加入外部信息和上下文相关性,这样将更加符合LogonUser方法。
2. 自定义异常.Net Framework是通过异常类型来辨认异常的,应用程序建立源于ApplicationException的层次结构的异常体系,如下图:
层次结构的自定义异常体系对应用系统带来以下好处:
l 易于开发,因为通过继承派生异常可以扩充自己的属性
l 当系统部署完毕时,仍然可以通过继承扩充新的自定义异常。无须更改已经写好的异常处理代码,因为扩充的异常派生于基类异常,对基异常的处理也就是对它派生的异常处理。
1.1 设计层次结构异常.Net Framework采用可扩展、层次结构的异常体系,假如合适的异常已经定义,就采用.Net Framework提供的异常。大部分应用程序的层次结构异常体系的组织需要平滑、分组,同时扩充新的属性和功能。
是否在应用系统中建立自定义异常,可以参照以下问题:
l 在当前条件下是否存在异常?
出现在.Net Framework下的异常不必自定义异常。
l 异常细节是否需要单独处理?
应该建议一个新的异常类,使得代码块能够捕获该异常,并进行明确地处理。这样排除了处理一些非特殊的异常,通过逻辑判断来决定执行某个操作。
l 是否要执行特殊的处理或在异常中加入信息?
可以新建一个“应用级的异常”类,根据特殊需要向异常中添加信息和功能。
通常将一个应用系统的异常层次结构存放在一个程序集中,这样应用程序可以添加引用,并且便于子定义异常类的管理与部署。
1.2 建立自定义异常类自定义异常类要有良好的命名习惯。以Exception结尾,并且要表达适当的意思,同时要提供以下三种构造函数的实现。
using System;
public class YourBaseApplicationException : ApplicationException
{
// Default constructor
public YourBaseApplicationException ()
{
}
// Constructor accepting a single string message
public YourBaseApplicationException (string message) : base(message)
{
}
// Constructor accepting a string message and an
// inner exception which will be wrapped by this
// custom exception class
public YourBaseApplicationException(string message,
Exception inner) : base(message, inner)
{
}
1.2.1 建立派生于ApplicationException的自定义异常所有的自定义异常都继承ApplicationException。一般在自定义异常中加入“异常出现时间”、“服务器名”、“运行帐号”等信息。将异常的细节压缩到一个基类异常中,通过继承提高自定义异常的可用性。
1.2.2 远程访问自定义异常异常类实现了ISerializable接口,通过序列化,异常将通过系统边界,被远程服务所访问。要实现远程访问,必须在类定义上加上[Serializable],同时要实现下面的构造函数:
protected YourBaseApplicationException(SerializationInfo info,
StreamingContext context) : base(info, context)
{
}
如果自定义异常中增加了一些字段属性,就必须要重写GetObjectData方法,并把相应的字段属性加载到SerializationInfo中,如下:
[Serializable]
public class ExampleException : ApplicationException
{
public ExampleException() : base()
{
}
public ExampleException(string message) : base(message)
{
}
public ExampleException(string message,Exception inner) :
base(message,inner)
{
}
protected ExampleException(SerializationInfo info, StreamingContext context) :
base(info,context)
{
m_strMachineName = info.GetString("m_strMachineName");
}
public override void GetObjectData( SerializationInfo info,
StreamingContext context )
{
info.AddValue("m_strMachineName", m_strMachineName,
typeof(String));
base.GetObjectData(info,context);
}
private string m_strMachineName = Environment.MachineName;
public string MachineName
{
get
{
return m_strMachineName;
}
set
{
m_strMachineName = value;
}
}
}
通过自定义异常类的构造函数,异常中相应的字段属性通过SerializationInfo对象传递到远程服务器,通过SerializationInfo的GetValue方法来获取相关信息。详细内容请点击以下链接地址:
l .NET Framework Developer's Guide: Best Practices for Handling Exceptions
l Handling and Throwing Exceptions
l Implementing ISerializable
l Serialization Sample
2. 管理未经处理的异常当一个未经处理的异常传播到应用程序和用户端的边界时,而系统无法解决异常。此时,为了管理异常与用户的通信,必须搜集信息、记录日志、发送通告、执行cleanup和必要的过程代码。在大多数基于Web的应用系统中,异常和终端用户的边界是靠Web页面或Web服务来控制的。下面将讨论在系统边界如何管理未经处理的异常。
2.1 ASP.NETASP.NET提供了一些特定的方法来管理异常和设置异常信息显示给终端客户的方式。
2.1.1 设置Web.config<customErrors defaultredirect="http://hostname/error.aspx" mode="on">
<error statuscode="500" redirect="/errorpages/servererror.aspx" />
<error statuscode="404" redirect="/errorpages/filenotfound.htm" />
</customErrors>
在customErrors中设置默认重定向页面,以下是三种设置模式:
l on
未经处理的异常将用户重定向到一个统一的默认页面。一般用于产品化模式。
l off
用户将会看到异常提示信息,而非重定向到统一默认页面。一般用于项目开发模式。
l remoteonly
当用户通过“localhost”访问本地服务器时将会看到异常提示信息,而其他用户将重定向到一个统一的默认页面。一般用于Debug模式。
除了重定向到默认页面以外,也可以对某些HTTP错误代码设置特定的页面。例如,将所有的404、500错误定向到特定页面。
在.NET之前要实现上述定向,必须要设置IIS中的matabase。而ASP.NET将所有的应用设置都集中到web.config中,通过xcopy就可以实现部署。
必须注意这些设置只对ASP.NET文件有效(aspx、asmx),例如用户调用应用服务器上以htm、asp等为后缀的文件时,IIS将返回matabase上的HTTP错误设置,而非web.config上的设置。此时,必须将matabase和web.config的重定向页面设置一致。
2.1.2 使用@ Page Directiveweb.config将作用于当前目录和所有子目录,通过重写Page标识中的ErrorPage属性来设置特定的重定向页面,在下面的例子中,终端用户如果碰到了未处理的异常,将会跳转到customerror.aspx页面:
<%@ Page ErrorPage="customerror.aspx" %>
2.1.3 处理ASP.NET的异常ASP.NET提供了两种事件来处理代码中抛出的异常。
l Page_Error
当页面级存在无法解决的异常时,该事件将被激活。此时在代码里必须包含Page_Error的事件句柄或者已经设置了@ Page Directive,Page_Error的事件句柄代码如下:
Page.Error += new System.EventHandler(Page_Error);
l Application_Error
Application_Error事件的代码块在global.asax中,属于应用级。当一些特殊页面中存在无法解决的异常时,该事件将被激活。应该在Application_Error事件的代码块中设计到“日志记录”、“通告”、“必要的处理代码”,如下:
// In global.asax file
protected void Application_Error(Object sender, EventArgs e)
{
Exception exc = Server.GetLastError();
// Perform logging, send any notifications, etc.
}
注:Server.GetLastError方法获得当前未处理的异常对象,Server.ClearError方法清除当前未处理的异常对象并停止异常的传播。例如,应用程序的页面中Page_Error事件中应该包含Server.GetLastError方法来访问当前未处理的异常对象,假如此时不用Server.ClearError方法清除异常,那么异常将传播到Application_Error事件中,假如不清除,将跳转到Web.config中设置的异常跳转页面。
详细内容请点击以下链接地址:
l ASP.NET Homepage
l <customerrors> Section
1.1 Web服务Web服务用Simple Object Access Protocol (SOAP)来传播异常有很很多局限性,.NET Framework提供了SoapException类来处理异常。当CLR收到来自client的错误格式的SOAP请求时,自动抛出SoapException异常。
Web服务方法抛出的异常将被统一包装成SoapException异常,通过SOAP响应抛到Web服务的client,
2. 搜集信息应用系统必须捕获适当的异常信息,这些信息用来明确地描述异常环境。要做的就是将它们转化成容易接受的信息,而接受信息的角色包括以下几种:
l 终端用户
需要获得有含义的、友好的信息
l 应用系统开发者
需要了解更多关于异常的细节,为解决问题提供援助
l 操作员
需要获得解决问题的相关信息和操作步骤
2.1 捕获适当的信息所有的角色都要收到与他们相关的信息,允许他们适当地处理问题。不要把所有的信息都抛给以上角色。例如:终端用户没有必要知道是那个方法(过程)的哪一行产生了何种异常。同样地,用户也非常讨厌看到“出现错误,请与管理员联系”等提示信息,显示给用户的异常信息应该是友好的,并且可以尝试指导用户纠正错误。下表显示了角色对异常信息的关注角度:
角色
关注信息
终端用户
1) 给出一个提示给用户,告知请求是否成功
2) 给出友好的消息,告知出现什么问题
3) 告知如何尝试纠正错误
应用系统开发者
1) 异常出现的时间
2) 精确地定位到异常出现的代码块
3) 明确出现的异常类
4) 与异常关联的信息、系统状态等
操作员
1) 异常出现时间
2) 精确地定位到异常出现的代码块
3) 需要发给通报的资源名和通报信息
4) 在异常类型中告知是否可以通过某些操作或开发来解决问题
应用程序需要提供丰富的异常信息以确保可以将其裁减给各个角色,捕获到的异常要包含下表信息:
数据
来源
Date and time of exception
DateTime.Now
Machine name
Environment.MachineName
Exception source
Exception.Source
Exception type
Type.FullName obtained from Object.GetType
Exception message
Exception.Message
Exception stack trace
Exception.StackTrace—this trace starts at the point the exception is thrown and is populated as it propagates up the call stack.
Call stack
Environment.StackTrace—the complete call stack.
Application domain name
AppDomain.FriendlyName
Assembly name
AssemblyName.FullName, in the System.Reflection namespace
Assembly version
Included in the AssemblyName.FullName
Thread ID
AppDomain.GetCurrentThreadId
Thread user
Thread.CurrentPrincipal in the System.Threading namespace
2.2 访问异常数据异常有各种各样的结构,通过捕获暴露出来的属性,系统得到产生异常的原因等信息。利用反射(详见System.Reflection名称空间),系统很容易地获得异常属性值,详细的异常信息引导开发人员找到异常源。详细内容请点击以下链接地址:
l Reflecting on Stack Traces
l System.Reflection Namespace
3. 应用仪表仪表嵌入程序,为特定的人员监控应用程序中提供数据。应用仪表是掌握系统执行状况和评估系统的好工具。企业仪表框架(EIF)提供弹性良好的方法来处理应用程序事件(包括异常),EIF利用event sources和event sinks将系统日志从通告发布中分离出来。但是在处理异常时我们往往需要提出日志(logging)和通告(notification)方案。
第一次发表翻译文章,有问题请大家多多包涵。原英文文章地址:http://msdn.microsoft.com/library/en-us/dnbda/html/exceptdotnet.asp?frame=true