MVC过滤器使用案例:统一处理异常顺道精简代码
重构的乐趣在于精简代码,模块化设计,解耦功能……而对异常处理的重构则刚好满足上述三个方面,下面是我的一点小心得。
在文章《精简自己20%的代码》中,讨论了异常的统一处理,并通过对异常处理的封装达到精简代码的目的。具体有两种处理方法:
- 方法1:封装一个包含try{}catch{}finally{}的异常处理逻辑的方法类,将别的方法作为参数传入该方法,在页面内调用封装后的方法,大体代码如下:
- public class Process
- {
- public static bool Execute(Action action)
- {
- try
- {
- action.Invoke();
- return true;
- }
- catch (Exception ex)
- {
- return false;
- }
- finally
- {
- }
- }
- }
- public class Process
- 方法2:利用Attribute,为方法加上特性进行拦截式的统一处理(文章内貌似没有详细说明)
先简单回顾一下以前的知识:
以标准的三层结构来说,MVC对应表现层,其中Controller中的Action负责收集数据并把数据传递给业务逻辑层的功能;逻辑层通过调用数据访问层,获取数据并进行相应的业务逻辑处理,最终将处理后的数据返回至Model。最后将Model与View结合形成展现给终端用户浏览的页面。如下图:
按照方法1的思路,在Action里调用逻辑层方法的时候,用封装好的异常处理方法来包装一下,代码如下:
- public ActionResult Index(int id)
- {
- Boolean result = false; //接收返回的值
- Process.Execute(() => result = Save(id)); //执行方法
- return View();
- }
还可以利用Lambda我们可以包装更为复杂的逻辑,且不一定需要返回bool值:
- public ActionResult Index()
- {
- Process.Execute(()=>
- {
- ... //更为复杂的逻辑
- });
- return View();
- }
这样会在Action里包含大量的Process.Execute方法的调用,如果移至逻辑层里进行封装,则在逻辑层里会产生大量重复的代码,还是不够精简。而且这段代码不但手动重复书写,也没有和逻辑层的功能解耦,没有实现模块化。因此还需要继续重构。
从方法2使用Attribute的思路很容易就能想到MVC的过滤器,利用过滤器的拦截功能能很好的按照AOP思想实现异常处理,并解耦于逻辑层的模块。关于MVC过滤器的介绍,网上的文章很多,推荐《MVC过滤器详解》。这里要着重说一下过滤器的执行顺序。
- 一般的过滤器执行顺序
- IAuthorizationFilter->OnAuthorization(授权)
- IActionFilter ->OnActionExecuting(行为)
- Action
- IActionFilter ->OnActionExecuted(行为)
- IResultFilter ->OnResultExecuting(结果)
- View
- IResultFilter ->OnResultExecuted(结果)
- *IExceptionFilter ->OnException(异常),此方法并不在以上的顺序执行中,有异常发生时即会执行,有点类似于中断
- 当同时在Controller和Action中都设置了过滤器后,执行顺序一般是由外到里,即“全局”->“控制器”->“行为”
- Controller->IAuthorizationFilter->OnAuthorization
- Action ->IAuthorizationFilter->OnAuthorization
- Controller->IActionFilter ->OnActionExecuting
- Action ->IActionFilter ->OnActionExecuting
- Action
- Action ->IActionFilter ->OnActionExecuted
- Controller->IActionFilter ->OnActionExecuted
- Controller->IResultFilter ->OnResultExecuting
- Action ->IResultFilter ->OnActionExecuting
- Action ->IResultFilter ->OnActionExecuted
- Controller->IResultFilter ->OnActionExecuted
- 因为异常是从里往外抛,因次异常的处理顺序则刚好相反,一般是由里到外,即“行为”->“控制器”->“全局”
- Action ->IExceptionFilter->OnException
- Controller->IExceptionFilter->OnException
我们习惯使用的过滤器,要么是为Action加上Attribute,要么就是为Controller加上Attribute。上面所说的全局过滤器是怎么回事呢?先看看Gloabal里的代码:
- protected void Application_Start()
- {
- //注册Area
- AreaRegistration.RegisterAllAreas();
- //注册过滤器
- RegisterGlobalFilters(GlobalFilters.Filters);
- //注册路由
- RegisterRoutes(RouteTable.Routes);
- }
- public static void RegisterGlobalFilters(GlobalFilterCollection filters)
- {
- filters.Add(new HandleErrorAttribute());
- }
由上可知,在应用程序启动的时候就已经注册了全局过滤器,HandleErrorAttribute就是系统自带的异常过滤器。在这注册的全局过滤器,可以不用到每个Controller或者是每个Action去声明,直接作用于全局了,即可以捕捉整个站点的所有异常。看看它的源码是怎么处理异常的:
- public virtual void OnException(ExceptionContext filterContext)
- {
- if (filterContext == null)
- {
- throw new ArgumentNullException("filterContext");
- }
- if (!filterContext.IsChildAction && (!filterContext.ExceptionHandled && filterContext.HttpContext.IsCustomErrorEnabled))
- {
- Exception innerException = filterContext.Exception;
- if ((new HttpException(null, innerException).GetHttpCode() == ) && this.ExceptionType.IsInstanceOfType(innerException))
- {
- string controllerName = (string) filterContext.RouteData.Values["controller"];
- string actionName = (string) filterContext.RouteData.Values["action"];
- HandleErrorInfo model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
- ViewResult result = new ViewResult {
- ViewName = this.View,
- MasterName = this.Master,
- ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
- TempData = filterContext.Controller.TempData
- };
- filterContext.Result = result;
- filterContext.ExceptionHandled = true;
- filterContext.HttpContext.Response.Clear();
- filterContext.HttpContext.Response.StatusCode = ;
- filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
- }
- }
- }
HandleErrorAttribute的异常处理逻辑里,生成了一个HandleErrorInfo类的Model,并设置返回的结果为一个新生成的ViewResult。这个视图默认的ViewName是Error,对应于Share文件夹里的Error视图。而自带的Error视图没有用到HandleErrorInfo的Model,因此公开的信息也不是很多,可以根据具体的需求改造一下。例如:
- @model HandleErrorInfo
- <br />
- <div class="container">
- <div class="alert alert-error">
- <h4>
- Exception:</h4>
- <br />
- <p>
- There was a <b>@Model.Exception.GetType().Name</b> while rendering <b>@Model.ControllerName</b>'s <b>@Model.ActionName</b> action.</p>
- <p>
- @Model.Exception.Message
- </p>
- </div>
- <div class="alert">
- <h4>
- Stack trace:</h4>
- <br />
- <pre>@Model.Exception.StackTrace</pre>
- </div>
- </div>
这个过滤器要能起效,还需要在配置文件中配置一下:
- <customErrors mode="On" />
在实现异常的统一处理之前,先来明确一下需求:
- 站点所有页面在异常发生后,均需要记录异常日志,并转向错误提示页面(异常内容的详略程度由具体需求决定)
- 所有返回JSON数据的异步请求,不但需要记录异常日志,而且需要向客户端返回JSON格式的错误信息提示,而不是转向错误提示页面(异步请求也不可能转向错误提示页面)
- 采用AOP思想,将异常处理解耦
- 尽量精简声明Attribute的重复代码
实现1和3:
因为整个站点均需要记录异常日志,因此需要设计一个异常日志记录的过滤器(LogExceptionAttribute)进行拦截处理,这样既体现了AOP思想又满足了解耦的需要。代码如下:
- [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
- public class LogExceptionAttribute : HandleErrorAttribute
- {
- public override void OnException(ExceptionContext filterContext)
- {
- if (!filterContext.ExceptionHandled)
- {
- string controllerName = (string)filterContext.RouteData.Values["controller"];
- string actionName = (string)filterContext.RouteData.Values["action"];
- string msgTemplate = "在执行 controller[{0}] 的 action[{1}] 时产生异常";
- LogManager.GetLogger("LogExceptionAttribute").Error(string.Format(msgTemplate, controllerName, actionName), filterContext.Exception);
- }
- base.OnException(filterContext);
- }
- }
LogExceptionAttribute继承了HandleErrorAttribute,重写的OnException方法在记录异常日志后,通过调用base.OnException回到了系统默认的异常处理上,实现了向错误页面的跳转。
LogExceptionAttribute设置了自己的AttributeUsage特性,AttributeTargets.Class指定该过滤器只能用于类一级,即Controller;AllowMultiple = false设置不允许多次执行,即仅在Controller级执行一次。
实现4:
很明显,因为记录异常日志的需求是全局性的,因此采用注册全局性的过滤器,就能满足尽量精简代码的需求。在Gloabal注册过滤器时增加如下代码:
- public static void RegisterGlobalFilters(GlobalFilterCollection filters)
- {
- filters.Add(new HandleErrorAttribute());
- filters.Add(new LogExceptionAttribute());
- }
实现2:
返回JSON格式的错误信息不是全局性的,只是某些特定的Action才需要,因此需要设计一个异常过滤器专门返回异常的JSON信息。这个过滤器应该只需要作用于Action即可。根据之前的异常处理顺序,先里后外的原则,在处理异常时,会先处理这个JSON异常过滤器,再处理之前定义的LogExceptionAttribute,从而实现了返回JSON错误信息的同时并记录了异常日志。代码如下:
- [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
- public class JsonExceptionAttribute : HandleErrorAttribute
- {
- public override void OnException(ExceptionContext filterContext)
- {
- if (!filterContext.ExceptionHandled)
- {
- //返回异常JSON
- filterContext.Result = new JsonResult
- {
- Data = new { Success = false, Message = filterContext.Exception.Message }
- };
- }
- }
- }
JsonExceptionAttribute里会生成一个新的JsonResult对象,并赋值给返回结果(当然,这里也需要统一整个站点的JSON返回格式);同时通过AttributeTargets.Method指定该过滤器只能用于方法一级,即对应Action。
需要注意的是,不需要调用base.OnException,否则会跳过LogExceptionAttribute先执行HandleErrorAttribute的处理逻辑,从而返回结果不再是JsonResult,而是ViewResult,客户端也就无法处理非JSON的结果了。
这里也不需要设置filterContext.ExceptionHandled = true,否则在LogExceptionAttribute处理时,因为 !filterContext.ExceptionHandled 的判断条件,LogExceptionAttribute的逻辑不会执行,也就不会记录异常日志了。
使用时,仅需要在Action上声明这个特性即可。代码如下:
- [HttpPost]
- [JsonException]
- public JsonResult Add(string ip, int port)
- {
- ... //处理逻辑
- return Json(new { Success = true, Message = "添加成功" });
- }
为了配合JsonExceptionAttribute的正常运行,LogExceptionAttribute也需要做相应的改动:
- [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
- public class LogExceptionAttribute : HandleErrorAttribute
- {
- public override void OnException(ExceptionContext filterContext)
- {
- if (!filterContext.ExceptionHandled)
- {
- string controllerName = (string)filterContext.RouteData.Values["controller"];
- string actionName = (string)filterContext.RouteData.Values["action"];
- string msgTemplate = "在执行 controller[{0}] 的 action[{1}] 时产生异常";
- LogManager.GetLogger("LogExceptionAttribute").Error(string.Format(msgTemplate, controllerName, actionName), filterContext.Exception);
- }
- if (filterContext.Result is JsonResult)
- {
- //当结果为json时,设置异常已处理
- filterContext.ExceptionHandled = true;
- }
- else
- {
- //否则调用原始设置
- base.OnException(filterContext);
- }
- }
- }
注意前后比较一下,在LogExceptionAttribute不会直接调用base.OnException了,而是先判断当前的返回结果是不是JsonResult。返回结果是JsonResult,则表明之前处理过JsonExceptionAttribute,此时需要设置 filterContext.ExceptionHandled = true,并不再继续基类HandleErrorAttribute的处理;返回结果不是JsonResult,则调用base.OnException,继续执行基类HandleErrorAttribute的逻辑,转向错误页面。
如果需要扩展其他类型的异常处理,只需要增加对应的异常过滤器,并在LogExceptionAttribute里进行相应的改造即可。
添加以上的过滤器并配合配置文件中改变,我们的异常处理的几点需求就全部完成了。如果没有太大的变化,这样的处理模式是可以通用于MVC站点的。采用这种模式,如果没有特殊需求,在我们的控制器,逻辑层,数据访问层等,都不需要增加额外的异常处理的代码,产生异常后直接外抛,最终都会被异常过滤器拦截并进行处理。
即使因为特定需求的原因,可能需要为某些代码块加上try{}catch{}进行异常捕获和处理,也推荐在catch语句中处理完毕后仍使用throw语句将异常抛出,统一由LogExceptionAttribute来进行最终的捕捉和处理。这样将大量缩减try{}catch{}语句的重复出现。当然,最终具体如何处理异常还将视具体情况进行调整。
MVC过滤器使用案例:统一处理异常顺道精简代码的更多相关文章
- asp.net MVC 过滤器使用案例:统一处理异常顺道精简代码
重构的乐趣在于精简代码,模块化设计,解耦功能……而对异常处理的重构则刚好满足上述三个方面,下面是我的一点小心得. 一.相关的学习 在文章<精简自己20%的代码>中,讨论了异常的统一处理,并 ...
- 一种利用异常机制基于MVC过滤器的防止重复提交的机制分享
防止重复提交验证机制 某些时候因为系统反应稍慢,急性子用户可能不耐烦会进行重复的提交,这个操作不仅可能造成系统负担,也可能产生垃圾数据. 出现这两种状况都是我们不希望的. 为此,在公司项目系统设计了以 ...
- MVC过滤器进行统一登录验证
统一登录验证: 1.定义实体类Person:利用特性标签验证输入合法性设计登录页面 1 2 3 4 5 6 7 8 9 public class Person { [DisplayName(& ...
- ASP.NET MVC学习系列(4)——MVC过滤器FilterAttribute
1.概括 MVC提供的几种过滤器其实也是一种特性(Attribute),MVC支持的过滤器类型有四种,分别是:AuthorizationFilter(授权),ActionFilter(行为),Resu ...
- SpringMVC中的拦截器、过滤器的区别、处理异常
1. SpringMVC中的拦截器(Interceptor) 1.1. 作用 拦截器是运行在DispatcherServlet之后,在每个Controller之前的,且运行结果可以选择放行或拦截! 除 ...
- 在 ASP.NET Web API 中使用 Attribute 统一处理异常
并非所有的异常都需要 try-catch 进行重复的处理,这会导致大量的重复性代码,一旦后续系统出现异常处理机制的修改,随着代码量增多,修改也会变的更加困难. ASP.NET Web API 中特别增 ...
- 跟我一起学.NetCore之MVC过滤器,这篇看完走路可以仰着头走
前言 MVC过滤器在之前Asp.Net的时候就已经广泛使用啦,不管是面试还是工作,总有一个考点或是需求涉及到,可以毫不疑问的说,这个技术点是非常重要的: 在之前参与的面试中,得知很多小伙伴只知道有一两 ...
- mvc过滤器学习(1)
mvc 过滤器结构图 AuthorizeAttribute AuthorizeAttribute是IAuthorizationFilter的默认实现,添加了Authorize特性的Action将对用户 ...
- ASP.NET MVC 过滤器(一)
ASP.NET MVC 过滤器(一) 前言 前面的篇幅中,了解到了控制器的生成的过程以及在生成的过程中的各种注入点,按照常理来说篇幅应该到了讲解控制器内部的执行过程以及模型绑定.验证这些知识了.但是呢 ...
随机推荐
- 李洪强漫谈iOS开发[C语言-051]-判断整数位数
- 李洪强漫谈iOS开发[C语言-047]-数列求和
// // main.c // 53 - 数列求和 - 李洪强 // // Created by vic fan on 16/10/15. // Copyright © 2016年 李洪强. ...
- cookie的设置,获取,取消
<!DOCTYPE> <html> <head> <meta http-equiv=Content-Type content="text/html; ...
- HDU 2955
Robberies Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) Total ...
- 解决:Could not parse response code.Server Reply: SSH-2.0-OpenSSH_5.3
[摘要:办理:org.apache.commons.net.MalformedServerReplyException: Could not parse response code.Server Re ...
- 修改RectTransform的宽度和高度
rectTransform.sizeDelta = new Vector2( width, height);
- WxInput模块则比较彻底的解决了这个问题
基于wxpython的GUI输入对话框2 在程序输入中,有时会要求同时改变多个参数值,而且类型也不尽相同, 这时TextEntryDialog就显得不适用了.WxInput模块则比较彻底的解决了这个问 ...
- IOS第18天(9,核心动画-动画组)
****动画组 // 核心动画都是假象,不能改变layer的真实属性的值// 展示的位置和实际的位置不同.实际位置永远在最开始位置 #import "HMViewController.h&q ...
- poj2115-C Looooops(扩展欧几里德算法)
本题和poj1061青蛙问题同属一类,都运用到扩展欧几里德算法,可以参考poj1061,解题思路步骤基本都一样.一,题意: 对于for(i=A ; i!=B ;i+=C)循环语句,问在k位存储系统中循 ...
- Java中Collection和Collections的区别(引用自:http://www.cnblogs.com/dashi/p/3597937.html)
1.java.util.Collection 是一个集合接口(集合类的一个顶级接口).它提供了对集合对象进行基本操作的通用接口方法.Collection接口在Java 类库中有很多具体的实现.Co ...