重构的乐趣在于精简代码,模块化设计,解耦功能……而对异常处理的重构则刚好满足上述三个方面,下面是我的一点小心得。

一、相关的学习

在文章《精简自己20%的代码》中,讨论了异常的统一处理,并通过对异常处理的封装达到精简代码的目的。具体有两种处理方法:

  1. 方法1:封装一个包含try{}catch{}finally{}的异常处理逻辑的方法类,将别的方法作为参数传入该方法,在页面内调用封装后的方法,大体代码如下:
public class Process
{
public static bool Execute(Action action)
{
try
{
action.Invoke();
return true;
}
catch (Exception ex)
{
return false;
}
finally
{
}
}
}
二、在MVC站点上的应用

先简单回顾一下以前的知识:

以标准的三层结构来说,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方法的调用,如果移至逻辑层里进行封装,则在逻辑层里会产生大量重复的代码,还是不够精简。而且这段代码不但手动重复书写,也没有和逻辑层的功能解耦,没有实现模块化。因此还需要继续重构。

三、MVC过滤器

从方法2使用Attribute的思路很容易就能想到MVC的过滤器,利用过滤器的拦截功能能很好的按照AOP思想实现异常处理,并解耦于逻辑层的模块。关于MVC过滤器的介绍,网上的文章很多,推荐《MVC过滤器详解》。这里要着重说一下过滤器的执行顺序。

  • 一般的过滤器执行顺序
  1. IAuthorizationFilter->OnAuthorization(授权)
  2. IActionFilter          ->OnActionExecuting(行为)
  3. Action
  4. IActionFilter          ->OnActionExecuted(行为)
  5. IResultFilter          ->OnResultExecuting(结果)
  6. View
  7. IResultFilter          ->OnResultExecuted(结果)
  8. *IExceptionFilter    ->OnException(异常),此方法并不在以上的顺序执行中,有异常发生时即会执行,有点类似于中断
  • 当同时在Controller和Action中都设置了过滤器后,执行顺序一般是由外到里,即“全局”->“控制器”->“行为”
  1. Controller->IAuthorizationFilter->OnAuthorization
  2. Action     ->IAuthorizationFilter->OnAuthorization
  3. Controller->IActionFilter          ->OnActionExecuting
  4. Action     ->IActionFilter          ->OnActionExecuting
  5. Action
  6. Action     ->IActionFilter          ->OnActionExecuted
  7. Controller->IActionFilter          ->OnActionExecuted
  8. Controller->IResultFilter          ->OnResultExecuting
  9. Action     ->IResultFilter          ->OnActionExecuting
  10. Action     ->IResultFilter          ->OnActionExecuted
  11. Controller->IResultFilter          ->OnActionExecuted
  • 因为异常是从里往外抛,因次异常的处理顺序则刚好相反,一般是由里到外,即“行为”->“控制器”->“全局”
  1. Action     ->IExceptionFilter->OnException
  2. 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() == 500) && 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 = 500;
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" />
五、自定义的异常统一处理

在实现异常的统一处理之前,先来明确一下需求:

  1. 站点所有页面在异常发生后,均需要记录异常日志,并转向错误提示页面(异常内容的详略程度由具体需求决定)
  2. 所有返回JSON数据的异步请求,不但需要记录异常日志,而且需要向客户端返回JSON格式的错误信息提示,而不是转向错误提示页面(异步请求也不可能转向错误提示页面)
  3. 采用AOP思想,将异常处理解耦
  4. 尽量精简声明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);
}
}

ogExceptionAttribute继承了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{}语句的重复出现。当然,最终具体如何处理异常还将视具体情况进行调整。

