ABP演示 - 异常处理

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

一、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

  1. 基础异常示例 - 触发基础响应
  2. 带错误代码的异常示例 - 触发带错误代码的响应
  3. 带详细信息的异常示例 - 触发带详细信息的响应
  4. 验证错误由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异常处理的完整流程如下:

  1. 业务逻辑抛出异常(内置异常/自定义异常);
  2. AbpExceptionFilter 捕获异常(满足触发条件时);
  3. 若异常实现 IExceptionWithSelfLogging,执行自定义日志逻辑;
  4. IHasLogLevel 接口指定的级别记录日志(默认Error);
  5. 解析异常的代码、消息、详情等信息(基于实现的接口);
  6. 本地化异常消息(基于错误代码或 UserFriendlyException);
  7. 映射HTTP状态码(默认规则/自定义配置);
  8. 生成 RemoteServiceErrorResponse 格式的JSON响应返回客户端。

七、关键注意事项

  1. 错误代码唯一性:错误代码的 code-namespace 需在模块内唯一,避免不同业务模块的错误代码冲突;
  2. 生产环境配置:生产环境需关闭 SendExceptionsDetailsToClients,防止泄露敏感信息;
  3. 日志级别选择:业务异常建议使用 LogLevel.Warning,系统异常使用 LogLevel.Error,便于日志筛选;
  4. 本地化资源命名:确保本地化资源文件的命名和路径符合ABP规范,避免本地化失效;
  5. 异常派生规则:自定义异常若需支持ABP的完整特性,建议继承 BusinessExceptionUserFriendlyException,而非直接继承 Exception

通过本文的学习,你已掌握ABP异常处理的核心知识点和实战用法。ABP的异常处理机制遵循“约定优于配置”的原则,大部分场景下无需复杂配置即可满足需求,同时通过接口扩展和自定义配置,可适配各类复杂业务场景。在实际开发中,建议优先使用内置异常和标准接口,保持代码的规范性和可维护性。

posted @ 2026-01-10 20:18  【唐】三三  阅读(10)  评论(0)    收藏  举报