⭐️ Furion v4 版本支持【所有历史版本】无缝升级,一套代码兼容 .NET 5+ ⭐️
Skip to main content

7. 友好异常处理

7.1 什么是异常

异常一般是指运行期(此处特指 Exception 类)会发生的导致程序意外中止的问题,是一种对问题的描述后的封装对象。

在过去开发中,通常异常由系统运行时出错抛出,但现在的开发过程中,我们应在程序开发中合理的抛出异常,比如更新一条不存在的实体,或查询一个不存在的数据等等。

7.2 处理异常方式

  • 不处理,直接中断程序执行(不推荐)
  • 通过 try catch finally 处理(不推荐)
  • 全局统一处理,并记录异常信息(推荐)
  • 异常注解方式处理,支持本地化 (推荐)

7.3 什么是友好异常处理

7.3.1 非友好异常处理

在了解友好异常处理之前可以看看非友好异常处理:

  • 对终端用户抛出 500状态码 堆栈信息
  • 大量的 try catch 代码,污染正常业务逻辑
  • 没有规范化的异常状态码和异常消息管理
  • 没有异常日志收集记录
  • 不支持异常消息本地化处理
  • 不支持异常策略,失败后程序立即终止
  • 不支持分布式事务 CAP
  • 不支持异常传播
  • 返回的异常格式杂乱

7.3.2 友好异常处理

  • 对终端用户提示友好
  • 对后端开发人员提供详细的异常堆栈
  • 不干扰正常业务逻辑代码,如 没有 try catch 代码
  • 支持异常状态码多方设置
  • 支持异常消息本地化
  • 异常信息统一配置管理
  • 支持异常策略,如重试
  • 支持异常日志收集记录
  • 支持 CAP 分布式事务关联
  • 支持内部异常外部传播
  • 支持返回统一的异常格式数据

7.4 友好异常处理使用示例

Furion 框架提供了非常灵活的友好异常处理方式。

备注

.AddFriendlyException() 默认已经集成在 AddInject() 中了,无需再次注册。也就是 7.4.1 章节可不配置。

7.4.1 注册友好异常服务

Furion.Web.Core\FurWebCoreStartup.cs
using Microsoft.Extensions.DependencyInjection;namespace Furion.Web.Core{    [AppStartup(800)]    public sealed class FurWebCoreStartup : AppStartup    {        public void ConfigureServices(IServiceCollection services)        {            services.AddControllers()                    .AddFriendlyException();        }    }}
特别注意

.AddFriendlyException() 需在 services.AddControllers() 之后注册。

7.4.2 两个例子

简单抛个异常

using Furion.DynamicApiController;using Furion.FriendlyException;namespace Furion.Application{    public class FurionAppService : IDynamicApiController    {        public int Get(int id)        {            if (id < 3)            {                throw Oops.Oh($"{id} 不能小于3");            }            return id;        }    }}

如下图所示:

抛出特定类型异常

using Furion.DynamicApiController;using Furion.FriendlyException;using System;namespace Furion.Application{    public class FurionAppService : IDynamicApiController    {        public int Get(int id)        {            if (id < 3)            {                throw Oops.Oh($"{id} 不能小于3。", typeof(InvalidOperationException));            }            return id;        }    }}

如下图所示:

7.5 关于 Oops.Oh

通过上面的例子可以看出,Oops.Oh(errorMessage) 可以结合 throw 抛出异常。对于熟悉C#的人员来说,throw 后面只能 Exception 实例。Oops.Oh(...) 方法返回正是 Exception 实例。

7.5.1 为什么起这个名字?

这个名字来源于一个英语句子:Oh, Oops!,意思是 噢(哎),出错了!,所以就有了 Oops.Oh

7.5.2 Oops.Oh 重载方法

