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 注册友好异常服务
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 }}
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.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
提供器:
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
框架还提供了非常灵活的配置文件配置异常,通过这种方式可以实现异常信息后期配置,也就是无需在开发阶段预先定义。
{ "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]
和 IErrorCodeTypeProvider
和 appsettings.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
配置示例
{ "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。