閱讀724 返回首頁    go 京東網上商城


如何解決EnterLib異常處理框架最大的局限——基於異常"類型"的異常處理策略

個人覺得EnterLib的EHAB(Exception Handling Application Block)是一個不錯的異常處理框架,借助於EHAB,我們可以配置的方式來自定義異常處理策略,從而帶來最大的靈活性和可維護性。但是,在我看來,EHAB有一個最大的局限,把就是異常處理策略的粒度過大——隻能提供基於異常類型級別。本篇文章通過一個自定義ExceptionHandler很好地解決了這個問題。

一、EnterLib基於異常類型的異常處理策略

EnterLib的異常處理策略基本上可以通過這樣的的公式來表示:,它表達的意思是:“對於某種的異常,應該采用哪些Exception Handler去處理,而被處理後的異常還需要采用怎樣的後續操作(將異常吃掉、或者是重新拋出)”。

也就是說,拋出類型的異常類型決定了最終采取的處理策略,這在大部分情況下是可以接受的。但是在很多場景中,,我們期望的行為是:盡管異常類型一樣,我們也可以。

一個最為典型的場景就是基於數據庫的數據存取,如果你采用的SQL Server,拋出的異常永遠隻有一種:SqlException。如果完全按照EnterLib EHAB的做法,在任何情況下拋出的SqlException對應的處理方式都是一樣的。但是拋出SqlException的情況非常多,比如Server連接斷開、認證失敗、數據庫對象不存在、違反一致性約束等等,如果異常處理框架能夠根據最終拋出的異常的具體屬性,“智能”地應用相應的策略去處理,這才是我們樂於看到的。

二、一個特殊的ExceptionHandler——FilterableHandler

為了解決這個問題,我創建了一個特殊的Exception Handler,我將它起名為FilterableHandler。說它特別,是因為。

實際上,我在很早之前就定義了一個相似的FilterableHandler,有興趣的話可以參考《創建一個自定義Exception Handler改變ELAB的異常處理機製》。由於在最新的EnterLib中,底層的實現機製發生了根本性的改變,這個ExceptionHandler已經不能在使用。所以我對其進行的了修正,同時根據可擴展性進行重新設計。

之所以稱這個ExceptionHandler為FilterableHandler,是在於它具有對拋出的異常具有“篩選”的功能。說得具體點,FilterableHandler將拋出的異常對象,傳入一組具有篩選功能的ExceptionHandler列表,我個人將這個列表命名為FiterableExceptionHandlerPipeline。FiterableExceptionHandlerPipeline對象包含一個篩選器和一組ExceptionHandler,如果傳入的異常通過了篩選器的篩選,該異常最終被分發到相應的ExceptionHandler列表中進行處理。FiterableExceptionHandlerPipeline大概的定義如下:

   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列表去處理拋出的異常,否則就直接忽略掉。

   1: public interface IFilter
   2: {
   3:     bool Match(Exception ex);
   4: }

你可以從下麵給出的關於FilterableHandler的完整的代碼去分析具體的異常處理實現原理。而實際上,最為複雜的不是FilterableHandler本身的實現,而是與之相關的配置元素的定義。由於這會涉及到很多關於EnterLib底層和Unity相關的知識點,不是三言兩語就能講明白的,所以在這裏就不對FilterableHandler的配置體係作介紹了,有興趣的話可以通過這裏直接下載源代碼。

   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異常處理相關的內容,你可以參閱我下麵三篇文章:



為了對SqlException進行針對處理,我們對拋出的SqlException進行封裝。對應於上述三種類型,我定義如如下三種異常:SqlSystemException、SqlBusinessException和DbException。

   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的處理策略,整個配置定義在如下的代碼片斷中。

   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列表進行才處理。

   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。

   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

   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:          });

以下的輸出和我們的預期完全一致:

   1: Artech.ExceptionHandling.Demo.SqlSystemException
   2: Artech.ExceptionHandling.Demo.SqlBusinessException
   3: Artech.ExceptionHandling.Demo.DbException

四、FiterableHandler的可擴展性

FilterableHandler的核心在於有一組具有不同篩選器的FiterableExceptionHandlerPipeline。我默認定義了兩個基於屬性比較的FiterableExceptionHandlerPipeline,即PropertyValueEquivalencePipeline和PropertyValueRangePipeline。實際上你可以通過繼承FiterableExceptionHandlerPipeline基類,實現你自定義的篩選方式。


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-27 14:04:58

  上一篇:go  IoC+AOP的簡單實現
  下一篇:go  當InternalsVisibleToAttribute特性遭遇"強簽名"