友好异常处理 #
异常一般是指运行期(此处特指 Exception
类)会发生的导致程序意外中止的问题,是一种对问题的描述后的封装对象。
在过去开发中,通常异常由系统运行时出错抛出,但现在的开发过程中,我们应在程序开发中合理的抛出异常,比如更新一条不存在的实体,或查询一个不存在的数据等等。
处理异常方式 #
- 不处理,直接中断程序执行(不推荐)
- 通过
try catch finally
处理(不推荐) - 全局统一处理,并记录异常信息**(推荐)**
- 异常注解方式处理,支持本地化 (推荐)
什么是友好异常处理 #
非友好异常处理 #
在了解友好异常处理之前可以看看非友好异常处理:
- 对终端用户抛出
500状态码
堆栈信息 - 大量的
try catch
代码,污染正常业务逻辑 - 没有规范化的异常状态码和异常消息管理
- 没有异常日志收集记录
- 不支持异常消息本地化处理
- 不支持异常策略,失败后程序立即终止
- 不支持分布式事务 CAP
- 不支持异常传播
- 返回的异常格式杂乱
友好异常处理 #
- 对终端用户提示友好
- 对后端开发人员提供详细的异常堆栈
- 不干扰正常业务逻辑代码,如 没有
try catch
代码 - 支持异常状态码多方设置
- 支持异常消息本地化
- 异常信息统一配置管理
- 支持异常策略,如重试
- 支持异常日志收集记录
- 支持 CAP 分布式事务关联
- 支持内部异常外部传播
- 支持返回统一的异常格式数据
友好异常处理使用示例 #
mall3s
框架提供了非常灵活的友好异常处理方式。
小提示
.AddFriendlyException()
默认已经集成在 AddInject()
中了,无需再次注册。也就是 7.4.1
章节可不配置。
注册友好异常服务 #
Mall3s.Web.Core\WebCoreStartup.cs
using Microsoft.Extensions.DependencyInjection;
namespace Mall3s.Web.Core
{
[AppStartup(800)]
public sealed class WebCoreStartup : AppStartup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddFriendlyException();
}
}
}
特别注意 .AddFriendlyException() 需在services.AddControllers() 之后注册。
两个例子 #
简单抛个异常
using Mall3s.DynamicApiController;
using Mall3s.FriendlyException;
namespace Mall3s.Application
{
public class Mall3sAppService : IDynamicApiController {
public int Get(int id)
{
if (id < 3)
{
throw Mall3sException.Oh($"{id} 不能小于3");
}
return id;
}
}
}
如下图所示:
抛出特定类型异常
using Mall3s.DynamicApiController;
using Mall3s.FriendlyException;
using System;
namespace Mall3s.Application
{
public class Mall3sAppService : IDynamicApiController {
public int Get(int id)
{
if (id < 3)
{
throw Mall3sException.Oh($"{id} 不能小于3。
typeof(InvalidOperationException));
}
return id;
}
}
}
关于Mall3sException.Oh #
通过上面的例子可以看出,Mall3sException.Oh(errorMessage) 可以结合throw 抛出异常。
对于熟悉C## 的人员来说,throw 后面只能Exception 实例。
Mall3sException.Oh(...) 方法返回正是Exception 实例。
Mall3sException.Oh 重载方法 #
using System;
namespace Mall3s.FriendlyException
{
public static class Mall3sException
{
/// <summary>
/// 抛出字符串异常
/// </summary>
/// <param name="errorMessage"> 异常消息</param>
/// <param name="args">Stri ng. 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">Stri ng. Format 参数</param>
/// <returns> 异常实例</returns〉public static Exception Oh(string errorMessage, Type exceptionType,
params object[] args);
/// <summary>
/// 抛出错误码异常
/// </summary>
/// <param name="errorcode"> 错误码</param>
/// <param name="args">Stri ng. 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">Stri ng. Format 参数</param>
/// <returns> 异常实例</returns〉
public static Exception Oh(object errorcode, Type exceptionType, params object[] args);
}
}
最佳实践 #
在Mall3s 框架中,提供了非常灵活且规范化的友好异常处理方式,通过这个方式可以方便管理异常状态码、异常信息及异常本地化。
创建异常信息类型 #
实现自定义异常信息类型必须遵循以下配置:
- 类型必须是公开且是
Enum
枚举类型 - 枚举类型必须贴有
[ErrorCodeType]
特性 - 枚举中每一项必须贴有
[ErrorCodeItemMetadata]
特性
using Mall3s.FriendlyException;
namespace Mall3s.Application
{
[ErrorcodeType]
public enum Errorcodes
{
[ErrorcodeItemMetadata("{0} 不能小于{1}")]
z1000,
[ErrorcodeltemMetadata("数据不存在")]
x1000,
[ErrorcodeItemMetadata("{0} 发现{1} 个异常", "小明", 2)]
x1001,
[ErrorcodeltemMetadata("服务器运行异常",Errorcode = "Error")]
SERVER_ERROR
TIP
mall3s
框架提供了 [ErrorCodeType]
特性和 IErrorCodeTypeProvider
提供器接口来提供异常信息扫描,这里用的是 [ErrorCodeType]
特性类。
关于[ErrorCodeItemMetadata] #
Mall3s 框架提供了[ErrorCodeItemMetadata]
特性用来标识枚举字段异常元数据,该特性支持传入 消息内容
和 格式化参数
。最终会使用 String.Format(消息内容,格式化参数)
进行格式化。
如果消息内容中包含格式化占位符
但未指定格式化参数
,那么会查找异常所在方法是否贴有 [IfException]
特性且含有格式化参数,接着就会查找 Oops.Oh
中指定的 格式化参数
。
静态异常类使用 #
using Mall3s.DynamicApiController;
using Mall3s.FriendlyException;
namespace Mall3s.Application
{
public class Mall3sAppService : IDynamicApiController
{
public int Get(int id)
{
if (id < 3)
{
throw Mall3sException.Oh(ErrorCodes.z1000, id, 3);
}
return id;
}
}
}
如下图所示:
更多例子 #
throw Mall3sException.oh(1000);
throw Mall3sException.oh(ErrorCodes.x1000);
throw Mall3sException.oh("哈哈哈哈");
throw Mall3sException.oh(errorCode: "x1001");
throw Mall3sException.oh(1000, typeof(Exception));
throw Mall3sException.oh(1000).StatusCode(400); // 设置错误码
throw Mall3sException.Bah("用户名或密码错误");//抛出业务异常,状态码为400 throw Mall3sException.Bah(1000);
多个异常信息类型 #
using Mall3s.FriendlyException;
namespace Mall3s.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
}
}
特别注意 多个异常静态类中也必须保证常量值唯一性,不可重复。
IErrorCodeTypeProvider 提供器 #
在Mall3s 框架中,还提供了IErrorCodeTypeProvider 异常消息提供器接口,方便在不能贴[ErrorCodeType] 特性情况下使用:
using Mall3s.FriendlyException;
using System;
namespace Mall3s.Application
{
public class CustomErrorCodeTypeProvider : IErrorCodeTypeProvider
{
public Type[] Definitions => new[] {
typeof(ErrorCodes),
typeof(ErrorCodes2)
};
}
}
启用IErrorCodeTypeProvider 提供器: Mall3s.Web.Core\WebCoreStartup.cs
using Microsoft.Extensions.DependencyInjection;
namespace Mall3s.Web.Core
{
[AppStartup(800)]
public sealed class WebCoreStartup : AppStartup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers() .AddFriendlyException<CustomErrorCodeTypeProvider>();
}
}
}
小知识 只有使用IErrorCodeTypeProvider 方式才需使用泛型方式注册。通过上面的方式注册可以同时支持IErrorCodeTypeProvider 和[ErrorCodeType] 方式。
appsetting.json 中配置 #
Mall3s 框架还提供了非常灵活的配置文件配置异常,通过这种方式可以实现异常信息后期配置,也就是无需在开发阶段预先定义。 Mall3s.Web.Entry/appsettings.json
{
"ErrorCodeMessageSettings": {
"Definitions": [
[5000, "{0} 不能小于{1}"],
[5001, " 我叫{0} 名字", " 引迈"],
[5002, " 出错了"]
]
}
}
Definitions 类型为二维数组,二维数组中的每一个数组第一个参数为ErrorCode 也就是错误码,第二个参数为ErrorMessage 消息内容,剩余参数作为ErrorMessage 的格式化参数。 使用示例
using Mall3s.DynamicApiController;
using Mall3s.FriendlyException;
namespace Mall3s.Application
{
public class Mall3sAppService : IDynamicApiController
{
public int Get(int id)
{
if (id < 3)
{
throw Mall3sException.Oh(5000, id, 3); // 可以将5000作为常量配置起来
} return id;
}
}
}
小知识 [ErrorCodeType] 和IErrorCodeTypeProvider 和appsettings.json 可以同时使用。
[IfException] 使用 #
Mall3s 框架提供了 [IfException]
特性可以覆盖默认消息配置。也就是覆盖 异常消息类型
和 appsettings.json
中的配置。
:::caution 特别注意
[IfException]
只能贴在方法上,支持多个。
:::
使用示例 #
异常消息类定义
[ErrorCodeType]
public static class ErrorCodes
{
[ErrorCodeItemMetadata("{0} 不能小于{1}")] z1000
}
覆盖默认配置
using Mall3s.DynamicApiController;
using Mall3s.FriendlyException;
namespace Mall3s.Application
{
public class Mall3sAppService : IDynamicApiController
{
[IfException(ErrorCodes.z1000, ErrorMessage = "我覆盖了默认的:{0} 不能小于
{1}")]
public int Get(int id)
{
if (id < 3)
{
throw Mall3sException.Oh(ErrorCodes.z1000, id, 3);
}
return id;
}
}
}
如下图所示:
更多例子 #
using Mall3s.DynamicApiController;
using Mall3s.FriendlyException;
namespace Mall3s.Application
{
public class Mall3sAppService : 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 Mall3sException.Oh(ErrorCodes.z1000, id, 3);
} return id;
}
}
}
格式化流程
如果消息内容中包含格式化占位符
但未指定格式化参数
,那么会查找异常所在方法是否贴有 [IfException]
特性且含有格式化参数,接着就会查找 Oops.Oh
中指定的 格式化参数
。
异常消息优先级 #
[ErrorCodeItemMetadata]
-> appsettings.json
-> [IfException]
。(低 -> 高)
[IfException]
会覆盖appsettings.json
定义的状态码消息。appsettings.json
会覆盖[ErrorCodeItemMetadata]
定义的消息。
多语言支持 #
参见【全球化和本地化(多语言)】章节
规范化结果异常处理 #
using Mall3s.DataValidation;
using Mall3s.Dependency;
using Mall3s.UnifyResult.Internal;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Threading.Tasks;
namespace Mall3s.UnifyResult
{
/// <summary>
/// RESTful 风格返回值
/// </summary>
[SuppressSniffer, UnifyModel(typeof(RESTfulResult<>))] public class RESTfulResultProvider : IUnifyResultProvider {
/// <summary>
/// 异常返回值
/// </summary>
/// <param name="context"></param>
/// <param name="metadata"></param>
/// <returns></returns>
public IActionResult OnException(ExceptionContext context, ExceptionMetadata metadata)
{
return new JsonResult(RESTfulResult(metadata.StatusCode, errors: metadata.Errors));
}
/// <summary>
/// 成功返回值
/// </summary>
/// <param name="context"></param>
/// <param name="data"></param>
/// <returns></returns>
public IActionResult OnSucceeded(ActionExecutedContext context, object data)
{
return new JsonResult(RESTfulResult(StatusCodes.Status200OK, true, data));
}
/// <summary>
/// 验证失败返回值
/// </summary>
/// <param name="context"></param>
/// <param name="metadata"></param>
/// <returns></returns>
public IActionResult OnValidateFailed(ActionExecutingContext context, ValidationMetadata metadata)
{
return new JsonResult(RESTfulResult(StatusCodes.Status400BadRequest, errors: metadata.ValidationResult));
}
/// <summary>
/// 特定状态码返回值
/// </summary>
/// <param name="context"></param>
/// <param name="statusCode"></param>
/// <param name="unifyResultSettings"></param>
/// <returns></returns>
public async Task OnResponseStatusCodes(HttpContext context, int statusCode, UnifyResultSettingsOptions unifyResultSettings)
{
// 设置响应状态码
unifyContext.SetResponseStatusCodes(context, statusCode, unifyResultSettings);
switch (statusCode)
{
// 处理401 状态码
case StatusCodes.Status401unauthorized:
await
context.Response.WriteAsJsonAsync(RESTfulResult(statusCode, errors: "401 unauthorized")
, App.GetOptions<JsonOptions>()?.JsonSerializerOptions); break;
// 处理403 状态码
case StatusCodes.Status403Forbidden:
await
context.Response.WriteAsJsonAsync(RESTfulResult(statusCode, errors: "403 Forbidden")
, App.GetOptions<JsonOptions>()?.JsonSerializerOptions); break;
default: break;
}
}
/// <summary>
/// 返回RESTful 风格结果集
/// </summary>
/// <param name="statusCode"></param>
/// <param name="succeeded"></param>
/// <param name="data"></param>
/// <param name="errors"></param>
/// <returns></returns>
private static RESTfulResult<object> RESTfulResult(int statusCode, bool succeeded = default, object data = default, object errors = default)
{
return new RESTfulResult<object>
{
StatusCode = statusCode,
Succeeded = succeeded,
Data = data,
Errors = errors,
Extras = unifyContext.Take(),
Timestamp = DateTimeOffset.utcNow.TounixTimeMilliseconds()
};
}
}
}
之后在Startup.cs 中注册即可:
services.AddControllers() .AddInjectWithunifyResult<RESTfulResultProvider>();
全局异常处理提供器 #
通常我们需要在异常捕获的时候写日志,这时候就需要使用到IGlobalExceptionHandler 异常定义处理程序,如:
using Mall3s.DependencyInjection;
using Mall3s.FriendlyException;
using Microsoft.AspNetcore.Mvc.Filters;
using System.Threading.Tasks;
namespace Mall3s.Application
{
public class LogExceptionHandler : IGlobalExceptionHandler,ISingleton
{
public Task OnExceptionAsync(Exceptioncontext context)
{
// 写日志
return Task.completedTask;
}
}
}
FriendlyExceptionSettings 配置 #
HideErrorcode :隐藏错误码,bool 类型,默认false DefaultErrorcode :默认错误码,string 类型DefaultErrorMessage :默认错误消息,string 类型 配置示例
{
"FriendlyExceptionSettings": { "DefaultErrorMessage": "系统异常,请联系管理员"
}
}