ABP演示 - 异常处理
异常处理是Web应用程序的核心能力之一,ABP框架提供了标准化、自动化的异常处理模型,涵盖异常捕获、格式化响应、本地化、日志记录、HTTP状态码映射等全场景能力。本文将基于ABP官方文档,结合核心知识点,带你系统掌握ABP异常处理的设计逻辑与实战用法。

一、ABP异常处理核心体系
1. 核心特性
ABP的异常处理机制无需手动编写大量重复代码,核心优势包括:
- 自动捕获并处理所有异常,无需在每个接口中添加try-catch;
- 隐藏内部敏感错误信息,返回标准化客户端响应;
- 内置异常本地化支持,适配多语言场景;
- 自动映射常见异常到对应HTTP状态码,减少手动配置;
- 支持自定义异常扩展,满足复杂业务场景需求。
2. 异常处理触发条件
AbpExceptionFilter 会自动处理满足以下任一条件的异常:
- Controller/应用服务方法返回
ObjectResult(非ViewResult)时抛出异常; - 请求为AJAX类型(HTTP请求头
X-Requested-With: XMLHttpRequest); - 客户端接受的响应类型为
application/json(HTTP请求头Accept: application/json)。
3. 标准错误响应格式
所有处理后的异常都会以 RemoteServiceErrorResponse 类的JSON格式返回,结构如下:
(1)基础响应(仅含消息)
{
"error": {
"message": "操作失败,请联系管理员"
}
}
(2)带错误代码的响应
实现 IHasErrorCode 接口后,响应会包含 code 字段:
{
"error": {
"code": "App:010042",
"message": "该主题已锁定,无法添加新消息"
}
}
(3)带详细信息的响应
实现 IHasErrorDetails 接口后,响应会包含 details 字段:
{
"error": {
"code": "App:010042",
"message": "该主题已锁定",
"details": "主题ID:123,锁定时间:2026-01-10,锁定人:admin"
}
}
(4)验证错误响应
实现 IHasValidationErrors 接口(如 AbpValidationException)时,响应会包含 validationErrors 字段:
{
"error": {
"code": "App:010046",
"message": "请求参数无效,请修正后重试",
"validationErrors": [
{
"message": "用户名长度不能少于3位",
"members": ["userName"]
},
{
"message": "密码为必填项",
"members": ["password"]
}
]
}
}
例子:
新增 Code
src\Acme.BookStore.Domain.Shared\BookStoreDomainErrorCodes.cs
namespace Acme.BookStore;
public static class BookStoreDomainErrorCodes
{
/* You can add your business exception error codes here, as constants */
public const string AuthorAlreadyExists = "BookStore:00001";
//新增的code
public const string BookLocked = "BookStore:00002";
public const string InvalidLockReason = "BookStore:00003";
}新增自定义 IHasErrorDetails 详细信息
src\Acme.BookStore.Domain\Exceptions\BookLockedException.cs
using System;
using Volo.Abp;
using Volo.Abp.ExceptionHandling;
namespace Acme.BookStore.Exceptions
{
public class BookLockedException : BusinessException, IHasErrorDetails
{
public BookLockedException(string bookId, DateTime lockTime, string lockedBy)
: base(BookStoreDomainErrorCodes.BookLocked)
{
WithData("BookId", bookId);
WithData("LockTime", lockTime);
WithData("LockedBy", lockedBy);
Details = $"Book ID: {bookId}, Locked at: {lockTime}, By: {lockedBy}";
}
}
}
实现方法
src\Acme.BookStore.Application\Books\BookAppService.cs
- 基础异常示例 - 触发基础响应
- 带错误代码的异常示例 - 触发带错误代码的响应
- 带详细信息的异常示例 - 触发带详细信息的响应
- 验证错误由ABP自动处理,无需手动抛出
// 例如在DTO上使用[Required]等数据注解即可
namespace Acme.BookStore.Books
{
public class BookAppService :
CrudAppService<
Book,
BookDto,
Guid,
PagedAndSortedResultRequestDto,
CreateUpdateBookDto>,
IBookAppService
{
public BookAppService(
IRepository<Book, Guid> repository,
IDistributedCache<BookCacheItem, Guid> cache,
BookCacheKeyGenerator cacheKeyGenerator,
BookCacheBulkOperations cacheBulkOperations)
: base(repository)
{
//... 其他实现
}
//... 其他方法
/// <summary>
/// 锁定图书方法 - 演示4种标准错误响应格式
/// </summary>
public async Task LockBookAsync(string bookId = null, string lockReason = null)
{
// 1. 基础异常示例 - 触发基础响应
if (string.IsNullOrEmpty(bookId))
{
throw new UserFriendlyException("Book ID cannot be empty!");
}
// 2. 带错误代码的异常示例 - 触发带错误代码的响应
if (string.IsNullOrEmpty(lockReason))
{
throw new BusinessException(BookStoreDomainErrorCodes.InvalidLockReason, "Lock reason is required")
.WithData("Field", nameof(lockReason));
}
// 3. 带详细信息的异常示例 - 触发带详细信息的响应
if (lockReason.Length > 100)
{
throw new BookLockedException(
bookId: bookId,
lockTime: DateTime.Now,
lockedBy: CurrentUser.Name ?? "System"
);
}
// 4. 验证错误由ABP自动处理,无需手动抛出
// 例如在DTO上使用[Required]等数据注解即可
// 这里可以添加实际的锁定逻辑
await Task.CompletedTask;
}
}
}这里的异常为:
//1. 基础异常示例 - 触发基础响应
{
"error": {
"code": null,
"message": "Book ID cannot be empty!",
"details": null,
"data": {},
"validationErrors": null
}
}
//2. 带错误代码的异常示例 - 触发带错误代码的响应
// .WithData("Field", nameof(lockReason) 返回
{
"error": {
"code": "BookStore:00003",
"message": "对不起,在处理您的请求期间产生了一个服务器内部错误!!",
"details": null,
"data": {
"Field": "lockReason"
},
"validationErrors": null
}
}
IHasErrorDetails 详细信息,调用 BookLockedException.cs 写的方法
//3. 带详细信息的异常示例 - 触发带详细信息的响应
{
"error": {
"code": "BookStore:00002",
"message": "对不起,在处理您的请求期间产生了一个服务器内部错误!!",
"details": null,
"data": {
"BookId": " 1",
"LockTime": "2026-01-10T21:16:09.3797637+08:00",
"LockedBy": "System"
},
"validationErrors": null
}
}
二、异常核心接口与内置异常
ABP通过接口抽象定义异常能力,自定义异常时可通过实现对应接口扩展功能;同时提供多个内置异常,覆盖常见业务场景。
1. 核心扩展接口
| 接口名称 | 核心作用 | 关键成员/方法 |
|---|---|---|
IHasErrorCode |
为异常添加唯一错误代码 | string ErrorCode { get; } |
IHasErrorDetails |
为异常添加详细描述信息 | string Details { get; } |
IHasValidationErrors |
承载参数验证错误信息 | List<ValidationError> ValidationErrors { get; } |
IHasLogLevel |
指定异常的日志记录级别 | LogLevel LogLevel { get; set; } |
IExceptionWithSelfLogging |
自定义异常的日志记录逻辑 | void Log(ILogger logger) |
IBusinessException |
标记异常为业务异常(非系统异常) | 无抽象成员,仅作为标识 |
IUserFriendlyException |
标记异常为用户友好异常(直接返回消息) | 无抽象成员,继承自 IBusinessException |
2. 框架内置异常
| 异常类型 | 适用场景 | 默认HTTP状态码 | 实现接口 |
|---|---|---|---|
AbpAuthorizationException |
权限验证失败(未登录/已登录无权限) | 401/403 | - |
AbpValidationException |
请求参数验证失败 | 400 | IHasValidationErrors |
EntityNotFoundException |
请求的实体记录不存在(仓储自动抛出) | 404 | - |
BusinessException |
通用业务异常 | 403 | IHasErrorCode/IHasErrorDetails/IHasLogLevel |
UserFriendlyException |
直接面向用户的友好提示异常 | 403 | IUserFriendlyException(继承自BusinessException) |
内置异常使用示例
// 1. 实体未找到异常(仓储查询不到数据时自动抛出,也可手动抛出)
throw new EntityNotFoundException(typeof(Book), bookId);
// 2. 业务异常(带错误代码)
throw new BusinessException("BookStore:010001");
// 3. 用户友好异常(直接返回提示消息)
throw new UserFriendlyException("用户名已存在,请更换其他名称");
三、异常日志处理
ABP会自动记录所有捕获的异常,无需手动调用日志接口,同时支持自定义日志级别和日志内容。
1. 默认日志行为
- 所有未自定义日志级别的异常,默认以
LogLevel.Error级别记录; - 日志内容包含异常类型、消息、堆栈跟踪等核心信息,便于问题排查。
2. 自定义日志级别
实现 IHasLogLevel 接口,可指定异常的日志记录级别:
// 自定义异常,指定日志级别为Warning
public class InventoryShortageException : Exception, IHasLogLevel
{
public LogLevel LogLevel { get; set; } = LogLevel.Warning;
public InventoryShortageException(string productName)
: base($"商品「{productName}」库存不足")
{
}
}
// 使用示例
throw new InventoryShortageException("iPhone 15"); // 日志级别为Warning
3. 自定义日志内容
实现 IExceptionWithSelfLogging 接口,可自定义异常的日志记录逻辑(如添加额外业务信息):
public class OrderExpiredException : Exception, IExceptionWithSelfLogging
{
public Guid OrderId { get; }
public DateTime ExpireTime { get; }
public OrderExpiredException(Guid orderId, DateTime expireTime)
: base($"订单「{orderId}」已过期(过期时间:{expireTime})")
{
OrderId = orderId;
ExpireTime = expireTime;
}
// 自定义日志记录逻辑
public void Log(ILogger logger)
{
logger.LogWarning(
"订单过期警告 - 订单ID:{OrderId},过期时间:{ExpireTime},当前时间:{CurrentTime}",
OrderId, ExpireTime, DateTime.Now
);
}
}
4. 日志扩展方法
ABP提供 ILogger.LogException 扩展方法,可在手动处理异常时调用,保持日志格式统一:
try
{
// 业务逻辑
}
catch (Exception ex)
{
_logger.LogException(ex, LogLevel.Error, "处理订单时发生异常");
throw; // 继续抛出,让ABP统一处理响应
}
四、异常本地化
ABP提供两种异常本地化方案,适配不同场景(是否能注入 IStringLocalizer)。
1. 方案1:UserFriendlyException(直接注入本地化消息)
适用于可注入 IStringLocalizer 的场景(如应用服务、控制器),直接传入本地化后的消息:
(1)基础用法
public class BookAppService : ApplicationService
{
private readonly IStringLocalizer<BookResource> _stringLocalizer;
public BookAppService(IStringLocalizer<BookResource> stringLocalizer)
{
_stringLocalizer = stringLocalizer;
}
public async Task CreateAsync(CreateBookDto input)
{
// 检查用户名是否重复
var isDuplicate = await _bookRepository.AnyAsync(b => b.Name == input.Name);
if (isDuplicate)
{
// 抛出本地化后的用户友好异常
throw new UserFriendlyException(_stringLocalizer["BookNameDuplicateMessage"]);
}
}
}
(2)带参数的本地化消息
支持参数化占位符,适配动态内容:
// 抛出异常(传入参数)
throw new UserFriendlyException(_stringLocalizer["BookNameDuplicateWithParam", input.Name]);
// 本地化资源文件(zh-Hans.json)
{
"BookNameDuplicateWithParam": "图书名称「{0}」已存在,请更换其他名称"
}
2. 方案2:错误代码本地化(无IStringLocalizer注入场景)
适用于无法注入 IStringLocalizer 的场景(如实体方法、静态类),通过错误代码关联本地化资源:
(1)步骤1:配置代码命名空间映射
在模块类中,将错误代码的命名空间与本地化资源关联:
public class BookStoreModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
// 映射错误代码命名空间"BookStore"到BookResource资源
context.Services.Configure<AbpExceptionLocalizationOptions>(options =>
{
options.MapCodeNamespace("BookStore", typeof(BookResource));
});
}
}
(2)步骤2:配置本地化资源
在本地化资源文件中,添加错误代码对应的文本:
// BookResource.zh-Hans.json
{
"BookStore:010001": "图书名称「{BookName}」已存在,请更换其他名称",
"BookStore:010002": "图书库存不足,当前库存:{Stock},请求数量:{RequestCount}"
}
(3)步骤3:抛出带错误代码的异常
通过 BusinessException 抛出异常,无需注入本地化接口:
// 实体方法中抛出异常(无IStringLocalizer注入)
public class Book : AggregateRoot<Guid>
{
public string Name { get; set; }
public int Stock { get; set; }
public void ReduceStock(int count)
{
if (count > Stock)
{
// 带参数的错误代码异常
throw new BusinessException("BookStore:010002")
.WithData("Stock", Stock)
.WithData("RequestCount", count);
}
Stock -= count;
}
}
(4)关键说明
- 错误代码格式规范:
code-namespace:error-code(如BookStore:010001),code-namespace需在模块内唯一; WithData方法支持链式调用,可添加多个参数:.WithData("Key1", "Value1").WithData("Key2", "Value2");- 若未配置对应错误代码的本地化文本,ABP会返回默认消息,不会使用异常的
Message属性。
五、HTTP状态码映射
ABP会自动将常见异常映射到标准HTTP状态码,同时支持自定义映射规则。
1. 默认映射规则
| 异常类型/接口 | HTTP状态码 | 适用场景 |
|---|---|---|
AbpAuthorizationException |
401 | 用户未登录时访问需授权资源 |
AbpAuthorizationException |
403 | 用户已登录但无对应权限 |
AbpValidationException |
400 | 请求参数验证失败 |
EntityNotFoundException |
404 | 请求的实体记录不存在 |
IBusinessException |
403 | 通用业务异常(含 UserFriendlyException) |
NotImplementedException |
501 | 方法未实现 |
| 其他未定义异常 | 500 | 系统内部错误 |
2. 自定义状态码映射
通过配置 AbpExceptionHttpStatusCodeOptions,可重写特定错误代码的HTTP状态码:
public class BookStoreModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
// 为错误代码"BookStore:010001"映射HTTP 409(冲突)
context.Services.Configure<AbpExceptionHttpStatusCodeOptions>(options =>
{
options.Map("BookStore:010001", HttpStatusCode.Conflict);
});
}
}
3. 扩展状态码判断逻辑
ABP通过 IHttpExceptionStatusCodeFinder 接口(默认实现 DefaultHttpExceptionStatusCodeFinder)判断异常的HTTP状态码。若默认逻辑不满足需求,可自定义实现:
// 自定义状态码查找器
public class CustomHttpExceptionStatusCodeFinder : IHttpExceptionStatusCodeFinder, ITransientDependency
{
public HttpStatusCode FindStatusCode(Exception exception)
{
// 自定义逻辑:所有InventoryShortageException返回400
if (exception is InventoryShortageException)
{
return HttpStatusCode.BadRequest;
}
// 其他异常使用默认逻辑
var defaultFinder = new DefaultHttpExceptionStatusCodeFinder();
return defaultFinder.FindStatusCode(exception);
}
}
// 注册到DI容器(替换默认实现)
public class BookStoreModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.Replace(ServiceDescriptor.Transient<IHttpExceptionStatusCodeFinder, CustomHttpExceptionStatusCodeFinder>());
}
}
六、高级配置与扩展
1. 发送异常详情到客户端
默认情况下,ABP不会将异常堆栈跟踪等敏感信息返回给客户端。若需在开发/测试环境启用(便于调试),可通过配置开启:
public class BookStoreModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.Configure<AbpExceptionHandlingOptions>(options =>
{
// 开发环境启用,生产环境建议关闭
options.SendExceptionsDetailsToClients = context.Services.GetEnvironmentName() == "Development";
});
}
}
2. 自定义业务异常类
若通用 BusinessException 无法满足需求,可派生自定义业务异常类,扩展属性和逻辑:
// 自定义业务异常(图书相关异常)
public class BookBusinessException : BusinessException
{
// 扩展属性:图书ID
public Guid? BookId { get; set; }
// 构造函数1:仅错误代码
public BookBusinessException(string errorCode) : base(errorCode)
{
}
// 构造函数2:错误代码+参数
public BookBusinessException(string errorCode, params object[] data) : base(errorCode)
{
foreach (var item in data)
{
// 简化参数设置(假设data为键值对数组)
if (item is KeyValuePair<string, object> keyValue)
{
WithData(keyValue.Key, keyValue.Value);
}
}
}
}
// 使用示例
throw new BookBusinessException("BookStore:010002",
new KeyValuePair<string, object>("Stock", 10),
new KeyValuePair<string, object>("RequestCount", 20))
{
BookId = book.Id
};
例子
自定义一个异常
Acme.BookStore.Domain\Authors\AuthorAlreadyExistsException.cs
namespace Acme.BookStore.Authors
{
public class AuthorAlreadyExistsException : BusinessException
{
public AuthorAlreadyExistsException(string name)
: base(BookStoreDomainErrorCodes.AuthorAlreadyExists)
{
WithData("name", name);
}
}
}
使用
src\Acme.BookStore.Domain\Authors\AuthorManager.cs
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Domain.Services;
using Volo.Abp.Guids;
namespace Acme.BookStore.Authors
{
public class AuthorManager : DomainService
{
private readonly IAuthorRepository _authorRepository;
public AuthorManager(IAuthorRepository authorRepository)
{
_authorRepository = authorRepository;
}
public async Task<Author> CreateAsync(
[NotNull] string name,
DateTime birthDate,
[CanBeNull] string shortBio = null)
{
Check.NotNullOrWhiteSpace(name, nameof(name));
var existingAuthor = await _authorRepository.FindByNameAsync(name);
if (existingAuthor != null)
{
throw new AuthorAlreadyExistsException(name);
}
return new Author(
GuidGenerator.Create(),
name,
birthDate,
shortBio
);
}
public async Task ChangeNameAsync(
[NotNull] Author author,
[NotNull] string newName)
{
Check.NotNull(author, nameof(author));
Check.NotNullOrWhiteSpace(newName, nameof(newName));
var existingAuthor = await _authorRepository.FindByNameAsync(newName);
if (existingAuthor != null && existingAuthor.Id != author.Id)
{
throw new AuthorAlreadyExistsException(newName);
}
author.ChangeName(newName);
}
}
}3. 异常处理流程总结
ABP异常处理的完整流程如下:
- 业务逻辑抛出异常(内置异常/自定义异常);
AbpExceptionFilter捕获异常(满足触发条件时);- 若异常实现
IExceptionWithSelfLogging,执行自定义日志逻辑; - 按
IHasLogLevel接口指定的级别记录日志(默认Error); - 解析异常的代码、消息、详情等信息(基于实现的接口);
- 本地化异常消息(基于错误代码或
UserFriendlyException); - 映射HTTP状态码(默认规则/自定义配置);
- 生成
RemoteServiceErrorResponse格式的JSON响应返回客户端。
七、关键注意事项
- 错误代码唯一性:错误代码的
code-namespace需在模块内唯一,避免不同业务模块的错误代码冲突; - 生产环境配置:生产环境需关闭
SendExceptionsDetailsToClients,防止泄露敏感信息; - 日志级别选择:业务异常建议使用
LogLevel.Warning,系统异常使用LogLevel.Error,便于日志筛选; - 本地化资源命名:确保本地化资源文件的命名和路径符合ABP规范,避免本地化失效;
- 异常派生规则:自定义异常若需支持ABP的完整特性,建议继承
BusinessException或UserFriendlyException,而非直接继承Exception。
通过本文的学习,你已掌握ABP异常处理的核心知识点和实战用法。ABP的异常处理机制遵循“约定优于配置”的原则,大部分场景下无需复杂配置即可满足需求,同时通过接口扩展和自定义配置,可适配各类复杂业务场景。在实际开发中,建议优先使用内置异常和标准接口,保持代码的规范性和可维护性。

浙公网安备 33010602011771号