MVC過濾器使用案例:統一處理異常順道精簡代碼


重構的樂趣在於精簡代碼,模塊化設計,解耦功能……而對異常處理的重構則剛好滿足上述三個方面,下面是我的一點小心得。

一、相關的學習

在文章《精簡自己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
            {
            }
        }
    }
    View Code
  2. 方法2:利用Attribute,為方法加上特性進行攔截式的統一處理(文章內貌似沒有詳細說明)
二、在MVC站點上的應用

先簡單回顧一下以前的知識:

以標准的三層結構來說,MVC對應表現層,其中Controller中的Action負責收集數據並把數據傳遞給業務邏輯層的功能;邏輯層通過調用數據訪問層,獲取數據並進行相應的業務邏輯處理,最終將處理后的數據返回至Model。最后將Model與View結合形成展現給終端用戶瀏覽的頁面。如下圖:

 

按照方法1的思路,在Action里調用邏輯層方法的時候,用封裝好的異常處理方法來包裝一下,代碼如下:

public ActionResult Index(int id)
{
    Boolean result = false;                             //接收返回的值
    Process.Execute(() => result = Save(id)); //執行方法
    return View();
}
View Code

還可以利用Lambda我們可以包裝更為復雜的邏輯,且不一定需要返回bool值:

public ActionResult Index()
{
       Process.Execute(()=>
       {
               ...  //更為復雜的邏輯    
       });        
       return View();
}
View Code

這樣會在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()); }
View Code

由上可知,在應用程序啟動的時候就已經注冊了全局過濾器,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; } } }
View Code

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>
View Code

這個過濾器要能起效,還需要在配置文件中配置一下:

<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);
        }
    }
View Code

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()); }
View Code

實現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 } }; } } }
View Code

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 = "添加成功" });
}
View Code

為了配合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); } } }
View Code

注意前后比較一下,在LogExceptionAttribute不會直接調用base.OnException了,而是先判斷當前的返回結果是不是JsonResult。返回結果是JsonResult,則表明之前處理過JsonExceptionAttribute,此時需要設置 filterContext.ExceptionHandled = true,並不再繼續基類HandleErrorAttribute的處理;返回結果不是JsonResult,則調用base.OnException,繼續執行基類HandleErrorAttribute的邏輯,轉向錯誤頁面。

如果需要擴展其他類型的異常處理,只需要增加對應的異常過濾器,並在LogExceptionAttribute里進行相應的改造即可。

六、后記

添加以上的過濾器並配合配置文件中改變,我們的異常處理的幾點需求就全部完成了。如果沒有太大的變化,這樣的處理模式是可以通用於MVC站點的。采用這種模式,如果沒有特殊需求,在我們的控制器,邏輯層,數據訪問層等,都不需要增加額外的異常處理的代碼,產生異常后直接外拋,最終都會被異常過濾器攔截並進行處理。

即使因為特定需求的原因,可能需要為某些代碼塊加上try{}catch{}進行異常捕獲和處理,也推薦在catch語句中處理完畢后仍使用throw語句將異常拋出,統一由LogExceptionAttribute來進行最終的捕捉和處理。這樣將大量縮減try{}catch{}語句的重復出現。當然,最終具體如何處理異常還將視具體情況進行調整。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM