博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

谈谈异常处理策略

Posted on 2011-07-08 14:44  老刘很氓  阅读(2947)  评论(5编辑  收藏  举报
Hi, All
在这里对我知道的异常处理和大家分享一下,不足的地方请大家补充
 
一.异常发生的原因:
我们的程序往往有很多依赖,这些依赖是异常发生源,它们包括:
1.       外部系统(数据库,Remoting,WebService等)
2.       外部文件(配置文件,数据文件)
3.       他人写的类库,函数,.NET FRAMEWORK类库自身。
二.什么时候抓住异常
仅当以下一种或多种情况时,我们的代码才需要抓住异常
1.       记录异常(logging)
将异常记录到日志中,便于support人员查找错误原因。
2.       为这个异常添加相关信息(wrap exception)
加发生异常的环境信息记录,并产生新异常,交给调用本方法的代码负责处理。
3.       执行清理工作
比如关闭数据库连接,Dispose对象,Rollback,Compensate操作等。
4.       尝试从异常中恢复
比如程序可以取一个默认值让程序继续进行(上次举过实时显示设备温度的程序的例子,当程序请求设备,但设备有时会因为响应
超时导致程序异常,这是程序可以捕捉这个异常,以上次取得的温度结果显示给用户)
常见的例子还有数据库连接突然断掉,或者插入数据时主键冲突(有可能是输入错误,也有可能同一条数据被别的用户先插入到数据库了)。
5.       隐藏异常敏感信息,替换成友好信息给用户。
三.异常传播
如果一个方法不需要做二提到的5件事情,或者说是需要将怎样处理异常的决定权交给其他模块(比如数据库并发异常,怎么处理应该是业务逻辑的事),
这时我们就需要让这个异常传播出去,而不是Catch,更不能Catch住后什么事情都不做,直接让异常神秘的消失(这点是让人最痛苦的),   这样可以保持代码清洁明了。
传播异常的途径有三种:
1.       直接传播
忽略异常处理,让调用栈上的其他方法来处理。
2.       Catch并且rethrow
抓住异常,执行清理,或者其他一些当前方法中能做的处理(前面提到的日志之类),如果异常不能恢复,并且当前方法并不是消除异常的好地方,重新抛出异常。
3.       Catch 并且包装Exception,并且重新抛出。
有时我们为了隐藏内部实现,或者让异常类型更符合当前调用链的层次
(比如对于Biz层抛出异常时,名为BizProcessException就比具体的原始的SqlException要好理解。
再比如我们写了一个Web service供外部人员调用,我们不希望所有的异常信息暴露给外部调用人员,这时我们也需要构建一个新的Exception来告知用户,提示他可以怎样处理这个异常,是可以恢复的还是不可以恢复的等等)
但是我们同时为了保留原始的Exception便于更精确的处理它,我们可以使用Exception的InnerException来保留原来的Exception
需要提醒大家的是,新建异常按照编码规范最好继承与ApplicationException。
四.处理异常的指导意见:
1.       除非是第二点处提到的时候,其他时候不要处理异常
2.       在程序里的边界处处理异常(比如逻辑层的最上面一层,调用的服务处,UI等),并且屏蔽掉敏感信息,只保留对当前层次安全的信息。
3.       通过自定义异常的名字来表达异常类型,应该怎么处理,是否能恢复。
4.       不要一大段的try,接着就是catch(Exception ex),应根据代码中可能发生的异常(查阅MSDN或者服务调用文档)精确分类处理,为了避免遗漏,可以这样处理:
Try
{
 // exception here
 
}
Catch(ConnectionException ex)
{
      // handle connect error;
}
Catch(AccessDenyException ex)
{
      //handle access deny error
}
Catch(Exeption ex)
{
      //handle generic exception
}
 
5.       不要抓住异常后,什么处理都不做,直接抛出,例如:
Try
{
   // exception here
}
Catch(Exception ex)
{
   Throw ex;
}
这么做会让异常的堆栈信息是从Throw这行开始,而不是真正异常发生的地方,为将来查错带来不便。
 
6.       将异常提示给用户时,应该显示友好信息,并提供建议的做法,不应包括敏感信息如调用堆栈,服务器名等。
7.       为了support的方便,我们可以为提示给用户的异常信息安插一个异常处理ID,用户可以很方便的把这个ID拷贝下来并邮件发送给support人员,
Support人员根据这个ID去日志文件中查找这个ID相关联的异常记录,如图:
 
日志文件记录:
            
8.      引入异常处理框架和策略注入框架完善,简化异常处理。
比如:PolicyInjection.Create<TestPolicyInjection,ITestPolicyInjection>().GetData();
GetData()方法中并没有任何异常处理代码,但是,它却做了以下事情:
如果发生数据库连接异常,记录日志并抛出warp exception :DBConnectionException,
如过是主键冲突,记录日志并抛出wrap exception: DuplicateRecordException 
这是采用面向方面编程的一种策略实现,有兴趣的同学可以查看:http://msdn.microsoft.com/en-us/library/aa288717(VS.71).aspx
9.       对于一般情况不可能发生的事情的地方(但或许是外部系统的不正确,内部的配置不正确,环境的不正确等问题导致又有可能导致发生),用Debug.Assert处理
例,在XXXXX里面有一段代码逻辑是去取当前用户的NO,这个No是标识在机器名的第二位(此处无任何注释,我揣摩了很久)
但是我们调试时,仓库是自己插入的,曾经有段时间我把位置记错,导致代码执行到一个很远的地方时才报异常,我跟踪了半天,才发现是这个问题,
如果在取这个No这个代码里用断言提示的话,应该能让程序员很快知道问题所在:
        public static string GetWareHouseNumberInHostName(this string hostName)
        {
            Int16  parseNumber = 0;
            bool parseToNumerSucced = Int16.TryParse(hostName.Substring(1, 2), out parseNumber);
            if (!parseToNumerSucced)
            {
                parseToNumerSucced = Int16.TryParse(hostName.Substring(1, 1), out parseNumber);
               
            }
 
            System.Diagnostics.Debug.Assert(parseToNumerSucced,"host name doesen't contain warehouse number");
                                                
            return parseNumber.ToString("D2");
            
}
再比如,配置文件未正确配置,数据库某字段不能为空,但是建表时又未该字段加以限制,结果由于数据不正确导致程序异常这些情况,我们也可以在代码里先用断言确保一下正确性。

五.异常处理策略(High level Design):见下面两张图,截取自Enterprise Library 说明文档,大家可以有个直观的感受:

 

 六.列举一个实际项目的例子:

待续....