ABP - 异常处理(Exception Handling)[AbpExceptionFilter、UserFriendlyException、IExceptionSubscriber]

一、异常处理(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)
        {
            // 这里故意不处理,抛出框架自带的EntityNotFoundException
            throw 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)都会触发IExceptionSubscriberHandleAsync方法;
  • 可通过context.Exception判断异常类型,执行针对性处理。

4. ExceptionHandler:按类型定制异常处理逻辑

ExceptionHandler用于对特定类型的异常编写处理逻辑(如将SqlException转换为用户友好提示)。

示例:处理数据库异常

using Microsoft.Data.SqlClient;
using Volo.Abp.ExceptionHandling;

public class SqlExceptionHandler : ExceptionHandler<SqlException>
{
    // 处理SqlException
    public 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、异常处理流程总结

  1. 异常抛出:业务逻辑中抛出UserFriendlyException(已知业务错误)或其他异常(如EntityNotFoundException);
  2. 全局捕获AbpExceptionFilter自动捕获未处理的异常;
  3. 分发处理:通过ExceptionHandler按异常类型处理(如转换消息);
  4. 订阅扩展IExceptionSubscriber执行额外逻辑(如日志、告警);
  5. 响应返回:框架返回标准化JSON响应(开发/生产环境显示不同细节)。

4、常见问题与最佳实践

  1. 避免滥用UserFriendlyException:只用于用户能理解的业务错误,技术异常(如数据库连接失败)应使用框架自带异常,避免暴露实现细节;
  2. 开发/生产环境区分:通过AbpExceptionOptions控制是否返回堆栈信息(生产环境禁用);
  3. 异常日志完整性:在IExceptionSubscriber中记录完整异常信息(包括堆栈、用户ID、请求参数),方便排查问题;
  4. 自定义异常类型:复杂业务可定义自己的异常类(如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)
    {
        // 调用邮件服务发送...
    }
}

效果:异常“一石三鸟”

  1. 告诉用户错误;
  2. 记录日志到文件和数据库;
  3. 严重错误时自动告警,管理员能及时处理。

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个关键点

  1. 异常不是洪水猛兽:程序出错很正常,关键是要友好地告诉用户,同时方便自己排查;
  2. 优先用UserFriendlyException:业务逻辑错误(如“余额不足”)一定要用它,用户能懂;
  3. 不用写try-catch:ABP的AbpExceptionFilter会自动接住所有异常,你只需要抛异常就行。

7、常见问题(避坑指南)

  • 问题1:抛了异常但前端没收到提示?
    答:检查是否忘了throw关键字(比如只写了new UserFriendlyException(...),没写throw)。

  • 问题2:生产环境泄露了技术错误?
    答:在AbpExceptionOptions中配置SendExceptionsDetailsToClients = false(默认就是false,开发时设为true方便调试)。

  • 问题3:想记录异常但不知道怎么获取用户信息?
    答:在IExceptionSubscriber中用CurrentUser.Id获取当前登录用户ID(需要注入ICurrentUser)。

通过这些工具,ABP让异常处理变得简单:你只需要关注“什么时候抛什么错”,剩下的“怎么告诉用户”“怎么记录日志”“怎么处理特殊异常”都由框架或配置好的处理器完成。

posted @ 2025-10-24 21:51  【唐】三三  阅读(3)  评论(0)    收藏  举报