如何解决EnterLib异常处理框架最大的局限——基于异常"类型"的异常处理策略

2018-01-16 18:11:35 浏览数 (1)

个人觉得EnterLib的EHAB(Exception Handling Application Block)是一个不错的异常处理框架,借助于EHAB,我们可以配置的方式来自定义异常处理策略,从而带来最大的灵活性和可维护性。但是,在我看来,EHAB有一个最大的局限,把就是异常处理策略的粒度过大——只能提供基于异常类型级别。本篇文章通过一个自定义ExceptionHandler很好地解决了这个问题。

一、EnterLib基于异常类型的异常处理策略

EnterLib的异常处理策略基本上可以通过这样的的公式来表示:Exception Policy = Exception Type Exception Handlers Post Handling Action,它表达的意思是:“对于某种类型的异常,应该采用哪些Exception Handler去处理,而被处理后的异常还需要采用怎样的后续操作(将异常吃掉、或者是重新抛出)”。

也就是说,抛出类型的异常类型决定了最终采取的处理策略,这在大部分情况下是可以接受的。但是在很多场景中,不同情况下也可以抛出相同类型的异常,我们期望的行为是:尽管异常类型一样,我们也可以根据具体抛出的异常定义不同的异常处理策略。

一个最为典型的场景就是基于数据库的数据存取,如果你采用的SQL Server,抛出的异常永远只有一种:SqlException。如果完全按照EnterLib EHAB的做法,在任何情况下抛出的SqlException对应的处理方式都是一样的。但是抛出SqlException的情况非常多,比如Server连接断开、认证失败、数据库对象不存在、违反一致性约束等等,如果异常处理框架能够根据最终抛出的异常的具体属性,“智能”地应用相应的策略去处理,这才是我们乐于看到的。

二、一个特殊的ExceptionHandler——FilterableHandler

为了解决这个问题,我创建了一个特殊的Exception Handler,我将它起名为FilterableHandler。说它特别,是因为FilterableHandler并不从事具体的异常处理操作(比如异常封装、替换、日志等),而是为某个具体的异常类型重新定义了异常处理策略。

实际上,我在很早之前就定义了一个相似的FilterableHandler,有兴趣的话可以参考《创建一个自定义Exception Handler改变ELAB的异常处理机制》。由于在最新的EnterLib中,底层的实现机制发生了根本性的改变,这个ExceptionHandler已经不能在使用。所以我对其进行的了修正,同时根据可扩展性进行重新设计。

之所以称这个ExceptionHandler为FilterableHandler,是在于它具有对抛出的异常具有“筛选”的功能。说得具体点,FilterableHandler将抛出的异常对象,传入一组具有筛选功能的ExceptionHandler列表,我个人将这个列表命名为FiterableExceptionHandlerPipeline。FiterableExceptionHandlerPipeline对象包含一个筛选器和一组ExceptionHandler,如果传入的异常通过了筛选器的筛选,该异常最终被分发到相应的ExceptionHandler列表中进行处理。FiterableExceptionHandlerPipeline大概的定义如下:

代码语言:js复制
   1: public class FilterableHandlerPipeline
   2: {
   3:     public IFilter Filter { get; private set; }
   4:     public IEnumerable<IExceptionHandler> ExceptionHandlers { get; private set; }
   5:  
   6:     public FilterableHandlerPipeline(IFilter filter, IEnumerable<IExceptionHandler> exceptionHandlers)
   7:     {
   8:         Guard.ArgumentNotNull(filter, "filter");
   9:         Guard.ArgumentNotNull(exceptionHandlers, "exceptionHandlers");
  10:         this.Filter = filter;
  11:         this.ExceptionHandlers = exceptionHandlers;
  12:     }
  13: }

而IFilter接口在更为简单,仅仅具有如下一个唯一的Match方法。布尔类型的返回值表明是否和指定的异常相匹配,当返回值为True的时候,FiterableExceptionHandlerPipeline采用用自己的ExceptionHandler列表去处理抛出的异常,否则就直接忽略掉。

代码语言:js复制
   1: public interface IFilter
   2: {
   3:     bool Match(Exception ex);
   4: }

你可以从下面给出的关于FilterableHandler的完整的代码去分析具体的异常处理实现原理。而实际上,最为复杂的不是FilterableHandler本身的实现,而是与之相关的配置元素的定义。由于这会涉及到很多关于EnterLib底层和Unity相关的知识点,不是三言两语就能讲明白的,所以在这里就不对FilterableHandler的配置体系作介绍了,有兴趣的话可以通过这里直接下载源代码。

