异常处理
ABP框架提供内置的基础设施和标准模型来处理异常。
- 自动处理所有异常,并为API/AJAX请求向客户端发送标准格式化错误消息。
- 自动隐藏内部基础设施错误并返回标准错误消息。
- 提供简单且可配置的方式来本地化异常消息。
- 自动将标准异常映射到HTTP状态码,并提供可配置选项以映射自定义异常。
自动异常处理
当满足以下任一条件时,AbpExceptionFilter会处理异常:
- 异常由返回对象结果(非视图结果)的控制器操作抛出。
- 请求是AJAX请求(
X-Requested-WithHTTP头值为XMLHttpRequest)。 - 客户端明确接受
application/json内容类型(通过acceptHTTP头)。
如果异常被处理,它会自动记录日志,并向客户端返回格式化的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接口外,还实现了IHasErrorCode、IHasErrorDetails和IHasLogLevel接口。默认日志级别为Warning。
通常,特定业务异常会有一个错误代码。例如:
throw new BusinessException(QaErrorCodes.CanNotVoteYourOwnAnswer);
QaErrorCodes.CanNotVoteYourOwnAnswer只是一个const string。推荐使用以下错误代码格式:
<代码命名空间>:<错误代码>
代码命名空间是模块/应用特定的唯一值。示例:
Volo.Qa:010002
此处Volo.Qa是代码命名空间。代码命名空间将在本地化异常消息时使用。
- 需要时,可以直接抛出
BusinessException或从其派生自定义异常类型。 BusinessException类的所有属性都是可选的。但通常需要设置ErrorCode或Message属性。
异常本地化
抛出异常的一个问题是如何在向客户端发送错误消息时进行本地化。ABP提供两种模型及其变体。
用户友好异常
如果异常实现IUserFriendlyException接口,则ABP不会更改其Message和Details属性,而是直接发送给客户端。
UserFriendlyException类是IUserFriendlyException接口的内置实现。示例用法:
throw new UserFriendlyException(
"用户名必须唯一!"
);
这种方式完全不需要本地化。如果想本地化消息,可以注入并使用标准字符串本地化器(参见本地化文档)。示例:
throw new UserFriendlyException(_stringLocalizer["UserNameShouldBeUniqueMessage"]);
然后在本地化资源中为每种语言定义它。示例:
{
"culture": "en",
"texts": {
"UserNameShouldBeUniqueMessage": "用户名必须唯一!"
}
}
字符串本地化器已支持参数化消息。例如:
throw new UserFriendlyException(_stringLocalizer["UserNameShouldBeUniqueMessage", "john"]);
本地化文本可以是:
"UserNameShouldBeUniqueMessage": "用户名必须唯一!'{0}'已被占用!"
IUserFriendlyException接口继承自IBusinessException,UserFriendlyException类继承自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):可以启用或禁用向客户端发送异常的堆栈跟踪。如果想向客户端发送堆栈跟踪,必须将SendStackTraceToClients和SendExceptionsDetailsToClients选项都设置为true,否则堆栈跟踪不会发送给客户端。
抠丁客


