项目架构开发:异常处理及日志

Posted 漂亮的猫

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了项目架构开发:异常处理及日志相关的知识,希望对你有一定的参考价值。

上一篇我们完善了多层开发的效率问题,传送门:项目架构开发:展现层(下)

 

这次我们完成架构的异常处理功能,异常处理一般都与日志分不开的,因为分析及定位问题需要一些详细信息;

稍微正规一点的公司,都会分开发、测试及生产环境。在本地及测试环境出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         }

测试未通过

看看数据库有没有记录到异常

 

看到没?有了这些日志信息,什么问题都无法遁形。。

 

以上是关于项目架构开发:异常处理及日志的主要内容,如果未能解决你的问题,请参考以下文章

转载Airbnb 的核心日志系统架构及主要系统模块的设计之道

Asp.NetCore依赖注入和管道方式的异常处理及日志记录

前后端分离异常统一处理

java.util.MissingResourceException: Can't find bundle for base name init, locale zh_CN问题的处理(代码片段

异常处理和日志

javaweb+spring 项目集成异常的处理