处理异常的方式

  • 异常处理页
  • 异常处理匿名委托
  • IExceptionFilter
  • ExceptionFilterAttribute

示例

新建Web程序👉选择API模板
Controllers文件夹👉WeatherForecastController类👉Get方法首行抛出一个异常

  1. public IEnumerable<WeatherForecast> Get()
  2. {
  3. throw new Exception("throw a exception");
  4. // ...
  5. }

启动程序:image.png
这个页面是在Configure方法中配置的:如果是开发环境就会使用开发环境异常页

  1. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  2. {
  3. if (env.IsDevelopment())
  4. {
  5. app.UseDeveloperExceptionPage();
  6. }
  7. // ...
  8. }

这样的错误页我们在生产环境是需要关闭掉的。

异常处理页

可以通过定义一个错误页来承载我们的错误信息。
在在Configure方法中配置:

  1. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  2. {
  3. //if (env.IsDevelopment())
  4. //{
  5. // app.UseDeveloperExceptionPage();
  6. //}
  7. app.UseExceptionHandler("/error");
  8. // ...
  9. }

右键项目新建文件夹Exceptions👉新建IKnownException接口

  1. namespace ExceptionDemo.Exceptions
  2. {
  3. public interface IKnownException
  4. {
  5. public string Message { get; }
  6. public int ErrorCode { get; }
  7. public object[] ErrorData { get; }
  8. }
  9. }

新建KnownException类,对IKnownException接口的默认实现

  1. namespace ExceptionDemo.Exceptions
  2. {
  3. public class KnownException : IKnownException
  4. {
  5. public string Message { get; private set; }
  6. public int ErrorCode { get; private set; }
  7. public object[] ErrorData { get; private set; }
  8. public readonly static IKnownException UnKnown = new KnownException { ErrorCode = 9999, Message = "未知错误" };
  9. public static IKnownException FromKnownException(IKnownException knownException)
  10. {
  11. return new KnownException { Message = knownException.Message, ErrorCode = knownException.ErrorCode, ErrorData = knownException.ErrorData };
  12. }
  13. }
  14. }

这里使用FromKnownException静态方法,用于全局控制赋值时的特殊处理。
为什么会需要我们去定义这样一个类型呢?是因为通常情况下,我们系统里面的异常和业务逻辑的异常是不同的。业务逻辑异常上比如说输入的参数,订单的状态不符合条件,当前账户余额不足等。我们有两种处理方式,一种处理方式就是对不同的逻辑输出不同的业务对象,还有一种就是对业务逻辑异常直接输出,用异常来承载我们逻辑的特殊分支。这个时候我们就需要识别出来哪些是我们的业务异常,哪些是我们不确定的异常(比如说网络异常,数据库连接断了)。

Controllers文件夹👉新建ErrorController

  1. using ExceptionDemo.Exceptions;
  2. using Microsoft.AspNetCore.Diagnostics;
  3. using Microsoft.AspNetCore.Mvc;
  4. using Microsoft.Extensions.DependencyInjection;
  5. using Microsoft.Extensions.Logging;
  6. namespace ExceptionDemo.Controllers
  7. {
  8. public class ErrorController : Controller
  9. {
  10. [Route("/error")]
  11. public IActionResult Index()
  12. {
  13. // 获取异常
  14. var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
  15. var ex = exceptionHandlerPathFeature?.Error;
  16. // 尝试将异常转换为已知的异常
  17. var knownException = ex as IKnownException;
  18. if (knownException == null)
  19. {
  20. // 如果不是已知的异常,我们就不应该把错误异常完整的输出给客户端
  21. var logger = HttpContext.RequestServices.GetService<ILogger<ErrorController>>();
  22. logger.LogError(ex, ex.Message);
  23. knownException = KnownException.UnKnown;
  24. }
  25. else
  26. {
  27. // 是已知的异常
  28. knownException = KnownException.FromKnownException(knownException);
  29. }
  30. return View(knownException);
  31. }
  32. }
  33. }

