稍微正規一點的公司,都會分開發、測試及生產環境。在本地及測試環境出BUG了,問題很好解決
調試跟蹤問題,三下五除二就搞完了;但是在生產環境出問題,基本上是不允許直連數據庫調試的;
這時候如何沒有足夠的異常信息參考,那你就悲催了,你等着加班熬夜吧。
爲了解決這個問題,所以異常信息的捕捉及記錄就顯得非常重要了,一個完善的系統,出問題後不可能要去調試才能知道具體原因的
1、解決展現層的異常
1.1 其實ASP.NET MVC已經支持全局異常的處理,就是這個:HandleErrorAttribute,這裏我們只是簡單介紹他的使用方法
詳情可以看看這篇文章:http://shiyousan.com/post/635838881238204198,下面我們一步步來。
FilterConfig.cs,這是系統默認生成的
1 using System.Web; 2 using System.Web.Mvc; 3 4 namespace Presentation.MVC 5 { 6 public class FilterConfig 7 { 8 public static void RegisterGlobalFilters(GlobalFilterCollection filters) 9 { 10 filters.Add(new HandleErrorAttribute()); 11 } 12 } 13 }
1.2 要在Web.config中開啓customErrors,不然沒有效果
1 <customErrors mode="On" defaultRedirect="~/Error/Index"> 2 </customErrors>
1.3 設置好後,系統發生異常後會自動跳轉到默認的Error.cshtml界面
1 <div class="container"> 2 <h1 class="text-danger">錯誤。</h1> 3 <h2 class="text-danger">處理你的請求時出錯。</h2> 4 5 @if (Model != null) 6 { 7 <p class="bg-danger text-danger"> 8 異常類型:@Model.Exception.GetType().Name 9 </p> 10 <p class="bg-danger text-danger"> 11 觸發異常的控制器:@Model.ControllerName 12 </p> 13 <p class="bg-danger text-danger"> 14 觸發異常的操作方法:@Model.ActionName 15 </p> 16 <p class="bg-danger text-danger"> 17 錯誤信息:@Model.Exception.Message 18 </p> 19 <p class="bg-info text-info"> 20 頁面路徑:~/Views/Shared/Error.cshtml 21 </p> 22 } 23 </div>
1.4 我們再Home/Index初始頁觸發一個異常試試看
1 public ActionResult Login() 2 { 3 string str = null; 4 str.GetType();//空引用 5 6 return View(); 7 }
可以看到已經跳轉到默認錯誤顯示頁面了
但是這樣是不夠的,一般這個頁面會美化,客戶端用戶會看到更加友好的提示信息
而且這裏並沒有異常堆棧,看不到異常的具體信息,這樣定位問題就困難;
所以還需要加工一下,我們設計一個自定義的異常處理類
2、自定義異常處理
2.1 LjrExecptionAttribute.cs,很簡單啊,就不解釋了
1 using Infrastructure.Core; 2 using System.Web.Mvc; 3 4 namespace Presentation.MVC 5 { 6 public class LjrExecptionAttribute : HandleErrorAttribute 7 { 8 public override void OnException(ExceptionContext filterContext) 9 { 10 Logger.Error(filterContext.Exception.Message, filterContext.Exception); 11 12 base.OnException(filterContext); 13 } 14 } 15 }
2.2 然後FilterConfig.cs 要改一下
1 public class FilterConfig 2 { 3 public static void RegisterGlobalFilters(GlobalFilterCollection filters) 4 { 5 //filters.Add(new HandleErrorAttribute()); 6 filters.Add(new LjrExecptionAttribute()); 7 } 8 }
2.3 web.config也改一下,因爲HandleErrorAttribute 處理不了HTTP404
1 <customErrors mode="On" defaultRedirect="~/Error/Index"> 2 <error redirect="~/Error/NotFound" statusCode="404" /> 3 </customErrors>
2.4 新建ErrorController.cs
1 public class ErrorController : Controller 2 { 3 public ActionResult Index() 4 { 5 return View(); 6 } 7 8 public ActionResult NotFound() 9 { 10 return View(); 11 } 12 }
2.5 NotFound.cshtml
1 @{ 2 Layout = null; 3 } 4 5 <div style=" margin:0px auto; width:500px; margin:20px;"> 6 <h2>NotFound</h2> 7 一般人看不出來,這是一個美化了的NotFound頁面。 8 </div>
2.6 Error.cshtml改得更友好一些
1 <div style=" margin:0px auto; width:500px; margin:20px;"> 2 <h2>默認異常頁面</h2> 3 你好,這是系統默認異常界面,已經美化過了,請放心使用。 4 </div>
2.7 在Home控制器中手動觸發異常
1 public ActionResult ThrowHttp500() 2 { 3 throw new HttpException(500, "服務器錯誤"); 4 } 5 6 public ActionResult ThrowHttp404() 7 { 8 throw new HttpException(404, "頁面未找到"); 9 } 10 11 [HandleError(ExceptionType = typeof(NullReferenceException))] 12 public ActionResult ThrowNullReferenceException() 13 { 14 throw new NullReferenceException(); 15 } 16 17 public ActionResult ThrowFormatException() 18 { 19 string str = ""; 20 int count = Convert.ToInt32(str); 21 return View("Index"); 22 }
2.8 運行以下幾種異常會跳到之前的默認異常頁面
http://localhost:5572/Home/ThrowHttp500
http://localhost:5572/Home/ThrowNullReferenceException
http://localhost:5572/Home/ThrowFormatException
2.9 頁面未找到會轉至:http://localhost:5572/Home/ThrowHttp404
2.10 當然了,別忘了在2.2 中我們還在中記錄了日誌功能(LjrExecptionAttribute),我們去看看
堆棧信息都有了,這就很好定位BUG位置了,通過分析其日誌,大概可以知道問題原因
上邊已經解決了WEB中的異常信息,但是一個項目不可能只有WEB,還有很多類庫,WebService等
3、類庫異常處理
3.1 看看捕捉異常的一般做法
1 public bool CommonMethod(LoginUserCURequest entity) 2 { 3 try 4 { 5 this.repository.Add(new LoginUser() 6 { 7 Id = entity.Id, 8 LoginName = entity.LoginName, 9 Password = entity.Password, 10 IsEnabled = entity.IsEnabled, 11 CreateTime = DateTime.Now 12 }); 13 14 foreach (var Id in entity.Roles) 15 { 16 this.roleUserMappingRepository.Add(new RoleUserMapping() 17 { 18 Id = Guid.NewGuid(), 19 RoleId = Id, 20 LoginUserId = entity.Id, 21 CreateTime = DateTime.Now 22 }); 23 } 24 25 this.unitOfWork.Commit(); 26 27 return true; 28 } 29 catch (Exception ex) 30 { 31 Logger.Error(ex.Message, ex); 32 return false; 33 } 34 }
大部分人應該都是像上邊一樣處理異常,這本身沒有問題
但是仔細想想,還是有點問題的,我們來看看
1、每個方法都要寫try{}catch{},到最後整個類庫都是try catch,這很醜。。,顏值太低了,就提不起多少興趣了。
2、處理異常這本身就不應該屬於業務邏輯的一部分,得把他弄走,因爲他污染了業務邏輯
3、只記錄了異常堆棧,輸入參數沒有,如果要記錄參數值,還得寫一堆日誌代碼,那就更醜了。
3.2 有困難,攔截器來幫忙;對於這種現象,於是一些聰明的開發者就搞出了AOP編程
AOP是基於特性(Attribute)的,不過自己搞的話貌似還挺複雜的,簡單的還行,複雜的我也不會
於是我就盯上了PostSharp,版本1.5以上是收費的,這點要注意。我們先來搞一個簡單的異常攔截器
1 [Serializable] 2 [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] 3 public class ExceptionAttribute : PostSharp.Aspects.OnExceptionAspect 4 { 5 public override void OnException(MethodExecutionArgs args) 6 { 7 StringBuilder sb = new StringBuilder(); 8 sb.AppendLine(args.Exception.Message); 9 sb.AppendFormat("位置:{0}.{1}", args.Method.ReflectedType, args.Method).AppendLine(); 10 11 sb.AppendLine("參數:"); 12 foreach(var item in args.Arguments) 13 { 14 sb.AppendFormat(item.ToString()).AppendLine(); 15 } 16 17 Logger.Error(sb.ToString(), args.Exception); 18 19 args.FlowBehavior = FlowBehavior.ThrowException; 20 }
21 }
標紅的得注意了,必須要寫清楚的,不然沒有效果
還有一個小坑就是PostSharp不支持實體參數的,傳普通類型的string、int之類的在OnException中是可以拿到值的
如果是實體,比如這種
1 public class LoginUserCURequest 2 { 3 /// <summary>Id</summary> 4 public Guid Id { get; set; } 5 6 /// <summary>登錄賬戶名</summary> 7 public string LoginName { get; set; } 8 9 /// <summary>登錄密碼</summary> 10 public string Password { get; set; } 11 12 /// <summary>是否有效</summary> 13 public short? IsEnabled { get; set; } 14 15 /// <summary>所屬角色</summary> 16 public IEnumerable<Guid> Roles { get; set; } 17 }
這種是取不到值的(橙色部分),因爲args.Arguments返回的是object數組,取不到實體屬性
這是很不好的一點,不過可以克服的,我的方法就是重寫ToString(),如下
1 public class LoginUserCURequest 2 { 3 /// <summary>Id</summary> 4 public Guid Id { get; set; } 5 6 /// <summary>登錄賬戶名</summary> 7 public string LoginName { get; set; } 8 9 /// <summary>登錄密碼</summary> 10 public string Password { get; set; } 11 12 /// <summary>是否有效</summary> 13 public short? IsEnabled { get; set; } 14 15 /// <summary>所屬角色</summary> 16 public IEnumerable<Guid> Roles { get; set; } 17 18 public override string ToString() 19 { 20 return string.Format("Id:{0},LoginName:{1},Password:{2},IsEnabled:{3}", 21 Id.ToString(), LoginName, Password, IsEnabled.ToString()); 22 } 23 }
這樣就沒問題了,還能按照自己的格式輸出;
3.3 使用異常攔截器
攔截器我們已經搞好了,直接寫在方法或類名上邊就可以,如下圖
然後我們再Add方法裏邊觸發一個異常
我們測試一下,還是之前的LoginUserApplicationTest.cs,這裏再貼一次
1 [TestMethod] 2 public void Add() 3 { 4 var list = new List<Guid>(); 5 list.Add(Guid.NewGuid()); 6 list.Add(Guid.NewGuid()); 7 8 var flag = this.loginUserApplication.Add(new LoginUserCURequest() 9 { 10 Id = Guid.NewGuid(), 11 LoginName = "lanxiaoke-" + Guid.NewGuid().ToString(), 12 Password = "123456", 13 IsEnabled = 1, 14 Roles = list 15 }); 16 17 Assert.AreEqual(true, flag); 18 }
測試未通過
看看數據庫有沒有記錄到異常
看到沒?有了這些日誌信息,什麼問題都無法遁形。。