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
{
<SPAN s