惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
爱范儿
爱范儿
J
Java Code Geeks
L
LangChain Blog
V
V2EX
大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
博客园 - Franky
Microsoft Azure Blog
Microsoft Azure Blog
Jina AI
Jina AI
Blog — PlanetScale
Blog — PlanetScale
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The Cloudflare Blog
博客园 - 司徒正美
B
Blog
G
Google Developers Blog
Stack Overflow Blog
Stack Overflow Blog
罗磊的独立博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Apple Machine Learning Research
Apple Machine Learning Research
Engineering at Meta
Engineering at Meta
MyScale Blog
MyScale Blog
有赞技术团队
有赞技术团队
Hugging Face - Blog
Hugging Face - Blog

博客园 - 【唐】三三

《python编程从入门到实践》15 生成数据 《python编程从入门到实践》10-11章 文件和异常、测试代码 《python编程从入门到实践》8-9章 函数、类 fastcode 代码理解 AI 框架 Oh-My-OpenCode 3.5.6 完整使用指南 AI工具免费模型排名对比 (2026-02-12) ABP演示 - 分布式 Event Bus(使用rabbitmq) ABP演示 - 工作单元 (UOW) ABP演示 - 缓存 APP - 实体变更监视事件 [EntityCreatedEventData、EntityUpdatedEventData、EntityDeletedEventData] ABP演示 - 授权 ABP演示 - 本地Event Bus ABP演示 - 实现选项 OPtion ABP演示 - 简单的实现DDD LINQ:SelectMany LINQ - Concat、Union、Intersect、Except IEqualityComparer Nginx - 内置变量 vite - vue 打包 vue - 以deifne开头的 API vue - 进阶
ABP演示 - 异常处理
【唐】三三 · 2026-01-10 · via 博客园 - 【唐】三三

异常处理是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的异常处理机制遵循“约定优于配置”的原则,大部分场景下无需复杂配置即可满足需求,同时通过接口扩展和自定义配置,可适配各类复杂业务场景。在实际开发中,建议优先使用内置异常和标准接口,保持代码的规范性和可维护性。