项目

异常处理

ABP框架提供内置的基础设施和标准模型来处理异常。

  • 自动处理所有异常,并为API/AJAX请求向客户端发送标准格式化错误消息
  • 自动隐藏内部基础设施错误并返回标准错误消息。
  • 提供简单且可配置的方式来本地化异常消息。
  • 自动将标准异常映射到HTTP状态码,并提供可配置选项以映射自定义异常。

自动异常处理

当满足以下任一条件时,AbpExceptionFilter会处理异常:

  • 异常由返回对象结果(非视图结果)的控制器操作抛出。
  • 请求是AJAX请求(X-Requested-With HTTP头值为XMLHttpRequest)。
  • 客户端明确接受application/json内容类型(通过accept HTTP头)。

如果异常被处理,它会自动记录日志,并向客户端返回格式化的JSON消息

错误消息格式

错误消息是RemoteServiceErrorResponse类的实例。最简单的错误JSON包含一个message属性,如下所示:

{
  "error": {
    "message": "该主题已锁定,无法添加新消息"
  }
}

根据发生的异常,还可以填充可选字段

错误代码

错误代码是一个可选的、唯一的字符串值。抛出的Exception应实现IHasErrorCode接口以填充此字段。示例JSON值:

{
  "error": {
    "code": "App:010042",
    "message": "该主题已锁定,无法添加新消息"
  }
}

错误代码还可用于本地化异常和自定义HTTP状态码(参见下文相关部分)。

错误详情

错误详情是JSON错误消息的可选字段。抛出的Exception应实现IHasErrorDetails接口以填充此字段。示例JSON值:

{
  "error": {
    "code": "App:010042",
    "message": "该主题已锁定,无法添加新消息",
    "details": "关于错误的更详细信息..."
  }
}

验证错误

如果抛出的异常实现IHasValidationErrors接口,则会填充标准字段validationErrors

{
  "error": {
    "code": "App:010046",
    "message": "您的请求无效,请更正后重试!",
    "validationErrors": [{
      "message": "用户名长度至少为3个字符。",
      "members": ["userName"]
    },
    {
      "message": "密码为必填项",
      "members": ["password"]
    }]
  }
}

AbpValidationException实现了IHasValidationErrors接口,当请求输入无效时,框架会自动抛出此异常。因此,除非有高度自定义的验证逻辑,否则通常不需要处理验证错误。

日志记录

捕获的异常会自动记录。

日志级别

默认情况下,异常以Error级别记录。如果异常实现IHasLogLevel接口,则可以由其确定日志级别。示例:

public class MyException : Exception, IHasLogLevel
{
    public LogLevel LogLevel { get; set; } = LogLevel.Warning;

    //...
}

自记录异常

某些异常类型可能需要写入额外的日志。如果需要,它们可以实现IExceptionWithSelfLogging接口。示例:

public class MyException : Exception, IExceptionWithSelfLogging
{
    public void Log(ILogger logger)
    {
        //...记录额外信息
    }
}

使用ILogger.LogException扩展方法写入异常日志。需要时可以使用相同的扩展方法。

业务异常

大多数自定义异常将是业务异常。IBusinessException接口用于将异常标记为业务异常。

BusinessException除了实现IBusinessException接口外,还实现了IHasErrorCodeIHasErrorDetailsIHasLogLevel接口。默认日志级别为Warning

通常,特定业务异常会有一个错误代码。例如:

throw new BusinessException(QaErrorCodes.CanNotVoteYourOwnAnswer);

QaErrorCodes.CanNotVoteYourOwnAnswer只是一个const string。推荐使用以下错误代码格式:

<代码命名空间>:<错误代码>

代码命名空间是模块/应用特定的唯一值。示例:

Volo.Qa:010002

此处Volo.Qa是代码命名空间。代码命名空间将在本地化异常消息时使用。

  • 需要时,可以直接抛出BusinessException或从其派生自定义异常类型。
  • BusinessException类的所有属性都是可选的。但通常需要设置ErrorCodeMessage属性。

异常本地化

抛出异常的一个问题是如何在向客户端发送错误消息时进行本地化。ABP提供两种模型及其变体。

用户友好异常

如果异常实现IUserFriendlyException接口,则ABP不会更改其MessageDetails属性,而是直接发送给客户端。

UserFriendlyException类是IUserFriendlyException接口的内置实现。示例用法:

throw new UserFriendlyException(
    "用户名必须唯一!"
);

这种方式完全不需要本地化。如果想本地化消息,可以注入并使用标准字符串本地化器(参见本地化文档)。示例:

throw new UserFriendlyException(_stringLocalizer["UserNameShouldBeUniqueMessage"]);

然后在本地化资源中为每种语言定义它。示例:

{
  "culture": "en",
  "texts": {
    "UserNameShouldBeUniqueMessage": "用户名必须唯一!"
  }
}

字符串本地化器已支持参数化消息。例如:

throw new UserFriendlyException(_stringLocalizer["UserNameShouldBeUniqueMessage", "john"]);

本地化文本可以是:

