首页 > 代码库 > 项目架构开发:异常处理及日志
项目架构开发:异常处理及日志
上一篇我们完善了多层开发的效率问题,传送门:项目架构开发:展现层(下)
这次我们完成架构的异常处理功能,异常处理一般都与日志分不开的,因为分析及定位问题需要一些详细信息;
稍微正规一点的公司,都会分开发、测试及生产环境。在本地及测试环境出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.ControllerName12 </p>13 <p class="bg-danger text-danger">14 触发异常的操作方法:@Model.ActionName15 </p>16 <p class="bg-danger text-danger">17 错误信息:@Model.Exception.Message18 </p>19 <p class="bg-info text-info">20 页面路径:~/Views/Shared/Error.cshtml21 </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 FilterConfig2 {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.Now12 });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.Now22 });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 = list15 });16 17 Assert.AreEqual(true, flag);18 }
测试未通过
看看数据库有没有记录到异常
看到没?有了这些日志信息,什么问题都无法遁形。。
项目架构开发:异常处理及日志