任何系統都需要處理錯誤,本文介紹的異常公共操作類,用於對業務上的錯誤進行簡單支持。
對於剛剛接觸.Net的新手,碰到錯誤的時候,一般喜歡通過返回bool值的方式指示是否執行成功。
public bool 方法名() { //執行代碼,成功返回true,否則返回false
}
不過上面的方法有一個問題是,無法知道確切的錯誤原因,所以需要添加一個out參數來返回錯誤消息。
public bool 方法名( out string errorMessage ) { //執行代碼,成功返回true,否則返回false
}
由於out參數用起來很麻煩,所以有些人干脆直接返回字符串,用特殊字符代表成功,比如返回”ok”代表成功。
public string 方法名() { //執行代碼,成功返回"ok",否則返回錯誤消息
}
隨着經驗的不斷提升,很快就會意識到用方法返回錯誤不是太方便,主要問題是如果調用棧很長,比如方法A調用方法B,B又調用C,C調用D,現在D出了問題,需要返回到A就要層層返回。所以需要找到一種更高效的錯誤處理手段,這就是異常。
.Net提供了大量異常類來支持不同的錯誤類型,所有異常都派生自基類Exception。
剛用上異常的初學者,一般會直接拋出Exception,主要目標是獲取拋出的錯誤消息。
public void 方法名() { //執行代碼,如果發生錯誤就執行下面的代碼
throw new Exception("發生錯誤了,快處理"); }
還有些高標准的團隊,要求對業務上所有的錯誤創建自定義異常,以提供精確和清晰的異常處理方式。
上面兩種異常處理方式是兩個極端。
直接使用Exception的好處是省力,壞處是無法識別出究竟是系統異常還是業務上的錯誤,這有什么關系?需要分清系統異常和業務錯誤的原因是,你可能不想把系統內部的異常暴露給終端客戶,比如給客戶提示“未將對象引用設置到對象的實例”感覺如何,當然,這可能只是讓客戶摸不着頭腦,還不是很嚴重,有一些異常會暴露系統的弱點,從而導致更易受攻擊。
為每個業務錯誤創建一個自定義異常,好處是可以對異常精確定位,另外可以方便的為異常處理提供相關數據。這種方式的主要毛病是工作量很大,一般程序員都不會這么干。
現在來考慮我們一般是如何處理異常的?大部分時候,可能只是記錄了一個日志,然后將該異常轉換為客戶端能識別的消息,客戶端會把異常消息顯示出來。更進一步,可能會識別出系統異常,給客戶端提示一個默認消息,比如“系統忙,請稍后再試”,如果是業務錯誤,就直接顯示給客戶。
可以看到,只有在你真正需要進行特定異常處理的時候,創建業務錯誤對應的自定義異常才會有價值,如果你創建出來的自定義異常,僅僅記錄了個日志,那就沒有多大必要了。
現在的關鍵是你需要把系統異常和業務錯誤識別出來,以指示你是否應該把錯誤消息暴露給客戶。我們可以創建一個自定義異常來表示通用的業務錯誤,我使用Warning來表示這個異常,即業務警告。
單元測試WarningTest的代碼如下。
using System; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using Util.Logs; namespace Util.Tests { /// <summary>
/// 應用程序異常測試 /// </summary>
[TestClass] public class WarningTest { #region 常量
/// <summary>
/// 異常消息 /// </summary>
public const string Message = "A"; /// <summary>
/// 異常消息2 /// </summary>
public const string Message2 = "B"; /// <summary>
/// 異常消息3 /// </summary>
public const string Message3 = "C"; /// <summary>
/// 異常消息4 /// </summary>
public const string Message4 = "D"; #endregion
#region TestValidate_MessageIsNull(驗證消息為空)
/// <summary>
/// 驗證消息為空 /// </summary>
[TestMethod] public void TestValidate_MessageIsNull() { Warning warning = new Warning( null, "P1" ); Assert.AreEqual( string.Empty, warning.Message ); } #endregion
#region TestCode(設置錯誤碼)
/// <summary>
/// 設置錯誤碼 /// </summary>
[TestMethod] public void TestCode() { Warning warning = new Warning( Message, "P1" ); Assert.AreEqual( "P1", warning.Code ); } #endregion
#region TestLogLevel(測試日志級別)
/// <summary>
/// 測試日志級別 /// </summary>
[TestMethod] public void TestLogLevel() { Warning warning = new Warning( Message, "P1", LogLevel.Fatal ); Assert.AreEqual( LogLevel.Fatal, warning.Level ); } #endregion
#region TestMessage_OnlyMessage(僅設置消息)
/// <summary>
/// 僅設置消息 /// </summary>
[TestMethod] public void TestMessage_OnlyMessage() { Warning warning = new Warning( Message ); Assert.AreEqual( Message, warning.Message ); } #endregion
#region TestMessage_OnlyException(僅設置異常)
/// <summary>
/// 僅設置異常 /// </summary>
[TestMethod] public void TestMessage_OnlyException() { Warning warning = new Warning( GetException() ); Assert.AreEqual( Message, warning.Message ); } /// <summary>
/// 獲取異常 /// </summary>
private Exception GetException() { return new Exception( Message ); } #endregion
#region TestMessage_Message_Exception(設置錯誤消息和異常)
/// <summary>
/// 設置錯誤消息和異常 /// </summary>
[TestMethod] public void TestMessage_Message_Exception() { Warning warning = new Warning( Message2, "P1", LogLevel.Fatal, GetException() ); Assert.AreEqual( string.Format( "{0}\r\n{1}", Message2, Message ), warning.Message ); } #endregion
#region TestMessage_2LayerException(設置2層異常)
/// <summary>
/// 設置2層異常 /// </summary>
[TestMethod] public void TestMessage_2LayerException() { Warning warning = new Warning( Message3, "P1", LogLevel.Fatal, Get2LayerException() ); Assert.AreEqual( string.Format( "{0}\r\n{1}\r\n{2}", Message3, Message2, Message ), warning.Message ); } /// <summary>
/// 獲取2層異常 /// </summary>
private Exception Get2LayerException() { return new Exception( Message2, new Exception( Message ) ); } #endregion
#region TestMessage_Warning(設置Warning異常)
/// <summary>
/// 設置Warning異常 /// </summary>
[TestMethod] public void TestMessage_Warning() { Warning warning = new Warning( GetWarning() ); Assert.AreEqual( Message, warning.Message ); } /// <summary>
/// 獲取異常 /// </summary>
private Warning GetWarning() { return new Warning( Message ); } #endregion
#region TestMessage_2LayerWarning(設置2層Warning異常)
/// <summary>
/// 設置2層Warning異常 /// </summary>
[TestMethod] public void TestMessage_2LayerWarning() { Warning warning = new Warning( Message3, "", Get2LayerWarning() ); Assert.AreEqual( string.Format( "{0}\r\n{1}\r\n{2}", Message3, Message2, Message ), warning.Message ); } /// <summary>
/// 獲取2層異常 /// </summary>
private Warning Get2LayerWarning() { return new Warning( Message2, "", new Warning( Message ) ); } #endregion
#region TestMessage_3LayerWarning(設置3層Warning異常)
/// <summary>
/// 設置3層Warning異常 /// </summary>
[TestMethod] public void TestMessage_3LayerWarning() { Warning warning = new Warning( Message4, "", Get3LayerWarning() ); Assert.AreEqual( string.Format( "{0}\r\n{1}\r\n{2}\r\n{3}", Message4, Message3, Message2, Message ), warning.Message ); } /// <summary>
/// 獲取3層異常 /// </summary>
private Warning Get3LayerWarning() { return new Warning( Message3, "", new Exception( Message2, new Warning( Message ) ) ); } #endregion
#region 添加異常數據
/// <summary>
/// 添加異常數據 /// </summary>
[TestMethod] public void TestAdd_1Layer() { Warning warning = new Warning( Message ); warning.Data.Add( "key1", "value1" ); warning.Data.Add( "key2", "value2" ); StringBuilder expected = new StringBuilder(); expected.AppendLine( Message ); expected.AppendLine( "key1:value1" ); expected.AppendLine( "key2:value2" ); Assert.AreEqual( expected.ToString(), warning.Message ); } /// <summary>
/// 添加異常數據 /// </summary>
[TestMethod] public void TestAdd_2Layer() { Exception exception = new Exception( Message ); exception.Data.Add( "a", "a1" ); exception.Data.Add( "b", "b1" ); Warning warning = new Warning( exception ); warning.Data.Add( "key1", "value1" ); warning.Data.Add( "key2", "value2" ); StringBuilder expected = new StringBuilder(); expected.AppendLine( Message ); expected.AppendLine( "a:a1" ); expected.AppendLine( "b:b1" ); expected.AppendLine( "key1:value1" ); expected.AppendLine( "key2:value2" ); Assert.AreEqual( expected.ToString(), warning.Message ); } #endregion } }
Warning的代碼如下。
using System; using System.Collections; using System.Text; using Util.Logs; namespace Util { /// <summary>
/// 應用程序異常 /// </summary>
public class Warning : Exception { #region 構造方法
/// <summary>
/// 初始化應用程序異常 /// </summary>
/// <param name="message">錯誤消息</param>
public Warning( string message ) : this( message, "" ) { } /// <summary>
/// 初始化應用程序異常 /// </summary>
/// <param name="message">錯誤消息</param>
/// <param name="code">錯誤碼</param>
public Warning( string message, string code ) : this( message, code, LogLevel.Warning ) { } /// <summary>
/// 初始化應用程序異常 /// </summary>
/// <param name="message">錯誤消息</param>
/// <param name="code">錯誤碼</param>
/// <param name="level">日志級別</param>
public Warning( string message, string code, LogLevel level ) : this( message, code, level, null ) { } /// <summary>
/// 初始化應用程序異常 /// </summary>
/// <param name="exception">異常</param>
public Warning( Exception exception ) : this( "", "", LogLevel.Warning, exception ) { } /// <summary>
/// 初始化應用程序異常 /// </summary>
/// <param name="message">錯誤消息</param>
/// <param name="code">錯誤碼</param>
/// <param name="exception">異常</param>
public Warning( string message, string code, Exception exception ) : this( message, code, LogLevel.Warning, exception ) { } /// <summary>
/// 初始化應用程序異常 /// </summary>
/// <param name="message">錯誤消息</param>
/// <param name="code">錯誤碼</param>
/// <param name="level">日志級別</param>
/// <param name="exception">異常</param>
public Warning( string message, string code, LogLevel level, Exception exception ) : base( message ?? "", exception ) { Code = code; Level = level; _message = GetMessage(); } /// <summary>
/// 獲取錯誤消息 /// </summary>
private string GetMessage() { var result = new StringBuilder(); AppendSelfMessage( result ); AppendInnerMessage( result, InnerException ); return result.ToString().TrimEnd( Environment.NewLine.ToCharArray() ); } /// <summary>
/// 添加本身消息 /// </summary>
private void AppendSelfMessage( StringBuilder result ) { if ( string.IsNullOrWhiteSpace( base.Message ) ) return; result.AppendLine( base.Message ); } /// <summary>
/// 添加內部異常消息 /// </summary>
private void AppendInnerMessage( StringBuilder result, Exception exception ) { if ( exception == null ) return; if ( exception is Warning ) { result.AppendLine( exception.Message ); return; } result.AppendLine( exception.Message ); result.Append( GetData( exception ) ); AppendInnerMessage( result, exception.InnerException ); } /// <summary>
/// 獲取添加的額外數據 /// </summary>
private string GetData( Exception ex ) { var result = new StringBuilder(); foreach ( DictionaryEntry data in ex.Data ) result.AppendFormat( "{0}:{1}{2}", data.Key, data.Value, Environment.NewLine ); return result.ToString(); } #endregion
#region Message(錯誤消息)
/// <summary>
/// 錯誤消息 /// </summary>
private readonly string _message; /// <summary>
/// 錯誤消息 /// </summary>
public override string Message { get { if ( Data.Count == 0 ) return _message; return _message + Environment.NewLine + GetData( this ); } } #endregion
#region TraceId(跟蹤號)
/// <summary>
/// 跟蹤號 /// </summary>
public string TraceId { get; set; } #endregion
#region Code(錯誤碼)
/// <summary>
/// 錯誤碼 /// </summary>
public string Code { get; set; } #endregion
#region Level(日志級別)
/// <summary>
/// 日志級別 /// </summary>
public LogLevel Level { get; set; } #endregion
#region StackTrace(堆棧跟蹤)
/// <summary>
/// 堆棧跟蹤 /// </summary>
public override string StackTrace { get { if ( !string.IsNullOrWhiteSpace( base.StackTrace ) ) return base.StackTrace; if ( base.InnerException == null ) return string.Empty; return base.InnerException.StackTrace; } } #endregion } }
需要注意的是,除了給Warning添加了一些有用的屬性以外,還重寫了Message屬性。這是因為當一個異常被拋出以后,其它代碼可能會進行攔截,之后這些代碼會拋出自己的異常,並把之前的異常包裝在自己內部,所以你要訪問之前的異常,就需要通過遞歸的方式訪問InnerException,直到InnerException為null。所以大家會在后面看到Warning類不僅被用來充當業務異常,還是一個獲取異常全部消息的公共操作類。
最后,再補充一個重構小知識,觀察Warning的代碼,多個構造方法中,只有參數最多的方法實現了功能,其它構造方法挨個調用,這稱為鏈構造函數。這個手法對於重載方法都適用,不要在每個方法中重復實現代碼,把實現代碼放到參數最多的方法中,其它重載只是該方法提供了默認值的版本而已。
本文簡單介紹了在開發過程中與異常相關的內容,下一篇將回到領域實體,我將介紹如何以規約模式等方式對領域實體進行驗證。
.Net應用程序框架交流QQ群: 386092459,歡迎有興趣的朋友加入討論。
謝謝大家的持續關注,我的博客地址:http://www.cnblogs.com/xiadao521/
下載地址:http://files.cnblogs.com/xiadao521/Util.2014.11.19.1.rar