using System;namespace Furion.FriendlyException{    public static class Oops    {        /// <summary>        /// 抛出字符串异常        /// </summary>        /// <param name="errorMessage">异常消息</param>        /// <param name="args">String.Format 参数</param>        /// <returns>异常实例</returns>        public static Exception Oh(string errorMessage, params object[] args);        /// <summary>        /// 抛出字符串异常        /// </summary>        /// <param name="errorMessage">异常消息</param>        /// <param name="exceptionType">具体异常类型</param>        /// <param name="args">String.Format 参数</param>        /// <returns>异常实例</returns>        public static Exception Oh(string errorMessage, Type exceptionType, params object[] args);        /// <summary>        /// 抛出错误码异常        /// </summary>        /// <param name="errorCode">错误码</param>        /// <param name="args">String.Format 参数</param>        /// <returns>异常实例</returns>        public static Exception Oh(object errorCode, params object[] args);        /// <summary>        /// 抛出错误码异常        /// </summary>        /// <param name="errorCode">错误码</param>        /// <param name="exceptionType">具体异常类型</param>        /// <param name="args">String.Format 参数</param>        /// <returns>异常实例</returns>        public static Exception Oh(object errorCode, Type exceptionType, params object[] args);    }}

7.6 最佳实践 🤗

Furion 框架中,提供了非常灵活且规范化的友好异常处理方式,通过这个方式可以方便管理异常状态码、异常信息及异常本地化。

7.6.1 创建异常信息类型

实现自定义异常信息类型必须遵循以下配置:

  • 类型必须是公开且是 Enum 枚举类型
  • 枚举类型必须贴有 [ErrorCodeType] 特性
  • 枚举中每一项必须贴有 [ErrorCodeItemMetadata] 特性
using Furion.FriendlyException;namespace Furion.Application{    [ErrorCodeType]    public enum ErrorCodes    {        [ErrorCodeItemMetadata("{0} 不能小于 {1}")]        z1000,        [ErrorCodeItemMetadata("数据不存在")]        x1000,        [ErrorCodeItemMetadata("{0} 发现 {1} 个异常", "百小僧", 2)]        x1001,        [ErrorCodeItemMetadata("服务器运行异常", ErrorCode = "Error")]        SERVER_ERROR    }}
info

Furion 框架提供了 [ErrorCodeType] 特性和 IErrorCodeTypeProvider 提供器接口来提供异常信息扫描,这里用的是 [ErrorCodeType] 特性类。

7.6.2 关于 [ErrorCodeItemMetadata]

Furion 框架提供了[ErrorCodeItemMetadata] 特性用来标识枚举字段异常元数据,该特性支持传入 消息内容格式化参数。最终会使用 String.Format(消息内容,格式化参数) 进行格式化。

如果消息内容中包含格式化占位符但未指定格式化参数,那么会查找异常所在方法是否贴有 [IfException] 特性且含有格式化参数,接着就会查找 Oops.Oh 中指定的 格式化参数

7.6.3 静态异常类使用

using Furion.DynamicApiController;using Furion.FriendlyException;namespace Furion.Application{    public class FurionAppService : IDynamicApiController    {        public int Get(int id)        {            if (id < 3)            {                throw Oops.Oh(ErrorCodes.z1000, id, 3);            }            return id;        }    }}

如下图所示:

7.6.4 异常方法重试

调整说明

v2.17.0+ 版本下面方法请使用 Retry.Invoke() 替代。

Oops.Retry(() => {    // Do.....}, 3, 1000);// 带返回值var value = Oops.Retry<int>(() => {    // Do.....}, 3, 1000);// 只有特定异常才监听Oops.Retry(() => {}, 3, 1000, typeof(ArgumentNullException));

7.6.5 更多例子

throw Oops.Oh(1000);throw Oops.Oh(ErrorCodes.x1000);throw Oops.Oh("哈哈哈哈");throw Oops.Oh(errorCode: "x1001");throw Oops.Oh(1000, typeof(Exception));throw Oops.Oh(1000).StatusCode(400);    // 设置错误码throw Oops.Oh(1000).WithData(new Model {});    // 设置额外数据throw Oops.Bah("用户名或密码错误"); // 抛出业务异常,状态码为 400throw Oops.Bah(1000);

7.7 多个异常信息类型

using Furion.FriendlyException;namespace Furion.Application{    [ErrorCodeType]    public enum ErrorCodes    {        [ErrorCodeItemMetadata("{0} 不能小于 {1}")]        z1000,        [ErrorCodeItemMetadata("数据不存在")]        x1000,        [ErrorCodeItemMetadata("{0} 发现 {1} 个异常", "百小僧", 2)]        x1001,        [ErrorCodeItemMetadata("服务器运行异常", ErrorCode = "Error")]        SERVER_ERROR    }    [ErrorCodeType]    public enum UserErrorCodes    {        [ErrorCodeItemMetadata("用户数据不存在")]        u1000,        [ErrorCodeItemMetadata("其他异常")]        u1001    }}
特别注意

多个异常静态类中也必须保证常量值唯一性,不可重复。

7.8 IErrorCodeTypeProvider 提供器

Furion 框架中,还提供了 IErrorCodeTypeProvider 异常消息提供器接口,方便在不能贴 [ErrorCodeType] 特性情况下使用:

using Furion.FriendlyException;using System;namespace Furion.Application{    public class CustomErrorCodeTypeProvider : IErrorCodeTypeProvider    {        public Type[] Definitions => new[] {            typeof(ErrorCodes),            typeof(ErrorCodes2)        };    }}

启用 IErrorCodeTypeProvider 提供器:

Furion.Web.Core\FurWebCoreStartup.cs
using Microsoft.Extensions.DependencyInjection;namespace Furion.Web.Core{    [AppStartup(800)]    public sealed class FurWebCoreStartup : AppStartup    {        public void ConfigureServices(IServiceCollection services)        {            services.AddControllers()                    .AddFriendlyException<CustomErrorCodeTypeProvider>();        }    }}
小知识

只有使用 IErrorCodeTypeProvider 方式才需使用泛型方式注册。通过上面的方式注册可以同时支持 IErrorCodeTypeProvider[ErrorCodeType] 方式。

7.9 appsetting.json 中配置

Furion 框架还提供了非常灵活的配置文件配置异常,通过这种方式可以实现异常信息后期配置,也就是无需在开发阶段预先定义。

Furion.Web.Entry/appsettings.json
{  "ErrorCodeMessageSettings": {    "Definitions": [      ["5000", "{0} 不能小于 {1}"],      ["5001", "我叫 {0} 名字", "百小僧"],      ["5002", "Oops! 出错了"]    ]  }}

Definitions 类型为二维数组,二维数组中的每一个数组第一个参数为 ErrorCode 也就是错误码,第二个参数为 ErrorMessage 消息内容,剩余参数作为 ErrorMessage 的格式化参数。

使用示例

using Furion.DynamicApiController;using Furion.FriendlyException;namespace Furion.Application{    public class FurionAppService : IDynamicApiController    {        public int Get(int id)        {            if (id < 3)            {                throw Oops.Oh(5000, id, 3); // 可以将 5000作为常量配置起来            }            return id;        }    }}
小知识

[ErrorCodeType]IErrorCodeTypeProviderappsettings.json 可以同时使用。

7.10 [IfException] 使用

Furion 框架提供了 [IfException] 特性可以覆盖默认消息配置。也就是覆盖 异常消息类型appsettings.json 中的配置。

特别注意

[IfException] 只能贴在方法上,支持多个。

7.10.1 使用示例

  • 异常消息类定义
[ErrorCodeType]public enum ErrorCodes{   [ErrorCodeItemMetadata("{0} 不能小于 {1}")]   z1000}
  • 覆盖默认配置
using Furion.DynamicApiController;using Furion.FriendlyException;namespace Furion.Application{    public class FurionAppService : IDynamicApiController    {        [IfException(ErrorCodes.z1000, ErrorMessage = "我覆盖了默认的:{0} 不能小于 {1}")]        public int Get(int id)        {            if (id < 3)            {                throw Oops.Oh(ErrorCodes.z1000, id, 3);            }            return id;        }    }}

如下图所示:

7.10.2 更多例子

using Furion.DynamicApiController;using Furion.FriendlyException;namespace Furion.Application{    public class FurionAppService : IDynamicApiController    {        [IfException(typeof(ExceptionType), ErrorMessage = "特定异常类型全局拦截")]        [IfException(ErrorMessage = "全局异常拦截")]        [IfException(ErrorCodes.z1000, ErrorMessage = "我覆盖了默认的:{0} 不能小于 {1}")]        [IfException(ErrorCodes.x1001, "格式化参数1", "格式化参数2", ErrorMessage = "我覆盖了默认的:{0} 不能小于 {1}")]        [IfException(ErrorCodes.x1000, "格式化参数1", "格式化参数2")]        [IfException(ErrorCodes.SERVER_ERROR, "格式化参数1", "格式化参数2")]        public int Get(int id)        {            if (id < 3)            {                throw Oops.Oh(ErrorCodes.z1000, id, 3);            }            return id;        }    }}
格式化流程

如果消息内容中包含格式化占位符但未指定格式化参数,那么会查找异常所在方法是否贴有 [IfException] 特性且含有格式化参数,接着就会查找 Oops.Oh 中指定的 格式化参数

7.11 异常消息优先级

[ErrorCodeItemMetadata] -> appsettings.json -> [IfException](低 -> 高)

  • [IfException] 会覆盖 appsettings.json 定义的状态码消息。
  • appsettings.json 会覆盖 [ErrorCodeItemMetadata] 定义的消息。

7.12 多语言支持

参见 【全球化和本地化(多语言)】 章节

7.13 规范化结果异常处理

查看规范化结果文档

如需自定义规范化结果可查阅 【6.7 统一返回值模型

7.14 全局异常处理提供器

通常我们需要在异常捕获的时候写日志,这时候就需要使用到 IGlobalExceptionHandler 异常定义处理程序,如:

using Furion.DependencyInjection;using Furion.FriendlyException;using Microsoft.AspNetCore.Mvc.Filters;using System.Threading.Tasks;namespace Furion.Application{    public class LogExceptionHandler : IGlobalExceptionHandler, ISingleton    {        public Task OnExceptionAsync(ExceptionContext context)        {            // 写日志            return Task.CompletedTask;        }    }}

7.15 FriendlyExceptionSettings 配置

  • HideErrorCode:隐藏错误码,bool 类型,默认 false
  • DefaultErrorCode:默认错误码,string 类型
  • DefaultErrorMessage:默认错误消息,string 类型
  • ThrowBah:是否将 Oops.Oh 默认抛出为业务异常,bool 类型,默认 false,设置 true 之后 Oops.Oh 默认进入 OnValidateFailed 处理,而不是 OnException
  • LogError:是否输出异常日志,bool 类型,默认 true

配置示例

{  "FriendlyExceptionSettings": {    "DefaultErrorMessage": "系统异常,请联系管理员"  }}

7.16 BadPageResult 错误页

版本说明

以下内容仅限 Furion 3.6.1 + 版本使用。

Furion 在该版本之后内置了 BadPageResult 错误结果类型,该类型派生自 IActionResult,如需返回只需要在 Action 中返回即可。

using Furion.FriendlyException;public IActionResult Add(Person person){    if(!ModelState.IsValid)    {        return new BadPageResult();    }}
  • BadPageResult 更多配置
    • 构造函数 statusCode:状态码,int 类,默认 400
    • Title:页面标题,string 类型,默认 ModelState Invalid
    • Description:页面描述,string 类型,默认 User data verification failed. Please input it correctly.
    • Code:详细错误代码,string 类型,支持 代码,默认空字符串
    • CodeLang:详细错误代码语言,string 类型,默认 json
    • Base64Icon:页面图标,string 类型,带默认值,自定义必须是 base64 格式图标

7.17 反馈与建议

与我们交流

给 Furion 提 Issue

演练场