mvc自定义全局异常处理

Posted 焰尾迭

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了mvc自定义全局异常处理相关的知识,希望对你有一定的参考价值。

  异常信息处理是任何网站必不可少的一个环节,怎么有效显示,记录,传递异常信息又成为重中之重的问题。本篇将基于上篇介绍的html2cancas截图功能,实现mvc自定义全局异常处理。先看一下最终实现效果:http://yanweidie.myscloud.cn/Home/Index

阅读目录

我理解中好的异常处理

    好的异常信息处理应该具有以下几个优点

  • 显示效果佳,而不是原生黄页
  • 能够从异常中直接分析出异常源
  • 能够记录传递异常信息给开发人员

     1.第一点显示效果方面可以自定义页面,常见的包括404和500状态码页面。在mvc中404页面可以通过以下两种方式进行自定义

  
<system.web>
<!--添加customErrors节点 定义404跳转页面-->
 <customErrors mode="On">
      <error statusCode="404" redirect="/Error/Path404" />
    </customErrors>
 </system.web>
//Global文件的EndRequest监听Response状态码
protected void Application_EndRequest()
{
  var statusCode = Context.Response.StatusCode;
    var routingData = Context.Request.RequestContext.RouteData;
    if (statusCode == 404 || statusCode == 500)
    {
      Response.Clear();
       Response.RedirectToRoute("Default", new { controller = "Error", action = "Path404" });
    }
}     
      2.第二点 异常信息应该详细,能够记录下请求参数,请求地址,浏览器版本服务器和当前用户等相关信息,这就需要对异常信息记录改造加工
      3.第三点 常见的异常信息都是记录在日志文件里面,日志文件过大时也不太好分析。发生异常时要是能马上将异常信息通过邮件或者图片等方式发给开发者,可以加快分析速度。

自定义异常处理

    

  这里采用mvc的过滤器进行异常处理,分别为接口500错误和页面500错误进行处理,接口部分异常需要记录请求参数,方便分析异常。

     首先定义了异常信息实体,异常实体包含了 请求地址类型(页面,接口),服务器相关信息(位数,CPU,操作系统,iis版本),客户端信息(UserAgent,HttpMethod,IP)

     异常实体代码如下

    /// <summary>
    /// 系统错误信息
    /// </summary>
    public class ErrorMessage
    {
        public ErrorMessage()
        {

        }
        public ErrorMessage(Exception ex,string type)
        {
            MsgType = ex.GetType().Name;
            Message = ex.InnerException != null ? ex.InnerException.Message : ex.Message;
            StackTrace = ex.StackTrace;
            Source = ex.Source;
            Time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
            Assembly = ex.TargetSite.Module.Assembly.FullName;
            Method = ex.TargetSite.Name;
            Type = type;

            DotNetVersion = Environment.Version.Major + "." + Environment.Version.Minor + "." + Environment.Version.Build + "." + Environment.Version.Revision;
            DotNetBit = (Environment.Is64BitProcess ? "64" : "32") + "";
            OSVersion = Environment.OSVersion.ToString();
            CPUCount = Environment.GetEnvironmentVariable("NUMBER_OF_PROCESSORS");
            CPUType = Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER");
            OSBit = (Environment.Is64BitOperatingSystem ? "64" : "32") + "";

            var request = HttpContext.Current.Request;
            IP = GetIpAddr(request) + ":" + request.Url.Port;
            IISVersion = request.ServerVariables["SERVER_SOFTWARE"];
            UserAgent = request.UserAgent;
            Path = request.Path;
            HttpMethod = request.HttpMethod;
        }
        /// <summary>
        /// 消息类型
        /// </summary>
        public string MsgType { get; set; }

        /// <summary>
        /// 消息内容
        /// </summary>
        public string Message { get; set; }

        /// <summary>
        /// 请求路径
        /// </summary>
        public string Path { get; set; }

        /// <summary>
        /// 程序集名称
        /// </summary>
        public string Assembly { get; set; }

        /// <summary>
        /// 异常参数
        /// </summary>
        public string ActionArguments { get; set; }

        /// <summary>
        /// 请求类型
        /// </summary>
        public string HttpMethod { get; set; }

        /// <summary>
        /// 异常堆栈
        /// </summary>
        public string StackTrace { get; set; }

        /// <summary>
        /// 异常源
        /// </summary>
        public string Source { get; set; }

        /// <summary>
        /// 服务器IP 端口
        /// </summary>
        public string IP { get; set; }

        /// <summary>
        /// 客户端浏览器标识
        /// </summary>
        public string UserAgent { get; set; }

        /// <summary>
        /// .NET解释引擎版本
        /// </summary>
        public string DotNetVersion { get; set; }

        /// <summary>
        ///  应用程序池位数
        /// </summary>
        public string DotNetBit { get; set; }


        /// <summary>
        /// 操作系统类型
        /// </summary>
        public string OSVersion { get; set; }

        /// <summary>
        /// 操作系统位数
        /// </summary>
        public string OSBit { get; set; }

        /// <summary>
        /// CPU个数
        /// </summary>
        public string CPUCount { get; set; }

        /// <summary>
        /// CPU类型
        /// </summary>
        public string CPUType { get; set; }

        /// <summary>
        /// IIS版本
        /// </summary>
        public string IISVersion { get; set; }

        /// <summary>
        /// 请求地址类型
        /// </summary>
        public string Type { get; set; }

        /// <summary>
        /// 是否显示异常界面
        /// </summary>
        public bool ShowException { get; set; }

        /// <summary>
        /// 异常发生时间
        /// </summary>
        public string Time { get; set; }

        /// <summary>
        /// 异常发生方法
        /// </summary>
        public string Method { get; set; }

        //这段代码用户请求真实IP
        private static string GetIpAddr(HttpRequest request)
        {
            //HTTP_X_FORWARDED_FOR
            string ipAddress = request.ServerVariables["x-forwarded-for"];
            if (!IsEffectiveIP(ipAddress))
            {
                ipAddress = request.ServerVariables["Proxy-Client-IP"];
            }
            if (!IsEffectiveIP(ipAddress))
            {
                ipAddress = request.ServerVariables["WL-Proxy-Client-IP"];
            }
            if (!IsEffectiveIP(ipAddress))
            {
                ipAddress = request.ServerVariables["Remote_Addr"];
                if (ipAddress.Equals("127.0.0.1") || ipAddress.Equals("::1"))
                {
                    // 根据网卡取本机配置的IP
                    IPAddress[] AddressList = Dns.GetHostEntry(Dns.GetHostName()).AddressList;
                    foreach (IPAddress _IPAddress in AddressList)
                    {
                        if (_IPAddress.AddressFamily.ToString() == "InterNetwork")
                        {
                            ipAddress = _IPAddress.ToString();
                            break;
                        }
                    }
                }
            }
            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照\',\'分割
            if (ipAddress != null && ipAddress.Length > 15)
            {
                if (ipAddress.IndexOf(",") > 0)
                {
                    ipAddress = ipAddress.Substring(0, ipAddress.IndexOf(","));
                }
            }
            return ipAddress;
        }

        /// <summary>
        /// 是否有效IP地址
        /// </summary>
        /// <param name="ipAddress">IP地址</param>
        /// <returns>bool</returns>
        private static bool IsEffectiveIP(string ipAddress)
        {
            return !(string.IsNullOrEmpty(ipAddress) || "unknown".Equals(ipAddress, StringComparison.OrdinalIgnoreCase));
        }
    }