代码语言:js复制
   1: [ConfigurationElementType(typeof(FilterableHandlerData))]
   2: public class FilterableHandler:IExceptionHandler
   3: {
   4:     public IEnumerable<FilterableHandlerPipeline> FilterableHandlerPipelines { get; private set; }
   5:     public IEnumerable<IExceptionHandler> DefaultHandlers { get; private set; }
   6:  
   7:     public FilterableHandler(IEnumerable<FilterableHandlerPipeline> filterableHandlerPipelines, IEnumerable<IExceptionHandler> defaultHandlers)
   8:     {
   9:         Guard.ArgumentNotNull(defaultHandlers, "defaultHandlers");
  10:         filterableHandlerPipelines = filterableHandlerPipelines ?? new List<FilterableHandlerPipeline>();
  11:         this.FilterableHandlerPipelines = filterableHandlerPipelines;
  12:         this.DefaultHandlers = defaultHandlers;
  13:     }
  14:     public Exception HandleException(Exception exception, Guid handlingInstanceId)
  15:     {
  16:         Guard.ArgumentNotNull(exception,"exception");
  17:         var handlerPipeline = (from pipeline in this.FilterableHandlerPipelines
  18:                                where pipeline.Filter.Match(exception)
  19:                                select pipeline).FirstOrDefault();
  20:         if (null != handlerPipeline)
  21:         {
  22:             return ExecuteHandlerChain(exception, handlingInstanceId, handlerPipeline.ExceptionHandlers);
  23:         }
  24:         else
  25:         {
  26:             return ExecuteHandlerChain(exception, handlingInstanceId, DefaultHandlers);
  27:         }
  28:     }
  29:  
  30:     private static Exception ExecuteHandlerChain(Exception exception, Guid handlingInstanceId, IEnumerable<IExceptionHandler> handlers)
  31:     {
  32:         var lastHandlerName = String.Empty;
  33:         try
  34:         {
  35:             foreach (var handler in handlers)
  36:             {
  37:                 lastHandlerName = handler.GetType().Name;
  38:                 exception = handler.HandleException(exception, handlingInstanceId);
  39:             }
  40:         }
  41:         catch (Exception ex)
  42:         {
  43:             var errorMsg = string.Format("Unable to handle the exception: {0}", lastHandlerName);
  44:             throw new ExceptionHandlingException(errorMsg, ex);
  45:         }
  46:         return exception;
  47:     }
  48: }

三、通过FilterableHandler对SqlException进行针对性处理

我现在通过一个简单的例子来演示FilterableHandler如何使用(源代码从这里下载),我们使用的场景就是上面提到过的对SqlException的针对性处理。根据SqlException抛出的场景,本例将起分为三种类型:

  • 系统异常:基于SQL Server自身抛出的异常,我们将异常编号,即SqlException的Number小于50000的称为系统异常;
  • 业务异常:编程人员根在编写SQL脚本的时候,根据相应的业务逻辑,通过调用RAISERROR语句手工抛出的异常。在默认情况下这种异常的编号为50000;
  • 其他:任何编号高于50000的异常。

注:关于RAIERROR语句以及SQL Server异常处理相关的内容,你可以参阅我下面三篇文章:

谈谈基于SQL Server的Exception Handling - PART I 谈谈基于SQL Server 的Exception Handling - PART II 谈谈基于SQL Server 的Exception Handling - PART III

为了对SqlException进行针对处理,我们对抛出的SqlException进行封装。对应于上述三种类型,我定义如如下三种异常:SqlSystemException、SqlBusinessException和DbException。

代码语言:js复制
   1: namespace Artech.ExceptionHandling.Demo
   2: {
   3:     [Serializable]
   4:     public class SqlSystemException : Exception
   5:     {
   6:         public SqlSystemException() { }
   7:         public SqlSystemException(string message) : base(message) { }
   8:         public SqlSystemException(string message, Exception inner) : base(message, inner) { }
   9:         protected SqlSystemException(
  10:           System.Runtime.Serialization.SerializationInfo info,
  11:           System.Runtime.Serialization.StreamingContext context)
  12:             : base(info, context) { }
  13:     }
  14:  
  15:     [Serializable]
  16:     public class SqlBusinessException : Exception
  17:     {
  18:         public SqlBusinessException() { }
  19:         public SqlBusinessException(string message) : base(message) { }
  20:         public SqlBusinessException(string message, Exception inner) : base(message, inner) { }
  21:         protected SqlBusinessException(
  22:           System.Runtime.Serialization.SerializationInfo info,
  23:           System.Runtime.Serialization.StreamingContext context)
  24:             : base(info, context) { }
  25:     }
  26:  
  27:     [Serializable]
  28:     public class DbException : Exception
  29:     {
  30:         public DbException() { }
  31:         public DbException(string message) : base(message) { }
  32:         public DbException(string message, Exception inner) : base(message, inner) { }
  33:         protected DbException(
  34:           System.Runtime.Serialization.SerializationInfo info,
  35:           System.Runtime.Serialization.StreamingContext context)
  36:             : base(info, context) { }
  37:     }
  38: }

