任何系統都須要處理錯誤,本文介紹的異常公共操做類,用於對業務上的錯誤進行簡單支持。程序員
對於剛剛接觸.Net的新手,碰到錯誤的時候,通常喜歡經過返回bool值的方式指示是否執行成功。框架
public bool 方法名() { //執行代碼,成功返回true,不然返回false
}
不過上面的方法有一個問題是,沒法知道確切的錯誤緣由,因此須要添加一個out參數來返回錯誤消息。ide
public bool 方法名( out string errorMessage ) { //執行代碼,成功返回true,不然返回false
}
因爲out參數用起來很麻煩,因此有些人乾脆直接返回字符串,用特殊字符表明成功,好比返回」ok」表明成功。函數
public string 方法名() { //執行代碼,成功返回"ok",不然返回錯誤消息
}
隨着經驗的不斷提高,很快就會意識到用方法返回錯誤不是太方便,主要問題是若是調用棧很長,好比方法A調用方法B,B又調用C,C調用D,如今D出了問題,須要返回到A就要層層返回。因此須要找到一種更高效的錯誤處理手段,這就是異常。單元測試
.Net提供了大量異常類來支持不一樣的錯誤類型,全部異常都派生自基類Exception。測試
剛用上異常的初學者,通常會直接拋出Exception,主要目標是獲取拋出的錯誤消息。ui
public void 方法名() { //執行代碼,若是發生錯誤就執行下面的代碼
throw new Exception("發生錯誤了,快處理"); }
還有些高標準的團隊,要求對業務上全部的錯誤建立自定義異常,以提供精確和清晰的異常處理方式。this
上面兩種異常處理方式是兩個極端。spa
直接使用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