上面代码中用到了获取客户端请求IP的方法,用于获取请求来源的真实IP。

基础异常信息定义完后,剩下的是异常记录和页面跳转了,mvc中的异常过滤器实现如下。

 /// <summary>
    /// 全局页面控制器异常记录
    /// </summary>
    public class CustomErrorAttribute : HandleErrorAttribute
    {
        public override void OnException(ExceptionContext filterContext)
        {
            base.OnException(filterContext);

            ErrorMessage msg = new ErrorMessage(filterContext.Exception, "页面");
            msg.ShowException = MvcException.IsExceptionEnabled();

            //错误记录
            LogHelper.WriteLog(JsonConvert.SerializeObject(msg, Formatting.Indented), null);

            //设置为true阻止golbal里面的错误执行
            filterContext.ExceptionHandled = true;
            filterContext.Result = new ViewResult() { ViewName = "/Views/Error/ISE.cshtml", ViewData = new ViewDataDictionary<ErrorMessage>(msg) };

        }
    }

    /// <summary>
    /// 全局API异常记录
    /// </summary>
    public class ApiHandleErrorAttribute : ExceptionFilterAttribute
    {
        public override void OnException(HttpActionExecutedContext filterContext)
        {
            base.OnException(filterContext);

            //异常信息
            ErrorMessage msg = new ErrorMessage(filterContext.Exception, "接口");
            //接口调用参数
            msg.ActionArguments = JsonConvert.SerializeObject(filterContext.ActionContext.ActionArguments, Formatting.Indented);
            msg.ShowException = MvcException.IsExceptionEnabled();

            //错误记录
            string exMsg = JsonConvert.SerializeObject(msg, Formatting.Indented);
            LogHelper.WriteLog(exMsg, null);

            filterContext.Response = new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError, Content = new StringContent(exMsg) };
        }
    }

    /// <summary>
    /// 异常信息显示
    /// </summary>
    public class MvcException
    {
        /// <summary>
        /// 是否已经获取的允许显示异常
        /// </summary>
        private static bool HasGetExceptionEnabled = false;

        private static bool isExceptionEnabled;

        /// <summary>
        /// 是否显示异常信息
        /// </summary>
        /// <returns>是否显示异常信息</returns>
        public static bool IsExceptionEnabled()
        {
            if (!HasGetExceptionEnabled)
            {
                isExceptionEnabled = GetExceptionEnabled();
                HasGetExceptionEnabled = true;
            }
            return isExceptionEnabled;
        }

        /// <summary>
        /// 根据Web.config AppSettings节点下的ExceptionEnabled值来决定是否显示异常信息
        /// </summary>
        /// <returns></returns>
        private static bool GetExceptionEnabled()
        {
            bool result;
            if(!Boolean.TryParse(ConfigurationManager.AppSettings["ExceptionEnabled"],out result))
            {
                return false;
            }
            return result;
        }
    }