右键项目👉新建文件夹Views👉新建文件夹Error👉新建文件Index.cshtml

  1. @model ExceptionDemo.Exceptions.IKnownException
  2. @{
  3. ViewData["Title"] = "Index";
  4. }
  5. <h1>错误信息</h1>
  6. <div>
  7. ErrorCode : <label>@Model.ErrorCode</label>
  8. </div>
  9. <div>
  10. Message : <label>@Model.Message</label>
  11. </div>

Startup类👉ConfigureServices方法

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. //services.AddControllers();
  4. services.AddControllersWithViews();
  5. }

执行程序:image.png
这个时候未知的错误就会显示出来,而具体的错误会通过logger日志输出出来。

异常处理匿名委托

就是把上面那段逻辑直接定义在我们注册的地方

  1. public void Configure(IApplicationBuilder app, IWebHostEnvironment env,ILoggerFactory loggerFactory)
  2. {
  3. //if (env.IsDevelopment())
  4. //{
  5. // app.UseDeveloperExceptionPage();
  6. //}
  7. //app.UseExceptionHandler("/error");
  8. app.UseExceptionHandler(error =>
  9. {
  10. error.Run(async context =>
  11. {
  12. var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
  13. var knownException = exceptionHandlerPathFeature.Error as IKnownException;
  14. if (knownException == null)
  15. {
  16. var logger = loggerFactory.CreateLogger("Api.Exception");
  17. logger.LogError(exceptionHandlerPathFeature.Error, exceptionHandlerPathFeature.Error.Message);
  18. knownException = KnownException.UnKnown;
  19. // 将错误Http响应码设置为500
  20. context.Response.StatusCode = StatusCodes.Status500InternalServerError;
  21. }
  22. else
  23. {
  24. // 是已知的异常
  25. knownException = KnownException.FromKnownException(knownException);
  26. // 将Http响应码设置为200
  27. context.Response.StatusCode = StatusCodes.Status200OK;
  28. }
  29. var jsonOptions = context.RequestServices.GetService<IOptions<JsonOptions>>();
  30. context.Response.ContentType = "application/json;charset=utf-8";
  31. await context.Response.WriteAsync(JsonSerializer.Serialize(knownException, jsonOptions.Value.JsonSerializerOptions));
  32. });
  33. });
  34. // ...
  35. }

为什么对于未知的异常要输出 Http 500,而对于业务逻辑的异常,建议输出 Http 200?
因为监控系统实际上会对 Http 的响应码进行识别,当监控系统识别到 Http 响应是 500 的比例比较高的情况下,会认为系统的可用性有问题,这个时候告警系统就会发出警告
对于已知的业务逻辑的这种正常的识别的话,用正常的 Http 200 来处理是一个正常的行为,这样就可以让监控系统更好的工作,正确的识别出系统的一些未知的错误信息,错误的告警,让告警系统更加的灵敏,也避免了业务逻辑的异常干扰告警系统

通过异常过滤器的方式

这种方式实际上是作用在 MVC 的整个框架的体系下面的,它并不是在中间件的最早期发生作用的,它是在 MVC 的整个生命周期里面发生作用,也就是说它只能工作在 MVC Web API 的请求周期里面
Exceptions文件夹👉新建MyExceptionFilter

  1. using Microsoft.AspNetCore.Http;
  2. using Microsoft.AspNetCore.Mvc;
  3. using Microsoft.AspNetCore.Mvc.Filters;
  4. using Microsoft.Extensions.DependencyInjection;
  5. using Microsoft.Extensions.Logging;
  6. namespace ExceptionDemo.Exceptions
  7. {
  8. public class MyExceptionFilter : IExceptionFilter
  9. {
  10. public void OnException(ExceptionContext context)
  11. {
  12. IKnownException knownException = context.Exception as IKnownException;
  13. if (knownException == null)
  14. {
  15. var logger = context.HttpContext.RequestServices.GetService<ILogger<MyExceptionFilterAttribute>>();
  16. logger.LogError(context.Exception, context.Exception.Message);
  17. knownException = KnownException.UnKnown;
  18. context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
  19. }
  20. else
  21. {
  22. knownException = KnownException.FromKnownException(knownException);
  23. context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
  24. }
  25. context.Result = new JsonResult(knownException)
  26. {
  27. ContentType = "application/json; charset=utf-8"
  28. };
  29. }
  30. }
  31. }