我们需要作的进行通过配置定义处理SqlException的处理策略,整个配置定义在如下的代码片断中。

代码语言:js复制
   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:    ... ...
   4:     <exceptionHandling>
   5:         <exceptionPolicies>
   6:             <add name="Policy">
   7:                 <exceptionTypes>
   8:                     <add name="All Exceptions" type="System.Data.SqlClient.SqlException, System.Data, Version=4.0.0.0, 
   9: Culture=neutral, PublicKeyToken=b77a5c561934e089"
  10:                         postHandlingAction="ThrowNewException">
  11:                         <exceptionHandlers>
  12:                           <add name="filterHandler" type="Artech.ExceptionHandling.FilterableHandler, Artech.ExceptionHandling.Lib">
  13:                             <default>
  14:                               <add name="Wrap Handler" type="Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.WrapHandler, 
  15: Microsoft.Practices.EnterpriseLibrary.ExceptionHandling"
  16:                                 exceptionMessage="Business Error..." wrapExceptionType="Artech.ExceptionHandling.Demo.DbException, Artech.ExceptionHandling.Demo" />
  17:                             </default>
  18:                             <filters>
  19:                               <add name="businessError" type="Artech.ExceptionHandling.PropertyValueEquivalencePipeline, Artech.ExceptionHandling.Lib" 
  20: property="Number" value="50000">
  21:                                 <exceptionHandlers>
  22:                                   <add name="Wrap Handler" type="Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.WrapHandler, 
  23: Microsoft.Practices.EnterpriseLibrary.ExceptionHandling"
  24:                                exceptionMessage="Busiess Error..." wrapExceptionType="Artech.ExceptionHandling.Demo.SqlBusinessException, Artech.ExceptionHandling.Demo" />
  25:                                 </exceptionHandlers>
  26:                               </add>
  27:                               <add name="systemError" type="Artech.ExceptionHandling.PropertyValueRangePipeline, Artech.ExceptionHandling.Lib" property="Number"  
  28: upperBound="50000" upperRangeBoundType="Exclusive">
  29:                                 <exceptionHandlers>
  30:                                   <add name="Wrap Handler" type="Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.WrapHandler, 
  31: Microsoft.Practices.EnterpriseLibrary.ExceptionHandling"
  32:                                exceptionMessage="System Error..." wrapExceptionType="Artech.ExceptionHandling.Demo.SqlSystemException, 
  33: Artech.ExceptionHandling.Demo" />
  34:                                 </exceptionHandlers>
  35:                               </add>
  36:                             </filters>
  37:                           </add>                            
  38:                         </exceptionHandlers>
  39:                     </add>
  40:                 </exceptionTypes>
  41:             </add>
  42:         </exceptionPolicies>
  43:     </exceptionHandling>
  44: </configuration>

虽然配置稍微复杂了一点,但是结构还算是很清楚的。我们将FilterableHandler作为处理SqlException的唯一的ExceptionHandler。而FilterableHandler整个配置包含如下两个部分<default>和<filters>。<filters>自然就是定义的一组筛选分支,而<default>则是定义了一个后备——如果抛出的异常满足所有的筛选分支,则通过定义在<default>中的ExceptionHandler列表进行才处理。

代码语言:js复制
   1: <add name="filterHandler" type="Artech.ExceptionHandling.FilterableHandler, Artech.ExceptionHandling.Lib">
   2:   <default>
   3:     <add name="Wrap Handler" type="Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.WrapHandler, Microsoft.Practices.EnterpriseLibrary.ExceptionHandling"
   4:       exceptionMessage="Business Error..." wrapExceptionType="Artech.ExceptionHandling.Demo.DbException, Artech.ExceptionHandling.Demo" />
   5:   </default>
   6:   <filters>
   7:     <add name="businessError" type="Artech.ExceptionHandling.PropertyValueEquivalencePipeline, Artech.ExceptionHandling.Lib" property="Number" value="50000">
   8:       <exceptionHandlers>
   9:         <add name="Wrap Handler" type="Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.WrapHandler, 
  10: Microsoft.Practices.EnterpriseLibrary.ExceptionHandling"
  11:      exceptionMessage="Bar" wrapExceptionType="Artech.ExceptionHandling.Demo.SqlBusinessException, Artech.ExceptionHandling.Demo" />
  12:       </exceptionHandlers>
  13:     </add>
  14:     <add name="systemError" type="Artech.ExceptionHandling.PropertyValueRangePipeline, Artech.ExceptionHandling.Lib" 
  15: property="Number"  upperBound="50000" upperRangeBoundType="Exclusive">
  16:       <exceptionHandlers>
  17:         <add name="Wrap Handler" type="Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.WrapHandler, 
  18: Microsoft.Practices.EnterpriseLibrary.ExceptionHandling"
  19:      exceptionMessage="System Error..." wrapExceptionType="Artech.ExceptionHandling.Demo.SqlSystemException, Artech.ExceptionHandling.Demo" />
  20:       </exceptionHandlers>
  21:     </add>
  22:   </filters>
  23: </add>   

