处理异常的方式
异常处理页
异常处理匿名委托方法
IExceptionFilter
ExceptionFilterAttribute
代码实现 创建项目 创建名字为ExceptionDemo
的ASP.NET Core
项目,类型为API
注释掉系统自带的异常处理中间件 在Startup.Configure
中有app.UseDeveloperExceptionPage();
这个中间件,这个就是ASP.NET Core
自带的一个异常处理页,但是这个页面错误信息太多,只适合开发时对开发人员进行提示,不适合放到生产环境,所以这里注释掉这个中间件
创建自定义的异常类 为什么要创建自定义的异常类 通常情况下我们系统里面的异常与我们业务逻辑里的异常是不同的,业务逻辑上的判断异常,比如输入的参数不合法、订单状态不符合条件,当前账户余额不足这样的错误信息,我们有两种处理方式,一种处理方式是对不同的逻辑输出不同的业务对象,还有一种方式就是对于这种业务逻辑输出一个异常,用异常来承载我们的逻辑的特殊分支,那这个时候我们就需要识别出哪些是我们的业务异常,哪些是我们不确定的未知异常 ,比如网络突发的无法连接、MySql的闪断之类的
那这里怎么识别出哪些是业务异常,哪些是未知异常?
首先通过定义一个接口,接口里有错误码和错误信息,当我们有一个业务出现异常,我们可以人为的抛出一个已经实现了这个接口的自定义异常类。然后在异常处理过程中,我们尝试将捕获到的异常转为我们定义的异常接口,如果能转成功,说明这个异常是我们认为抛出的业务异常,否则为系统抛出的未知异常
自定义异常类 在项目根目录创建文件夹Exceptions
,所有异常的自定义类都放在这里
创建IKnownException
接口,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 namespace ExceptionDemo.Exceptions { public interface IKnownException { string Message { get ; } int ErrorCode { get ; } object [] ErrorData { get ; } } }
创建KnownException
类,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 namespace ExceptionDemo.Exceptions { public class KnownException :IKnownException { public KnownException (object [] errorData, int errorCode, string message ) { ErrorData = errorData; ErrorCode = errorCode; Message = message; } public string Message { get ; } public int ErrorCode { get ; } public object [] ErrorData { get ; } public static readonly IKnownException UnKnown = new KnownException(errorData: new object [] { }, errorCode: 9999 , message: "未知错误" ); public static IKnownException FromKnownException (IKnownException exception ) { return new KnownException(errorData: exception.ErrorData, errorCode: exception.ErrorCode, message: exception.Message); } } }
创建测试用的InvalidParameterException
类,用来模拟参数错误的异常,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 using System;namespace ExceptionDemo.Exceptions { public class InvalidParameterException : Exception ,IKnownException { public InvalidParameterException (int errorCode, string message, params object [] errorData ) : base (message ) { ErrorCode = errorCode; ErrorData = errorData; } public int ErrorCode { get ; } public object [] ErrorData { get ; } } }
异常处理页代码实现 创建处理页面控制器ErrorController
,在Index
方法中获取到当前请求上下文的异常信息,并尝试进行转成IKnownException
,如果转成功则表示为业务逻辑异常,如果失败则表示为未知异常,未知异常则通过KnownException
的静态方法生成一个特定的未知异常对象,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 using ExceptionDemo.Exceptions;using Microsoft.AspNetCore.Diagnostics;using Microsoft.AspNetCore.Mvc;using Microsoft.Extensions.Logging;namespace ExceptionDemo.Controllers { public class ErrorController : Controller { private readonly ILogger<ErrorController> _logger; public ErrorController (ILogger<ErrorController> logger ) { _logger = logger; } [Route("/error" ) ] public IActionResult Index ( ) { var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>(); var ex = exceptionHandlerPathFeature?.Error; var knownException = ex as IKnownException; if (knownException == null ) { _logger.LogError(ex,ex.Message); knownException = KnownException.UnKnown; } else { knownException = KnownException.FromKnownException(knownException); } return View(knownException); } } }
对应的试图Index.cshtml
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @model ExceptionDemo.Exceptions.IKnownException @{ Layout = null; } <!DOCTYPE html > <html > <head > <meta name ="viewport" content ="width=device-width" /> <title > Index</title > </head > <body > <div > <p > 错误码:@Model.ErrorCode</p > <p > 错误信息:@Model.Message</p > </div > </body > </html >
回到Startup
,对ConfigureServices
和Configure
两个方法做出调整,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void ConfigureServices (IServiceCollection services ) { services.AddControllersWithViews(); } public void Configure (IApplicationBuilder app, IWebHostEnvironment env ) { app.UseExceptionHandler("/error" ); app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
切换到WeatherForecastController
,在这里来主动抛出异常,将Get
方法修改如下:
1 2 3 4 5 [HttpGet ] public IEnumerable<WeatherForecast> Get ( ) { throw new InvalidParameterException(65 , "参数有误" , new List<string >() {"exception info 1" ,"exception info 2" }); }
运行代码,访问/weatherforecast
,可以看到返回了以下
将WeatherForecastController.Get
里的异常换成一个普通的异常,在重新运行代码,可以看到页面会变成未知错误
的提示,同时控制台打印出来的日志是完全的异常日志
异常处理匿名委托方法代码实现 将Startup.Configure
方法中的app.UseExceptionHandler("/error");
注释掉,原位置新增以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 app.UseExceptionHandler(errApp => { errApp.Run(async context => { var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>(); var knownException = exceptionHandlerPathFeature.Error as IKnownException; if (knownException == null ) { var logger = context.RequestServices.GetService<ILogger<Startup>>(); logger.LogError(exceptionHandlerPathFeature.Error, exceptionHandlerPathFeature.Error.Message); knownException = KnownException.UnKnown; context.Response.StatusCode = StatusCodes.Status500InternalServerError; } else { knownException = KnownException.FromKnownException(knownException); context.Response.StatusCode = StatusCodes.Status200OK; } var jsonOptions = context.RequestServices.GetService<IOptions<JsonOptions>>(); context.Response.ContentType = "application/json; charset=utf-8" ; await context.Response.WriteAsync(JsonSerializer.Serialize(knownException, jsonOptions.Value.JsonSerializerOptions)); }); });
这里的操作与异常处理页逻辑差不多,只是不再返回视图,而是返回json
,同时设定好业务逻辑异常返回200状态码,未知异常返回500状态码(这样做的好处后面说明),运行代码,访问/weatherforecast
,通过修改抛出异常,可看到对应的返回结果
IExceptionFilter
代码实现在Exceptions
文件夹新建MyExceptionFilter.cs
,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;using Microsoft.AspNetCore.Mvc.Filters;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Logging;namespace ExceptionDemo.Exceptions { public class MyExceptionFilter :IExceptionFilter { public void OnException (ExceptionContext context ) { var knownException = context.Exception as IKnownException; if (knownException == null ) { var logger = context.HttpContext.RequestServices.GetService<ILogger<MyExceptionFilter>>(); logger.LogError(context.Exception,context.Exception.Message); knownException = KnownException.UnKnown; context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; } else { knownException = KnownException.FromKnownException(knownException); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; } context.Result = new JsonResult(knownException) { ContentType = "application/json; charset=utf-8" }; } } }
这里对异常的处理逻辑与异常处理匿名委托方法一样
修改Startup
的ConfigureServices
、Configure
,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void ConfigureServices (IServiceCollection services ) { services.AddControllersWithViews(options => { options.Filters.Add<MyExceptionFilter>(); }).AddJsonOptions( options => { options.JsonSerializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; }); } public void Configure (IApplicationBuilder app, IWebHostEnvironment env ) { app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
运行代码,访问/weatherforecast
,通过修改抛出异常,可看到对应的返回结果
ExceptionFilterAttribute
代码实现在Exceptions
中新建MyExceptionFilterAttribute.cs
,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;using Microsoft.AspNetCore.Mvc.Filters;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Logging;namespace ExceptionDemo.Exceptions { public class MyExceptionFilterAttribute : ExceptionFilterAttribute { public override void OnException (ExceptionContext context ) { var knownException = context.Exception as IKnownException; if (knownException == null ) { var logger = context.HttpContext.RequestServices.GetService<ILogger<MyExceptionFilterAttribute>>(); logger.LogError(context.Exception, context.Exception.Message); knownException = KnownException.UnKnown; context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; } else { knownException = KnownException.FromKnownException(knownException); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; } context.Result = new JsonResult(knownException) { ContentType = "application/json; charset=utf-8" }; } } }
修改Startup
的ConfigureServices
、Configure
,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void ConfigureServices (IServiceCollection services ) { services.AddControllersWithViews(); } public void Configure (IApplicationBuilder app, IWebHostEnvironment env ) { app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
在WeatherForecastController
中添加类特性,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 using System.Collections.Generic;using ExceptionDemo.Exceptions;using Microsoft.AspNetCore.Mvc;using Microsoft.Extensions.Logging;namespace ExceptionDemo.Controllers { [ApiController ] [Route("[controller]" ) ] [MyExceptionFilter ] public class WeatherForecastController : ControllerBase { private static readonly string [] Summaries = new [] { "Freezing" , "Bracing" , "Chilly" , "Cool" , "Mild" , "Warm" , "Balmy" , "Hot" , "Sweltering" , "Scorching" }; private readonly ILogger<WeatherForecastController> _logger; public WeatherForecastController (ILogger<WeatherForecastController> logger ) { _logger = logger; } [HttpGet ] public IEnumerable<WeatherForecast> Get ( ) { throw new InvalidParameterException(65 , "参数有误!!!" , new List<string >() {"exception info 1" ,"exception info 2" }); } } }
运行代码,访问/weatherforecast
,通过修改抛出异常,可看到对应的返回结果
总结
用特定的异常类或接口表示业务逻辑异常
为业务逻辑异常定义全局错误码
为未知异常定义特定的输出信息和错误码,不应该输出系统内部的异常堆栈
对已知的业务逻辑异常相应HTTP 200
,这样对监控系统友好,不会区分不开真异常和逻辑异常
对于未预见的异常相应HTTP 500
为所有异常记录详细的日志