ABP Framework-ExceptionHanding源码解析
目录
https://abp.io/docs/6.0/Exception-Handling
Version
6.0.3
Package
Volo.Abp.ExceptionHandling
AspNetCore异常体系
Abp的异常体系基于Asp.Net Core的异常体系,后者简要概括分为两部分,第一个是全局异常处理即ExceptionHandlingMiddleware,还有一个是Mvc middleware中的ExceptionFilter,这部分可以参考文档:
https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/error-handling?view=aspnetcore-8.0
https://learn.microsoft.com/zh-cn/aspnet/core/web-api/handle-errors?view=aspnetcore-8.0
https://source.dot.net/#BasicWebSite/Filters/TestExceptionFilter.cs,9
Abp异常体系
Abp异常处理
Abp在AspNetCore的基础上增加了与之对应的类,一个是AbpExceptionHandlingMiddleware.cs和AbpExceptionFilter.cs,其异常处理思路是一致的,当Controller/Action中出现异常时,AbpExceptionFilter先处理,如下代码所示
protected virtual async Task HandleAndWrapException(ExceptionContext context)
{
    var exceptionHandlingOptions = context.GetRequiredService<IOptions<AbpExceptionHandlingOptions>>().Value;
    //异常信息转换为错误信息
    var exceptionToErrorInfoConverter = context.GetRequiredService<IExceptionToErrorInfoConverter>();
    var remoteServiceErrorInfo = exceptionToErrorInfoConverter.Convert(context.Exception, options =>
    {
        options.SendExceptionsDetailsToClients = exceptionHandlingOptions.SendExceptionsDetailsToClients;
        options.SendStackTraceToClients = exceptionHandlingOptions.SendStackTraceToClients;
    });
    //记录异常信息日志
    var logLevel = context.Exception.GetLogLevel();
    var remoteServiceErrorInfoBuilder = new StringBuilder();
    remoteServiceErrorInfoBuilder.AppendLine($"---------- {nameof(RemoteServiceErrorInfo)} ----------");
    remoteServiceErrorInfoBuilder.AppendLine(context.GetRequiredService<IJsonSerializer>().Serialize(remoteServiceErrorInfo, indented: true));
    var logger = context.GetService<ILogger<AbpExceptionFilter>>(NullLogger<AbpExceptionFilter>.Instance);
    logger.LogWithLevel(logLevel, remoteServiceErrorInfoBuilder.ToString());
    logger.LogException(context.Exception, logLevel);
    //异常信息消息通知
    await context.GetRequiredService<IExceptionNotifier>().NotifyAsync(new ExceptionNotificationContext(context.Exception));
    //异常错误码转换
    if (context.Exception is AbpAuthorizationException)
    {
        await context.HttpContext.RequestServices.GetRequiredService<IAbpAuthorizationExceptionHandler>()
            .HandleAsync(context.Exception.As<AbpAuthorizationException>(), context.HttpContext);
    }
    else
    {
        context.HttpContext.Response.Headers.Add(AbpHttpConsts.AbpErrorFormat, "true");
        context.HttpContext.Response.StatusCode = (int)context
            .GetRequiredService<IHttpExceptionStatusCodeFinder>()
            .GetStatusCode(context.HttpContext, context.Exception);
        context.Result = new ObjectResult(new RemoteServiceErrorResponse(remoteServiceErrorInfo));
    }
    //标识异常被处理
    context.Exception = null; //Handled!
}
其中核心类为IExceptionToErrorInfoConverter和IHttpExceptionStatusCodeFinder,通过对应实现来将不同类型的异常转换成对应的错误信息以及响应的状态码,如Abp的UserFriendlyException便是在这其中处理从而有403的状态码。
AbpExceptionHandlingOptions
用于配置是否将详细异常信息及堆栈信息暴露给请求方。
Configure<AbpExceptionHandlingOptions>(options =>
{
    options.SendExceptionsDetailsToClients = true;
    options.SendStackTraceToClients = false;
});
实践过程中,直接都false了,具体详情错误从日志中去获取,不从返回中去判断错误源头。
IExceptionToErrorInfoConverter
可快速概括下该部分源码,该接口下Abp提供了实现类DefaultExceptionToErrorInfoConverter.cs,在这其中核心方法时CreateErrorInfoWithoutCode,根据是否是否暴露异常详情与堆栈的开关设置AbpExceptionHandlingOptions类,返回内容也有所不同。当不打算暴露异常详情给外部,则按照异常类型转换到错误消息类中,否则会返回包含更详细的错误信息与堆栈信息。
protected virtual RemoteServiceErrorInfo CreateErrorInfoWithoutCode(Exception exception, AbpExceptionHandlingOptions options)
{
    if (options.SendExceptionsDetailsToClients)
    {
        return CreateDetailedErrorInfoFromException(exception, options.SendStackTraceToClients);
    }
    exception = TryToGetActualException(exception);
    if (exception is AbpRemoteCallException remoteCallException && remoteCallException.Error != null)
    {
        return remoteCallException.Error;
    }
    if (exception is AbpDbConcurrencyException)
    {
        return new RemoteServiceErrorInfo(L["AbpDbConcurrencyErrorMessage"]);
    }
    if (exception is EntityNotFoundException)
    {
        return CreateEntityNotFoundError(exception as EntityNotFoundException);
    }
    var errorInfo = new RemoteServiceErrorInfo();
    if (exception is IUserFriendlyException || exception is AbpRemoteCallException)
    {
        errorInfo.Message = exception.Message;
        errorInfo.Details = (exception as IHasErrorDetails)?.Details;
    }
    if (exception is IHasValidationErrors)
    {
        if (errorInfo.Message.IsNullOrEmpty())
        {
            errorInfo.Message = L["ValidationErrorMessage"];
        }
        if (errorInfo.Details.IsNullOrEmpty())
        {
            errorInfo.Details = GetValidationErrorNarrative(exception as IHasValidationErrors);
        }
        errorInfo.ValidationErrors = GetValidationErrorInfos(exception as IHasValidationErrors);
    }
    TryToLocalizeExceptionMessage(exception, errorInfo);
    if (errorInfo.Message.IsNullOrEmpty())
    {
        errorInfo.Message = L["InternalServerErrorMessage"];
    }
    errorInfo.Data = exception.Data;
    return errorInfo;
}
IHttpExceptionStatusCodeFinder
在该类中,按照异常类型返回对应错误码,当无法处理其错误类型,统一返回成500,因此,有些错误发生时,可能得到的预期错误码与理想的不一致,可重写其类,来实现想要的错误类型转换其对应错误码。在默认实现DefaultHttpExceptionStatusCodeFinder.cs中,Abp将自身常用的异常类及其对应状态码进行了对应。
public virtual HttpStatusCode GetStatusCode(HttpContext httpContext, Exception exception)
{
    if (exception is IHasHttpStatusCode exceptionWithHttpStatusCode &&
        exceptionWithHttpStatusCode.HttpStatusCode > 0)
    {
        return (HttpStatusCode)exceptionWithHttpStatusCode.HttpStatusCode;
    }
    if (exception is IHasErrorCode exceptionWithErrorCode &&
        !exceptionWithErrorCode.Code.IsNullOrWhiteSpace())
    {
        if (Options.ErrorCodeToHttpStatusCodeMappings.TryGetValue(exceptionWithErrorCode.Code, out var status))
        {
            return status;
        }
    }
    if (exception is AbpAuthorizationException)
    {
        return httpContext.User.Identity.IsAuthenticated
            ? HttpStatusCode.Forbidden
            : HttpStatusCode.Unauthorized;
    }
    //TODO: Handle SecurityException..?
    if (exception is AbpValidationException)
    {
        return HttpStatusCode.BadRequest;
    }
    if (exception is EntityNotFoundException)
    {
        return HttpStatusCode.NotFound;
    }
    if (exception is AbpDbConcurrencyException)
    {
        return HttpStatusCode.Conflict;
    }
    if (exception is NotImplementedException)
    {
        return HttpStatusCode.NotImplemented;
    }
    if (exception is IBusinessException)
    {
        return HttpStatusCode.Forbidden;
    }
    return HttpStatusCode.InternalServerError;
}
Abp异常处理注册逻辑
AbpExceptionHandlingMiddleware需要手动注册,在模板中没有自动注册。仅在测试项目中存在注册记录,其位置可以参考如下
AbpApplicationBuilderExtensions.cs
  UseUnitOfWork()
    UseMiddleware<AbpUnitOfWorkMiddleware>()
