在MVC2.0 中 遭遇無法被 Try Catch 的 “Exception”
前天當我為新項目新增完日志模塊后對日志模塊進行測試,測試時居然發現開發人員一段非常簡單的代碼,而且很標準的try ... catch .. 寫法。代碼整理如下:
{
try
{
//LinqToSql:返回IQueryable數據集合。
var iQueryableData = (from o in _Context.Orders//.Where(o => o.OrderID == 10248)
select new
{
ShipName = o.ShipName,
Employee = o.Employee,
}).ToList();
//LINQ:返回IEnumerable集合。
var iEnumerableData = from d in iQueryableData
select new
{
ShipName = d.ShipName,
EmployeeName = d.Employee.LastName //空引用未處理引發程序異常。
};
return Json(new { Success = true, Msg = iEnumerableData }, JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{
return Json(new { Success = false, Msg = ex.Message }, JsonRequestBehavior.AllowGet);
}
}
為方便大家閱讀,我用 NORTHWIND 數據庫。同時在該數據庫內執行 SQL :update orders set EmployeeID =null where OrderID =10248 。這樣造成上述代碼第18 行Linq代碼迭代時產生異常。您認為 這個異常可以被catch住么?答案當然是否定的!
當發現這個Bug后頓時讓我產生了興趣,我不知道如果微軟的MVC項目開發人員看到這個bug,他是如何解釋的。接下來讓我就來一個非官方解釋吧,歡迎拍磚!
我們可以在第18行設置好斷點,然后用VS2010 Debug程序,發現執行到第 21 行 return Json 時 VS未報任何bug。過數秒后斷點直接定位在第18行,提示也很簡單:未將對象應用到實例。這個確實是我們預期想要的 Exception 但是它始終沒有被 catch 住!通過StackTrace我發現了這么一句提示: at System.Web.Mvc.ControllerActionInvoker.InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult) 。上述代碼第21行return Json 被執行后返回了 JsonAction 這個 JsonAction 被 InvokeActionResult 調用?難道這個就是問題的關鍵所在?原因是寫有try catch 函數(本例的SaveTest()函數)的外層調用函數(InvokeActionResult()函數)出現了異常。也就是外層函數出現異常我們的內層函數SaveTest()又怎能try catch 到這個外層異常呢?于是通過Reflector進一步證實我的想法。具體的請看下圖:
主要是這句action.ExecuteResult(ControllerContext);它的執行過程如下:
最后一幅圖的JavaScriptSerializer serializer = new JavaScriptSerializer(); 進行 json 序列化時這個外層函數報出了異常。這個異常在我們的內層函數 SaveTest() 方法內是不可能被截獲到的。此刻我已完全弄明白這個無法被catch 的exception 是怎么出現的!至于這個異常為什么會被外層函數觸發,主要是.net Linq 延遲加載機制導致的,這不屬于本文討論的范疇,具體的園子內很多文章已經解釋的很清楚了。
為消滅掉這個截獲不到的exception現附上解決方案:
MVC雖然很年輕,但是這個架構真的很優秀我們可以非常方便對其擴展(我不是MVC的托,呵呵)。為了解決問題,我們只要自定義一個特性該特性繼承 HandleErrorAttribute 特性即可。接著重寫基類的 OnException 虛方法。代碼如下:
{
public override void OnException(ExceptionContext Context)
{
base.OnException(Context);
dynamic ex = Context.Exception;
if (!Context.ExceptionHandled)
return;
//TODO:將 ex 錯誤對象記錄到系統日志模塊
}
}
接著我們在MVC Controller 內這樣調用:
*截獲InvokeActionResult 調用 actionResult參數的 ExecuteResult 方法。
ExecuteResult 方法執行時 進行 以下操作:
JavaScriptSerializer serializer = new JavaScriptSerializer();
response.Write(serializer.Serialize(this.Data));
將C# 匿名對象序列化成json數據供 jquery ajax 方法回調。
*/
[CustomHandleError]
public JsonResult SaveTest()
{
//try
//{
//TestMehod();
//LinqToSql:返回IQueryable數據集合。
var iQueryableData = (from o in _Context.Orders//.Where(o => o.OrderID == 10248)
select new
{
ShipName = o.ShipName,
Employee = o.Employee,
}).ToList();
//LINQ:返回IEnumerable集合。
var iEnumerableData = from d in iQueryableData
select new
{
ShipName = d.ShipName,
EmployeeName = d.Employee.LastName //空引用未處理引發不可截獲的異常。
};
return Json(new { Success = true, Msg = iEnumerableData }, JsonRequestBehavior.AllowGet); //Json序列化。
//}
//catch //外層錯誤,導致內層函數catch失效,無法有效的截取錯誤信息。
//{
// return Json(new { Success = false, Msg = "操作失敗。" }, JsonRequestBehavior.AllowGet);
//}
}
代碼看上去更簡潔了吧?連try catch 都不用寫了更不用擔心啥時候這個截獲不到的exception神秘人物登場,這樣噩夢結束了。
本例代碼這里下載