MVC系列——一個異常消息傳遞引發的思考


前言:最近在某個項目里面遇到一個有點糾結的小問題,經過半天時間的思索和嘗試,問題得到解決。在此記錄一下解決的過程,以及解決問題的過程中對.net里面MVC異常處理的思考。都是些老生常談的問題,不多說,直接上“主菜”。

本文原創地址:http://www.cnblogs.com/landeanfen/p/8135844.html

一、問題重現

項目是一個傳統.net framework的MVC項目,為了簡便,項目里面定義了一個自定義異常類用於向客戶端傳遞錯誤消息,客戶端接收到異常的消息時在瀏覽器里面彈出提示。先來看看這個自定義異常類CustormerException的定義

    public class CustomerException : System.Exception
    {
        public CustomerException()
        {

        }
        public CustomerException(string message) : base(message)
        {

        }
        public CustomerException(string message, params object[] args) : base(string.Format(message, args))
        {

        }
    }

為了模擬重現問題,我盡量將代碼簡化再簡化。

  [BaseException]
    public class DefaultController : Controller
    {
        // GET: Default
        public ActionResult Index()
        {
            return View();
        }

        public JsonResult Login(string userName, string password)
        {
            if (userName == "admin" && password == "admin")
            {
                return Json(true, JsonRequestBehavior.AllowGet);
            }
            else
            {
                throw new CustomerException("用戶名或者密碼錯誤");
            }
        }
    }

    public class BaseExceptionAttribute : HandleErrorAttribute
    {
        public override void OnException(ExceptionContext filterContext)
        {
            if (filterContext.HttpContext.Request.IsAjaxRequest() && filterContext.Exception is CustomerException)
            {
                filterContext.ExceptionHandled = true;
                filterContext.HttpContext.Response.StatusCode = (int)System.Net.HttpStatusCode.BadRequest;
                var result = new ContentResult() { Content = filterContext.Exception.Message, ContentType = MediaTypeNames.Text.Plain };
                filterContext.Result = result;
            }
            else
            {
                //記錄日志
            }

            base.OnException(filterContext);

        }
    }

代碼不復雜,就是一個通用的異常過濾器,用於記錄日志和傳遞消息到客戶端。

然后我們看看客戶端的測試代碼:

@{
    ViewBag.Title = "Index";
}

用戶名:<input type="text" id="username"/>
密碼  :<input type="text" id="password"/>
<button id="btnAjaxError" type="button">登陸</button>
@section Script
{
  <script type="text/javascript">

    $(function () {
        $("#btnAjaxError").click(function () {
            $.ajax({
                url:"/Default/Login",
                data: { userName: $('#username').val(), password: $('#password').val() },
                type: 'post'
            }).done(function (data) {
                console.log("successful:"+data);
            }).fail(function (a, b, c) {
                debugger;
                console.log("fail:"+a.responseText);
            });
        });
    });
</script>  
}

本地調試、運行得到正常結果

 發布到IIS,本地訪問仍然正常。可是當我們遠程訪問的時候問題出現了。

 

所有的遠程訪問機器上面都出現了系統默認的錯誤消息,而不是我們返回的業務異常消息。

二、初次嘗試

對於這種本地能看到詳細異常,而遠程看不到詳細異常的問題,相信有一定經驗的朋友肯定想到了一個配置,那就是Web.config里面的CustomErrors節點,我們配置下默認開啟自定義異常不就行了嗎。嘿嘿!就是這么簡單!博主當初也是這么樂呵呵的去嘗試的。我們在Web.config的System.web節點下面加入這個節點

<customErrors mode="On"></customErrors>

可是很遺憾,問題依舊!后來想是不是自己對於On、Off、RemoteOnly的理解有誤?於是乎三個項逐個嘗試,結果均已失敗告終!