AbpExceptionFilter在Abp源码中默认注册好了,另外,在此处Filter也注册了其他许多如审计,参数验证,工作单元等Filter,其位置可以参考如下
AbpAspNetCoreMvcModule.cs
  ConfigureServices()
    Configure<MvcOptions>(mvcOptions)
      mvcOptions.AddAbp()     //AbpMvcOptionsExtensions.cs
        AddActionFilters()
          options.Filters.AddService(typeof(AbpExceptionFilter));
Abp异常分类
 核心异常类型与描述如下
核心异常类型与描述如下
| 异常类型 | 描述 | 
|---|---|
| AbpException | Abp 框架定义的基本异常类型,Abp 所有内部定义的异常类型都继承自本类。 | 
| AbpInitializationException | Abp 框架初始化时出现错误所抛出的异常。 | 
| AbpDbConcurrencyException | 当 EF Core 执行数据库操作时产生了 DbUpdateConcurrencyException 异常 | 
| AbpValidationException | 用户调用接口时,输入的DTO 参数有误会抛出本异常。 | 
| BackgroundJobException | 后台作业执行过程中产生的异常。 | 
| EntityNotFoundException | 当仓储执行 Get 操作时,实体未找到引发本异常。 | 
| UserFriendlyException | 如果用户需要将异常信息发送给前端,请抛出本异常。 | 
| AbpRemoteCallException | 远程调用一场,当使用 Abp 提供的 AbpWebApiClient 产生问题的时候 | 
Abp异常返回
最为常见的返回错误格式如下,包含了异常转错误信息后的属性,前端可根据其结构和状态码从错误信息中获取到具体错误描述。
{
  "error": {
    "code": null,
    "message": "",
    "details": "",
    "data": "",
    "validationErrors": []
  }
}
扩展
如果想要拦截一些特定的异常类型,或者处理异常状态码,可以进行扩展
- 继承DefaultExceptionToErrorInfoConverter,实现自定义异常转换错误信息类。在其中重写CreateErrorInfoWithoutCode方法。可参考如下重写,region区域内可以对特定的异常转换,整体代码上还是采用ABP的异常转换逻辑。
protected override RemoteServiceErrorInfo CreateErrorInfoWithoutCode(Exception exception, AbpExceptionHandlingOptions options)
{
    var errorInfo = base.CreateErrorInfoWithoutCode(exception, options);
    if (options.SendExceptionsDetailsToClients)
    {
        return errorInfo;
    }
    exception = TryToGetActualException(exception);
    #region Exception Extend
    if (exception is IHasValidationErrors)
    {
        errorInfo.Message = errorInfo.ValidationErrors?.FirstOrDefault()?.Message
                ?? exception.Message
                ?? L["ValidationErrorMessage"];
    }
    if (exception is ValidationException)
    {
        errorInfo.Message = exception.Message;
    }
    TryToLocalizeExceptionMessage(exception, errorInfo);
    if (errorInfo.Message.IsNullOrEmpty())
    {
        errorInfo.Message = L["InternalServerErrorMessage"];
    }
    #endregion
    return errorInfo;
}
- 继承DefaultHttpExceptionStatusCodeFinder,实现自定义异常状态码类,重写GetStatusCode方法,在其中根据特定异常返回期望的状态码。可参考如下重写,region区域内可以对特定异常返回状态码,整体代码上还是采用ABP的异常状态码转换逻辑。
public override HttpStatusCode GetStatusCode(HttpContext httpContext, Exception exception)
{
    var statusCode = base.GetStatusCode(httpContext, exception);
    #region Exception Extend
    if (exception is ValidationException)
    {
        statusCode = HttpStatusCode.BadRequest;
    }
    if (exception is DbUpdateConcurrencyException)
    {
        statusCode = HttpStatusCode.Conflict;
    }
    #endregion
    return statusCode;
}
参考
https://www.cnblogs.com/myzony/p/9460021.html
https://www.cnblogs.com/1zhk/p/5538983.html
2024-10-25,望技术有成后能回来看见自己的脚步。