"UserNameShouldBeUniqueMessage": "用户名必须唯一!'{0}'已被占用!"
  • IUserFriendlyException接口继承自IBusinessExceptionUserFriendlyException类继承自BusinessException类。

使用错误代码

UserFriendlyException很好,但在高级用法中存在一些问题:

  • 它要求您注入字符串本地化器,并在抛出异常时始终使用它。
  • 但在某些情况下,可能无法注入字符串本地化器(在静态上下文或实体方法中)。

可以使用错误代码将过程分离,而不是在抛出异常时进行本地化。

首先,在模块配置中定义代码命名空间本地化资源的映射:

services.Configure<AbpExceptionLocalizationOptions>(options =>
{
    options.MapCodeNamespace("Volo.Qa", typeof(QaResource));
});

然后,任何具有Volo.Qa命名空间的异常都将使用给定的本地化资源进行本地化。本地化资源应始终包含以错误代码为键的条目。示例:

{
  "culture": "en",
  "texts": {
    "Volo.Qa:010002": "您不能为自己的答案投票!"
  }
}

然后可以使用错误代码抛出业务异常:

throw new BusinessException(QaDomainErrorCodes.CanNotVoteYourOwnAnswer);
  • 抛出任何实现IHasErrorCode接口的异常行为相同。因此,错误代码本地化方法并非BusinessException类独有。
  • 错误消息不需要定义本地化字符串。如果未定义,ABP会向客户端发送默认错误消息。它不使用异常的Message属性!如果需要,请使用UserFriendlyException(或使用实现IUserFriendlyException接口的异常类型)。

使用消息参数

如果有参数化错误消息,可以使用异常的Data属性设置。例如:

throw new BusinessException("App:010046")
{
    Data =
    {
        {"UserName", "john"}
    }
};

幸运的是,有一种快捷方式编码:

throw new BusinessException("App:010046")
    .WithData("UserName", "john");

然后本地化文本可以包含UserName参数:

{
  "culture": "en",
  "texts": {
    "App:010046": "用户名必须唯一。'{UserName}'已被占用!"
  }
}
  • WithData可以链式调用多个参数(如.WithData(...).WithData(...))。

HTTP状态码映射

ABP尝试根据以下规则为常见异常类型自动确定最合适的HTTP状态码:

  • 对于AbpAuthorizationException
    • 如果用户未登录,返回401(未授权)。
    • 如果用户已登录,返回403(禁止访问)。
  • 对于AbpValidationException,返回400(错误请求)。
  • 对于EntityNotFoundException,返回404(未找到)。
  • 对于IBusinessException(及IUserFriendlyException,因为它扩展了IBusinessException),返回403(禁止访问)。
  • 对于NotImplementedException,返回501(未实现)。
  • 对于其他异常(假定为基础设施异常),返回500(内部服务器错误)。

IHttpExceptionStatusCodeFinder用于自动确定HTTP状态码。默认实现是DefaultHttpExceptionStatusCodeFinder类。可以根据需要替换或扩展它。

自定义映射

可以通过自定义映射覆盖自动HTTP状态码确定。例如:

services.Configure<AbpExceptionHttpStatusCodeOptions>(options =>
{
    options.Map("Volo.Qa:010002", HttpStatusCode.Conflict);
});

订阅异常

当ABP处理异常时,可以收到通知。它会自动将所有异常记录到标准日志记录器,但您可能希望做更多。

在这种情况下,在应用中创建从ExceptionSubscriber类派生的类:

public class MyExceptionSubscriber : ExceptionSubscriber
{
    public async override Task HandleAsync(ExceptionNotificationContext context)
    {
        //TODO...
    }
}

context对象包含有关发生的异常的必要信息。

可以有多个订阅者,每个都会收到异常的副本。订阅者抛出的异常会被忽略(但仍会记录)。

内置异常

框架会自动抛出一些异常类型:

  • 如果当前用户没有执行请求操作的权限,会抛出AbpAuthorizationException。详见授权
  • 如果当前请求的输入无效,会抛出AbpValidationException。详见验证
  • 如果请求的实体不可用,会抛出EntityNotFoundException。这主要由仓储抛出。

您也可以在代码中抛出这些类型的异常(尽管很少需要)。

AbpExceptionHandlingOptions

AbpExceptionHandlingOptions是配置异常处理系统的主要选项对象。可以在模块的ConfigureServices方法中配置它:

Configure<AbpExceptionHandlingOptions>(options =>
{
    options.SendExceptionsDetailsToClients = true;
    options.SendStackTraceToClients = false;
});

以下是可配置的选项列表:

  • SendExceptionsDetailsToClients(默认:false):可以启用或禁用向客户端发送异常详情。
  • SendStackTraceToClients(默认:true):可以启用或禁用向客户端发送异常的堆栈跟踪。如果想向客户端发送堆栈跟踪,必须将SendStackTraceToClientsSendExceptionsDetailsToClients选项都设置为true,否则堆栈跟踪不会发送给客户端。

另请参阅

在本文档中