在<filters>中定义了两个FilterableHandlerPipeline:PropertyValueEquivalencePipeline和PropertyValueRangePipeline。PropertyValueEquivalencePipeline的筛选器根据抛出异常的某个属性的值是否等于指定的值进行筛选,而PropertyValueRangePipeline的筛选器则根据抛出异常的某个属性值是否在指定的范围内进行筛选。在这里用作筛选的属性名名称为Number,PropertyValueRangePipeline指定的上限为5000,upperRangeBoundType为“Exclusive”表示包含此上限,并且没有指定下限,所以这里的筛选逻辑就Number<50000。而PropertyValueEquivalencePipeline通过value属性设置成50000,表明它需要筛选Number=50000的异常。

<filters>下的两个筛选元素,以及<default>节点下的ExceptionHandler列表包含一个EnterLib提供的WrapHandler,对抛出的异常进行封装,在这里我们指定了不同的封装异常类型:SqlBusinessException、SqlSystemException和DbException。

我们验证上面定义的异常处理策略,看看抛出的SqlException是否按照我们的预期进行了相应的封装,我现定义了如下一个辅助方法:HandleException。

代码语言:js复制
   1: private static void HandleException(Action task)
   2: {
   3:     try
   4:     {
   5:         try
   6:         {
   7:             task();
   8:         }
   9:         catch (SqlException ex)
  10:         {
  11:             if (ExceptionPolicy.HandleException(ex, "Policy"))
  12:             {
  13:                 throw;
  14:             }
  15:         }
  16:     }
  17:     catch (Exception ex)
  18:     {
  19:         Console.WriteLine(ex.GetType().FullName);
  20:     }
  21: }

现在我们分三种情况调用这个辅助方法:

1、创建一个数据库连接,但是指定一个错误的密码,当我们开启连接的时候,系统会自动抛出一个SqlException,这个异常应该被封装成SqlSystemException;

2、通过创建一个DbCommand,执行RAISERROR语句,并指定相应的出错信息、错误严重级别(Serveriry)和状态(State),这个异常应该被封装成SqlBusinessException(Number=50000);

3、通过创建一个DbCommand,执行RAISERROR语句,指定一个MessageId(通过调用系统存储过程sp_addmessage创建,该值会转换成SqlException的Number),这个异常应该被封装成DbException

代码语言:js复制
   1: HandleException(
   2:         () =>
   3:         {
   4:             var connstring = "Server=.; Database=TestDb; Uid=sa; Pwd=invalidPwd";
   5:             var conection = new SqlConnection(connstring);
   6:             conection.Open();
   7:         });
   8:  
   9: HandleException(
  10:          () =>
  11:          {
  12:              var connstring = "Server=.; Database=TestDb; Uid=sa; Pwd=password";
  13:              var conection = new SqlConnection(connstring);
  14:              var command = conection.CreateCommand();
  15:              command.CommandText = "RAISERROR ('The order record does not exist',16,1)";
  16:              conection.Open();
  17:              command.ExecuteNonQuery();
  18:          });
  19:  
  20: HandleException(
  21:          () =>
  22:          {
  23:              var connstring = "Server=.; Database=TestDb; Uid=sa; Pwd=password";
  24:              var conection = new SqlConnection(connstring);
  25:              var command = conection.CreateCommand();
  26:              command.CommandText = "RAISERROR (50001,16,1)";
  27:              conection.Open();
  28:              command.ExecuteNonQuery();
  29:          });

以下的输出和我们的预期完全一致:

代码语言:js复制
   1: Artech.ExceptionHandling.Demo.SqlSystemException
   2: Artech.ExceptionHandling.Demo.SqlBusinessException
   3: Artech.ExceptionHandling.Demo.DbException

四、FiterableHandler的可扩展性

FilterableHandler的核心在于有一组具有不同筛选器的FiterableExceptionHandlerPipeline。我默认定义了两个基于属性比较的FiterableExceptionHandlerPipeline,即PropertyValueEquivalencePipeline和PropertyValueRangePipeline。实际上你可以通过继承FiterableExceptionHandlerPipeline基类,实现你自定义的筛选方式。

0 人点赞