一、异常处理(Exception Handling)
常用核心辅助类:
AbpExceptionFilter:自动捕获并处理异常。UserFriendlyException:用户友好异常(直接返回给前端)。IExceptionSubscriber:自定义异常订阅。
1、核心类全解析
| 类/特性/接口 | 核心作用 | 适用场景 |
|---|---|---|
AbpExceptionFilter |
全局异常过滤器,自动捕获并处理异常 | 无需手动try-catch,框架自动处理所有未捕获异常 |
UserFriendlyException |
用户友好异常(直接返回给前端的提示信息) | 业务验证失败(如“用户名已存在”) |
IExceptionSubscriber |
异常订阅器,自定义异常处理逻辑 | 记录异常日志、发送告警、特殊异常处理 |
ExceptionHandler |
异常处理器,按异常类型分发处理逻辑 | 针对不同异常类型(如数据库异常、权限异常)定制处理 |
AbpExceptionOptions |
异常处理配置选项 | 全局配置异常处理行为(如是否显示详细堆栈) |
[IgnoreExceptionFilter] |
忽略异常过滤器(不自动处理异常) | 需要手动处理的特殊接口(如自定义错误响应) |
2、实战示例:从基础到进阶
1. UserFriendlyException:返回用户可理解的提示
这是最常用的异常类,用于业务逻辑验证失败,直接向用户展示友好提示(不会暴露技术细节)。
示例:注册时检查用户名是否已存在
public class UserAppService : ApplicationService
{private readonly IRepository<IdentityUser, Guid> _userRepo;public UserAppService(IRepository<IdentityUser, Guid> userRepo){_userRepo = userRepo;}public async Task RegisterAsync(RegisterInput input){// 检查用户名是否已存在var exists = await _userRepo.AnyAsync(u => u.UserName == input.UserName);if (exists){// 抛出用户友好异常:前端直接显示此消息throw new UserFriendlyException("用户名已被占用,请更换其他用户名");}// 其他注册逻辑...}
}
前端接收效果:
框架会自动将异常转换为标准化响应:
{"error": {"code": null,"message": "用户名已被占用,请更换其他用户名", // 直接显示给用户"details": null,"data": {}}
}
2. AbpExceptionFilter:全局自动捕获异常
ABP默认注册了AbpExceptionFilter,能自动捕获所有未手动处理的异常,无需写try-catch。
场景:未处理的异常自动转为标准化响应
public class ProductAppService : ApplicationService
{public async Task<ProductDto> GetAsync(Guid id){// 假设未找到商品(未处理的异常)var product = await _productRepo.FindAsync(id);if (product == null){// 这里故意不处理,抛出框架自带的EntityNotFoundExceptionthrow new EntityNotFoundException(typeof(Product), id);}return ObjectMapper.Map<Product, ProductDto>(product);}
}
自动处理效果:
AbpExceptionFilter捕获异常后,返回结构化响应(隐藏敏感堆栈,只给必要信息):
{"error": {"code": "EntityNotFound","message": "未找到ID为xxx的Product实体","details": "(开发环境可见堆栈信息,生产环境隐藏)","data": {"EntityType": "Product","Id": "xxx"}}
}
配置AbpExceptionOptions(控制异常响应):
在模块中配置异常处理行为(如生产环境是否显示详细信息):
public override void ConfigureServices(ServiceConfigurationContext context)
{Configure<AbpExceptionOptions>(options =>{// 生产环境是否显示详细错误信息(默认false,避免泄露技术细节)options.SendExceptionsDetailsToClients = context.Services.GetHostingEnvironment().IsDevelopment();// 生产环境是否显示异常堆栈(默认false)options.SendStackTraceToClients = context.Services.GetHostingEnvironment().IsDevelopment();});
}
3. IExceptionSubscriber:自定义异常订阅(如记录日志、告警)
通过实现IExceptionSubscriber,可在异常发生时执行额外逻辑(如写入日志、发送邮件告警)。
示例:异常发生时记录到数据库并发送告警
using Volo.Abp.DependencyInjection;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Logging;// 标记为单例,确保全局唯一
[Dependency(ServiceLifetime.Singleton)]
public class ErrorLogSubscriber : IExceptionSubscriber
{private readonly ILogger<ErrorLogSubscriber> _logger;private readonly IRepository<ErrorLog, Guid> _errorLogRepo;private readonly IEmailSender _emailSender; // 假设的邮件发送服务public ErrorLogSubscriber(ILogger<ErrorLogSubscriber> logger,IRepository<ErrorLog, Guid> errorLogRepo,IEmailSender emailSender){_logger = logger;_errorLogRepo = errorLogRepo;_emailSender = emailSender;}// 异常发生时触发public async Task HandleAsync(ExceptionNotificationContext context){var exception = context.Exception;// 1. 记录异常到数据库await _errorLogRepo.InsertAsync(new ErrorLog{Message = exception.Message,StackTrace = exception.StackTrace,OccurrenceTime = DateTime.Now,UserId = context.HttpContext?.User?.FindFirstValue(AbpClaimTypes.UserId) // 记录操作用户});// 2. 严重异常(如数据库连接失败)发送邮件告警if (exception is SqlException){await _emailSender.SendAsync("admin@example.com", "系统异常告警", $"数据库异常:{exception.Message}");}// 3. 记录到日志系统(如Serilog)_logger.LogError(exception, "发生未处理异常");}
}
原理:
- 所有异常(包括
UserFriendlyException)都会触发IExceptionSubscriber的HandleAsync方法; - 可通过
context.Exception判断异常类型,执行针对性处理。
4. ExceptionHandler:按类型定制异常处理逻辑
ExceptionHandler用于对特定类型的异常编写处理逻辑(如将SqlException转换为用户友好提示)。
示例:处理数据库异常
using Microsoft.Data.SqlClient;
using Volo.Abp.ExceptionHandling;public class SqlExceptionHandler : ExceptionHandler<SqlException>
{// 处理SqlExceptionpublic override Task HandleAsync(ExceptionHandlingContext context, SqlException exception){// 根据SQL错误号返回不同提示if (exception.Number == 1062) // 主键冲突{context.Result = new ExceptionHandlingResult{// 覆盖异常消息(隐藏SQL细节)Message = "数据已存在,无法重复添加",Details = "请检查输入内容是否重复"};}else // 其他数据库错误{context.Result = new ExceptionHandlingResult{Message = "数据操作失败,请稍后重试",Details = "系统正在处理此问题"};}return Task.CompletedTask;}
}// 注册异常处理器(在模块中)
public override void ConfigureServices(ServiceConfigurationContext context)
{Configure<AbpExceptionHandlingOptions>(options =>{// 注册SqlException的处理器options.Handlers.Add<SqlExceptionHandler>();});
}
5. [IgnoreExceptionFilter]:忽略全局异常处理(手动处理)
某些场景下需要完全自定义异常响应(如第三方接口对接),可通过[IgnoreExceptionFilter]禁用自动处理。
示例:手动处理异常并返回自定义响应
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc;public class ThirdPartyApiController : AbpController
{// 忽略全局异常过滤器,手动处理[IgnoreExceptionFilter][HttpPost("sync-data")]public async Task<IActionResult> SyncDataAsync(SyncInput input){try{// 调用第三方接口的逻辑...return Ok(new { success = true });}catch (Exception ex){// 完全自定义响应格式(适合第三方对接)return BadRequest(new{error_code = "SYNC_FAILED",error_msg = ex.Message,timestamp = DateTime.Now.Ticks});}}
}
3、异常处理流程总结
- 异常抛出:业务逻辑中抛出
UserFriendlyException(已知业务错误)或其他异常(如EntityNotFoundException); - 全局捕获:
AbpExceptionFilter自动捕获未处理的异常; - 分发处理:通过
ExceptionHandler按异常类型处理(如转换消息); - 订阅扩展:
IExceptionSubscriber执行额外逻辑(如日志、告警); - 响应返回:框架返回标准化JSON响应(开发/生产环境显示不同细节)。
4、常见问题与最佳实践
- 避免滥用
UserFriendlyException:只用于用户能理解的业务错误,技术异常(如数据库连接失败)应使用框架自带异常,避免暴露实现细节; - 开发/生产环境区分:通过
AbpExceptionOptions控制是否返回堆栈信息(生产环境禁用); - 异常日志完整性:在
IExceptionSubscriber中记录完整异常信息(包括堆栈、用户ID、请求参数),方便排查问题; - 自定义异常类型:复杂业务可定义自己的异常类(如
InsufficientBalanceException),并通过ExceptionHandler专门处理。
通过这些工具,ABP能优雅地处理从简单业务验证到复杂系统异常的全场景,既保证用户体验,又方便开发者排查问题。
二、异常处理入门:从“崩溃”到“优雅提示”的全过程
如果你是第一次接触异常处理,可以先简单理解:异常就是程序运行中出的“意外”(比如用户输入错误、数据库连接失败),而异常处理就是让程序在遇到这些“意外”时,不崩溃、不显示乱码,而是友好地告诉用户“出了什么问题”,同时方便开发者排查原因。
下面用生活化的例子,把ABP的异常处理讲透,包括“怎么抛异常”“框架怎么处理”“怎么自定义处理逻辑”。
1、先看个场景:没有异常处理会怎样?
假设你写了一个“转账”功能,代码里没做异常处理:
public void TransferMoney(string fromAccount, string toAccount, decimal amount)
{// 从A账户扣钱var from = GetAccount(fromAccount);from.Balance -= amount;// 给B账户加钱var to = GetAccount(toAccount);to.Balance += amount;
}
如果遇到“B账户不存在”的情况,程序会直接崩溃,屏幕上可能显示一堆看不懂的错误(比如NullReferenceException: 对象引用未设置到对象的实例),用户一脸懵,你也不知道具体哪里错了。
2、最基础:用UserFriendlyException告诉用户“出了什么错”
UserFriendlyException是ABP里最常用的“用户友好异常”,作用是把错误信息用用户能懂的话讲出来,而不是显示技术术语。
示例:转账时检查账户是否存在
public void TransferMoney(string fromAccount, string toAccount, decimal amount)
{// 检查转出账户是否存在var from = GetAccount(fromAccount);if (from == null){// 抛用户友好异常:直接告诉用户“转出账户不存在”throw new UserFriendlyException("转出账户不存在,请检查账号是否正确");}// 检查转入账户是否存在var to = GetAccount(toAccount);if (to == null){throw new UserFriendlyException("转入账户不存在,无法完成转账");}// 检查余额是否足够if (from.Balance < amount){throw new UserFriendlyException($"余额不足,当前余额:{from.Balance},需转出:{amount}");}// 执行转账(省略具体逻辑)
}
效果:用户能看懂的提示
当程序抛出UserFriendlyException时,ABP会自动把异常转换成友好的提示,前端显示:
转出账户不存在,请检查账号是否正确
而不是一堆技术错误,用户知道该怎么解决(比如检查账号)。
3、AbpExceptionFilter:框架自动“接住”所有没处理的异常
你可能会问:“如果我忘了抛UserFriendlyException,程序会不会又崩溃?”
不会!ABP有个“全局异常过滤器”AbpExceptionFilter,能自动“接住”所有没手动处理的异常,然后整理成用户能看懂的格式。
示例:没处理的异常被自动接住
public ProductDto GetProduct(Guid id)
{// 假设没找到商品,直接用框架自带的“实体未找到”异常var product = _productRepo.Find(id);if (product == null){// 这里没抛UserFriendlyException,而是抛框架自带的异常throw new EntityNotFoundException("商品不存在");}return ObjectMapper.Map<Product, ProductDto>(product);
}
效果:自动转为友好响应
AbpExceptionFilter会接住这个异常,返回给前端:
{"error": {"message": "商品不存在", // 用户能看懂"details": "(开发时能看到详细错误位置,上线后隐藏)"}
}
- 开发时:
details里会显示错误发生的代码行(方便你排查); - 上线后:
details会隐藏(避免用户看到技术细节)。
4、IExceptionSubscriber:异常发生时“偷偷做些事”
有时候,异常发生后除了告诉用户,还需要“偷偷”做一些事:比如记录日志(方便排查)、给管理员发邮件告警(比如数据库崩溃了)。这时候就需要IExceptionSubscriber(异常订阅器)。
示例:异常发生时自动记录日志
// 定义一个异常订阅器
public class LogExceptionSubscriber : IExceptionSubscriber
{private readonly ILogger _logger; // 日志工具private readonly IRepository<ErrorLog, Guid> _errorLogRepo; // 错误日志表// 框架自动把工具“递”进来public LogExceptionSubscriber(ILogger logger, IRepository<ErrorLog, Guid> errorLogRepo){_logger = logger;_errorLogRepo = errorLogRepo;}// 只要有异常发生,这个方法就会自动触发public async Task HandleAsync(ExceptionNotificationContext context){var exception = context.Exception; // 获取发生的异常// 1. 记录到日志文件(比如用Serilog)_logger.LogError(exception, "系统出错了!");// 2. 记录到数据库(方便后期分析)await _errorLogRepo.InsertAsync(new ErrorLog{Message = exception.Message, // 错误信息Time = DateTime.Now, // 发生时间UserId = CurrentUser.Id, // 哪个用户操作时出错的Url = context.HttpContext?.Request.Path // 哪个接口出错的});// 3. 如果是严重错误(比如数据库连接失败),发邮件给管理员if (exception is SqlException){await SendEmailToAdminAsync("数据库异常", exception.Message);}}// 发送告警邮件的方法(简化版)private async Task SendEmailToAdminAsync(string title, string content){// 调用邮件服务发送...}
}
效果:异常“一石三鸟”
- 告诉用户错误;
- 记录日志到文件和数据库;
- 严重错误时自动告警,管理员能及时处理。
5、ExceptionHandler:给不同异常“定制处理方案”
不同的异常可能需要不同的处理方式:比如“数据库连接失败”要提示“稍后重试”,“权限不足”要提示“没有访问权限”。ExceptionHandler就是用来给特定异常定制处理逻辑的。
示例:处理数据库异常
// 专门处理数据库异常的处理器
public class SqlExceptionHandler : ExceptionHandler<SqlException>
{// 当发生SqlException时,会自动调用这个方法public override Task HandleAsync(ExceptionHandlingContext context, SqlException exception){// 根据数据库错误号,返回不同提示if (exception.Number == 1062) // 错误号1062:主键冲突(数据重复){context.Result = new ExceptionHandlingResult{Message = "这条数据已经存在啦,不用重复添加~",Details = "请检查输入的内容是否和已有的重复"};}else if (exception.Number == 4060) // 错误号4060:数据库无法连接{context.Result = new ExceptionHandlingResult{Message = "系统暂时无法连接数据库,请稍后再试",Details = "技术人员已收到通知,正在处理"};}return Task.CompletedTask;}
}
注册处理器(让框架知道它)
在模块中配置,告诉ABP“遇到SqlException时用这个处理器”:
public override void ConfigureServices(ServiceConfigurationContext context)
{Configure<AbpExceptionHandlingOptions>(options =>{options.Handlers.Add<SqlExceptionHandler>(); // 注册数据库异常处理器});
}
6、新手必懂的3个关键点
- 异常不是洪水猛兽:程序出错很正常,关键是要友好地告诉用户,同时方便自己排查;
- 优先用
UserFriendlyException:业务逻辑错误(如“余额不足”)一定要用它,用户能懂; - 不用写try-catch:ABP的
AbpExceptionFilter会自动接住所有异常,你只需要抛异常就行。
7、常见问题(避坑指南)
-
问题1:抛了异常但前端没收到提示?
答:检查是否忘了throw关键字(比如只写了new UserFriendlyException(...),没写throw)。 -
问题2:生产环境泄露了技术错误?
答:在AbpExceptionOptions中配置SendExceptionsDetailsToClients = false(默认就是false,开发时设为true方便调试)。 -
问题3:想记录异常但不知道怎么获取用户信息?
答:在IExceptionSubscriber中用CurrentUser.Id获取当前登录用户ID(需要注入ICurrentUser)。
通过这些工具,ABP让异常处理变得简单:你只需要关注“什么时候抛什么错”,剩下的“怎么告诉用户”“怎么记录日志”“怎么处理特殊异常”都由框架或配置好的处理器完成。