於是乎開始有點郁悶了,這種問題原來怎么沒遇到過了,代碼“貌似”沒什么大問題啊,如果有問題,本地應該也不能得到才對啊。於是乎分析,這可能不是我們代碼的問題,而是IIS給我做了一層統一的異常處理,我們只需要將這層統一的異常處理去掉就行了啊。道理是這么個道理,可是如何去實現呢。於是乎把IIS的各個功能都試了個遍,最后的谷歌的一篇帖子里面找到了一些幫助。解決方案如下。

三、解決方案

1、“不是代碼的問題”的解決方案

上文說到這個問題或許不是代碼的問題,而是IIS配置的問題。於是乎真的讓博主找到了解決方案。解決步驟如下:

 

 

 

原來,IIS默認是不讓遠程用戶查看異常的詳細錯誤的,如果是遠程用戶,IIS會默認給你返回一個各種狀態碼對應的默認消息,我們自定義的消息將會被此覆蓋。如果改成選中第二項,就表示不管是本地用戶還是遠程用戶均可以看到詳細異常。

這樣配置之后不用更改任何代碼,不用理會是否配置了CustomErrors節點,遠程用戶均可以正常獲取到程序返回的異常消息:

2、“是代碼的問題”的解決方案

有了上面的解決方案,為何還會有“是代碼的問題”的解決方案呢?這才是本文想要表達的中心思想。既然我們通過配置IIS的錯誤頁可以解決這個問題,那么我們為什么不能在程序的范疇內去解決呢?博主是一個有點喜歡刨根問題的人,不斷分析代碼后發現,既然系統的默認錯誤消息可以覆蓋我們的自定義異常消息,那么反過來,我們自定義的異常消息為什么就不能覆蓋系統默認的異常消息呢?於是乎發現在重寫父類的OnException方法的時候,上面的代碼我們是先執行的我們自定義的異常消息,然后再調用 base.OnException(filterContext); 去執行系統默認的異常消息處理的,那么我們將這個順序倒置一下,反過來是不是可行呢?於是代碼就變成了這樣:

  public class BaseExceptionAttribute : HandleErrorAttribute
    {
        public override void OnException(ExceptionContext filterContext)
        {
            base.OnException(filterContext);
            if (filterContext.HttpContext.Request.IsAjaxRequest() && filterContext.Exception is CustomerException)
            {
                filterContext.ExceptionHandled = true;
                filterContext.HttpContext.Response.StatusCode = (int)System.Net.HttpStatusCode.BadRequest;
                var result = new ContentResult() { Content = filterContext.Exception.Message, ContentType = MediaTypeNames.Text.Plain };
                filterContext.Result = result;
            }
            else
            {
                //記錄日志
            }
        }
    }

 我們將上面通過配置IIS錯誤頁的解決方案還原,改成默認的配置。去掉CustomErrors節點,重新發布之后,問題完美解決:

問題能解決,說明博主上面的推想或許是正確的,自定義異常和默認異常是存在一個先后順序的,我們如果要覆蓋系統的異常,需要將我們自定義異常的代碼放在后面執行。這個論斷是通過上述解決問題的思路推理得來的,並不一定正確,有興趣的可以反編譯下dll看下是否真是這樣!

很有趣的一點就是,這樣改了代碼之后,我們如果在web.config里面加入customErrors節點,並且將mode設置為Off,遠程訪問的時候得到的異常消息又變成了“錯誤的請求”。其實這不難理解,當你禁用自定義錯誤信息,那么系統肯定會給你返回默認的異常信息了。

四、總結

由上述的兩種解決方案可以看出這里其實有三道防線:

第一道防線是最外層的防線,就是IIS的錯誤頁配置,如果這層配置選擇的是詳細錯誤,那么不管你其他的配置是什么樣,都會返回用戶自定義的錯誤信息;

第二道防線是中間的那層,就是web.config里面的CustomErrors節點,如果第一道防線是默認配置,這層防線才會生效;

第三道防線才是代碼的范疇,這個受限於CustomErrors節點的配置。

本文原創出處:http://www.cnblogs.com/landeanfen/

歡迎各位轉載,但是未經作者本人同意,轉載文章之后必須在文章頁面明顯位置給出作者和原文連接,否則保留追究法律責任的權利


免責聲明!

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



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