友好异常处理 #

异常一般是指运行期(此处特指 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;
        }
    }
}

如下图所示: image.png

抛出特定类型异常

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} 不能小于3typeof(InvalidOperationException));
            } 
            return id;
        }
    }
}
 

image.png

关于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;
        }
    }
}

如下图所示: image.png

更多例子 #

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": "系统异常,请联系管理员"
    }
}
上次更新: 3/10/2023, 5:33:48 PM