处理的逻辑也与之前相同
ConfigureServices方法注册Filters

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddMvc(mvcOptions =>
  4. {
  5. mvcOptions.Filters.Add<MyExceptionFilter>();
  6. }).AddJsonOptions(jsonOptions =>
  7. {
  8. jsonOptions.JsonSerializerOptions.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
  9. });
  10. }

启动执行:image.png
输出与之前的一致,因为这是在 Controller里面输出了错误
如果在 MVC 的中间件之前输出错误的话,它是没办法处理的
这个场景一般情况下是指需要对 **Controller **进行特殊的异常处理,而对于中间件整体来讲的话,又要用另一种特殊的逻辑来处理的时候,可以用 **ExceptionFilter** 的方式处理。
这种方式还可以通过 Attribute的方式
自定义一个 MyExceptionFilterAttribute

  1. using Microsoft.AspNetCore.Http;
  2. using Microsoft.AspNetCore.Mvc;
  3. using Microsoft.AspNetCore.Mvc.Filters;
  4. using Microsoft.Extensions.DependencyInjection;
  5. using Microsoft.Extensions.Logging;
  6. namespace ExceptionDemo.Exceptions
  7. {
  8. public class MyExceptionFilterAttribute : ExceptionFilterAttribute
  9. {
  10. public override void OnException(ExceptionContext context)
  11. {
  12. IKnownException knownException = context.Exception as IKnownException;
  13. if (knownException == null)
  14. {
  15. var logger = context.HttpContext.RequestServices.GetService<ILogger<MyExceptionFilterAttribute>>();
  16. logger.LogError(context.Exception, context.Exception.Message);
  17. knownException = KnownException.UnKnown;
  18. context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
  19. }
  20. else
  21. {
  22. knownException = KnownException.FromKnownException(knownException);
  23. context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
  24. }
  25. context.Result = new JsonResult(knownException)
  26. {
  27. ContentType = "application/json;charset=uft-8"
  28. };
  29. }
  30. }
  31. }

Controller上面标注 MyExceptionFilter

  1. [MyExceptionFilter]
  2. public class WeatherForecastController : ControllerBase

启动运行之后效果相同
这两种方式的效果是对等的,区别在于说可以更细粒度的对异常处理进行控制,可以指定部分的 Controller或者 Exception,来决定我们的异常处理,也可以在全局注册 ExceptionFilter
当然因为 ExceptionFilterAttribute 也实现了 IExceptionFilter,所以它也可以注册到全局,也可以把它当作全局异常处理的过滤器来使用,Controller 上面也就不需要标记了
注册 Filters

  1. services.AddMvc(mvcOptions =>
  2. {
  3. //mvcOptions.Filters.Add<MyExceptionFilter>();
  4. mvcOptions.Filters.Add<MyExceptionFilterAttribute>();
  5. }).AddJsonOptions(jsonoptions =>
  6. {
  7. jsonoptions.JsonSerializerOptions.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
  8. });

Controller上面取消标注 MyExceptionFilter

  1. //[MyExceptionFilter]
  2. public class WeatherForecastController : ControllerBase

启动程序,输出结果一致
这个场景对于我们定义一些 API,然后对 API 进行定义我们的异常处理的约定是很有帮助的

总结一下

首先我们需要定义特定的异常类或者接口,我们可以定义抽象类,也可以用接口的方式,例子中是通过接口的方式表示业务逻辑的异常
对于业务逻辑的异常,实际上需要定义全局的错误码
对于未知的异常,应该输出特定的输出信息和错误码,然后记录完整的日志,我们不应该把系统内部的一些比如说异常堆栈这些信息输出给用户
对于已知的业务逻辑的异常,用 Http 200 的方式,对于未知的异常,用 Http 500 的方式,这样可以让监控系统更好的工作
另外一个建议就是尽量记录所有的异常的详细信息,以供后续对日志进行分析,也供监控系统做一些特定的监控警告