asp.net MVC 过滤器使用案例:统一处理异常顺道精简代码的更多相关文章

  1. MVC过滤器使用案例:统一处理异常顺道精简代码

    重构的乐趣在于精简代码,模块化设计,解耦功能……而对异常处理的重构则刚好满足上述三个方面,下面是我的一点小心得. 一.相关的学习 在文章<精简自己20%的代码>中,讨论了异常的统一处理,并 ...

  2. ASP.NET MVC 过滤器(一)

    ASP.NET MVC 过滤器(一) 前言 前面的篇幅中,了解到了控制器的生成的过程以及在生成的过程中的各种注入点,按照常理来说篇幅应该到了讲解控制器内部的执行过程以及模型绑定.验证这些知识了.但是呢 ...

  3. ASP.NET MVC 过滤器(三)

    ASP.NET MVC 过滤器(三) 前言 本篇讲解行为过滤器的执行过程,过滤器实现.使用方式有AOP的意思,可以通过学习了解过滤器在框架中的执行过程从而获得一些AOP方面的知识(在顺序执行的过程中, ...

  4. ASP.NET MVC 过滤器(四)

    ASP.NET MVC 过滤器(四) 前言 前一篇对IActionFilter方法执行过滤器在框架中的执行过程做了大概的描述,本篇将会对IActionFilter类型的过滤器使用来做一些介绍. ASP ...

  5. ASP.NET MVC 过滤器(五)

    ASP.NET MVC 过滤器(五) 前言 上篇对了行为过滤器的使用做了讲解,如果在控制器行为的执行中遇到了异常怎么办呢?没关系,还好框架给我们提供了异常过滤器,在本篇中将会对异常过滤器的使用做一个大 ...

  6. ASP.NET没有魔法——ASP.NET MVC 过滤器(Filter)

    上一篇文章介绍了使用Authorize特性实现了ASP.NET MVC中针对Controller或者Action的授权功能,实际上这个特性是MVC功能的一部分,被称为过滤器(Filter),它是一种面 ...

  7. Asp.net Mvc 过滤器执行顺序

    Asp.net Mvc 过滤器执行顺序: IAuthorizationFilter(OnAuthorization)----->IActionFilter(OnActionExecuting)- ...

  8. ASP.NET MVC过滤器

    在ASP.NET MVC中有个重要特性就是过滤器,使得我们在MVC程序开发中更好的控制浏览器请求的URL,不是每个请求都有响应内容,只有特定得用户才有.园子里关于过滤器的资料也有很多,这篇文章主要是记 ...

  9. ASP.NET MVC过滤器(一)

    MVC过滤器是加在 Controller 或 Action 上的一种 Attribute,通过过滤器,MVC 网站在处理用户请求时,可以处理一些附加的操作,如:用户权限验证.系统日志.异常处理.缓存等 ...

随机推荐

  1. python -socket -client

    socket client 发起连接. 流程为: 创建接口 发起连接 创建接口参数同socket server相同 发起连接的函数为socket.connect(ip,port) 这个地方的ip与po ...

  2. /proc/sys/vm/参数

    1) /proc/sys/vm/block_dump该文件表示是否打开Block Debug模式,用于记录所有的读写及Dirty Block写回动作.缺省设置:0,禁用Block Debug模式2) ...

  3. js 表单验证控制代码大全

    js表单验证控制代码大全 关键字:js验证表单大全,用JS控制表单提交 ,javascript提交表单:目录:1:js 字符串长度限制.判断字符长度 .js限制输入.限制不能输入.textarea 长 ...

  4. python批量进行文件修改操作

    python批量修改文件扩展名 在网上下载了一些文件,因为某种原因,扩展名多了一个后缀'.xxx',手动修改的话因为文件太多,改起来费时费力,于是决定写个小脚本进行修改. 1.要点: import r ...

  5. [转载]ERP实施40问 60分钟外行变专家

    http://www.chinaodoo.net/thread-389-1-1.html 在多年的实践中,结合自身经验和多年的理论积累,总结出有关ERP实施的最关键的40个问题,以问答的形式,让您在最 ...

  6. Bypass WAF Cookbook

    PS.之前一直想把零零碎碎的知识整理下来,作为知识沉淀下来,正好借着wooyun峰会的机会将之前的流程又梳理了一遍,于是就有了下文.也希望整理的内容能给甲方工作者或则白帽子带来一些收获. 0x00 概 ...

  7. h5 与app交互

    http://www.jianshu.com/p/7151987f012d JSContext *context = [self.webView valueForKeyPath:@"docu ...

  8. LuaSrcDiet工具介绍(lua源码处理软件)

    Diet Food Diet (nutrition), the sum of the food consumed by an organism or group Dieting, the delibe ...

  9. DNS CNAME的一些细节

    1, 概述 DNS中的CNAME可以减轻运维压力,使得已有的DNS配置具有一定的灵活性和可扩展性.本文对CNAME中的一些细节做阐述, 使DNS服务器的运维人员和开发人员能合理地使用CNAME. 2, ...

  10. JavaScript基本操作

    一.如何编写? 1.JavaScript代码存在形式 <!-- 方式一 --> <script type="text/javascript" src=" ...