值得注意的是上面的MvcException类的GetExceptionEnabled方法,该方法从web.config appsetting中读取节点"ExceptionEnabled"来控制异常信息是否初始化显示。异常信息除了显示在页面,还使用了log4net组件记录在错误日志中,方便留痕。

过滤器定义完成后,需要在filterconfig添加引用

    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new CustomErrorAttribute());
            filters.Add(new HandleErrorAttribute());
        }
    }

 

问题拓展

  后台异常处理代码完成以后,前台还需进行相应的处理。这里主要针对api接口,因为请求页面后台可以直接转向500错误页面,而api接口一般是通过ajax或者客户端httpclient请求的,如果错误了跳转到500页面,这样对客户端来说就不友好了。基于这点所以api请求异常返回了异常的详细json对象,让客户端自己进行异常处理。我这里给出ajax处理异常的方式。

     在jquery中全局ajax请求可以设置相应默认参数,比如下面代码设置了全局ajax请求为异步请求,不缓存

//ajax请求全局设置
$.ajaxSetup({
    //异步请求
    async: true,
    //缓存设置
    cache: false
});

    ajax请求完成会触发Complete事件,在jquery中全局Complete事件可以通过下面代码监听

$(document).ajaxComplete(function (evt, request, settings) {
    var text = request.responseText;
    if (text) {
        try {
            //Unauthorized  登录超时或者无权限
            if (request.status == "401") {
                var json = $.parseJSON(text);
                if (json.Message == "logout") {
                    //登录超时,弹出系统登录框
                } else {
                    layer.alert(json.ExceptionMessage ? json.ExceptionMessage : "系统异常,请联系系统管理员", {
                        title: "错误提醒",
                        icon: 2
                    });
                }
            } else if (request.status == "500") {
                var json = $.parseJSON(text);
                $.ajax({
                    type: "post",
                    url: "/Error/Path500",
                    data: { "": json },
                    data: json,
                    dataType: "html",
                    success: function (data) {
                        //页面层
                        layer.open({
                            title: \'异常信息\',
                            type: 1,
                            shade: 0.8,
                            shift: -1,
                            area: [\'100%\', \'100%\'],
                            content: data,
                        });
                    }
                });

            }
        } catch (e) {
            console.log(e);
        }
    }
});
红色部分代码就是我用来处理500错误的代码,重新发请求到异常显示界面渲染成html后显示。其实这么做无疑增加了一次请求,最好的实现方式,直接通过异常信息json,通过js绘制出html。至此完成了mvc全局的页面,接口异常信息处理。通过结合上面的前端截图插件,快速截图留证,方便后续程序员分析异常信息。

总结

  通过一点小小的改造,我们完成了一个既美观又方便拓展的错误处理方式。看到上面萌萌的图片你是否心动了,想马上下载代码体验一把呢。下面就给出本文所有的源代码:

     git代码地址:https://github.com/CrazyJson/CustomGlobalError

      预告一下,下一篇将会对之前的TaskManager管理平台进行升级,主要实现管理界面方便查看当前运行的所有任务和管理任务。讲解管理平台运用到的技术,敬请期待!

 

 

以上是关于mvc自定义全局异常处理的主要内容,如果未能解决你的问题,请参考以下文章

全局异常方式处理自定义异常 @RestControllerAdvice + @ExceptionHandler

ASP.NET MVC Ajax 错误处理

MVC 自定义过滤器(Filter)实现路由控制异常处理授权处理(获取客户端信息)

Struts2自定义拦截器处理全局异常

spring的全局自定义异常案例「完美拦截Controller层全部异常」

七天学会ASP.NET MVC ——线程问题异常处理自定义URL