Abp 业务异常源码解读

article/2025/4/24 23:58:53

Abp 业务异常源码解读

最近一直在读代码整洁之道,我在读到第三章函数的3.9 使用异常替代返回错误码,其实在我的开发经历中都是使用返回错误码给到前端,之前在阅读ABP官网文档中就有看到过使用异常替代异常的做法,当时自己还是比较抵触,在读完本章之后我们就马上阅读了Abp的异常处理源码。


ABP 提供了一个内置的基础设施,并提供了一个标准模型来处理异常。

  • 自动处理所有异常并向客户端发送标准格式的错误消息以获取 API/AJAX 请求。
  • 自动隐藏内部基础架构错误并返回标准错误消息。
  • 提供一种简单且可配置的方式来本地化异常消息,可以实现多语言返回。
  • 自动将标准异常映射到HTTP 状态代码,并提供一个可配置的选项来映射自定义异常。

业务异常

您自己的大多数异常将是业务异常。该IBusinessException接口用于将异常标记为业务异常。BusinessExceptionIBusinessException除了IHasErrorCode,IHasErrorDetails和接口之外,还实现了IHasLogLevel接口。
默认日志级别是Warning.
特定业务异常相关的错误代码。例如:

throw new BusinessException(QaErrorCodes.CanNotVoteYourOwnAnswer);

QaErrorCodes.CanNotVoteYourOwnAnswer只是一个const string。建议使用以下错误代码格式: code-namespace是特定于您的模块/应用程序的 唯一值。例子:
Volo.Qa:010002
Volo.Qa是这里的代码命名空间。然后将在 本地化异常消息时使用代码命名空间。

  • 您可以在需要时直接抛出BusinessException派生您自己的异常类型。
  • 该类的所有属性都是可选的BusinessException。但是您通常设置ErrorCodeor Message属性。

BusinessException(自定义的业务异常)

下面是我们实现一个自定义异常的代码逻辑

[Serializable]
// 继承异常Exception类(实现自定义异常)
// IBusinessException (标识业务异常)
// IHasErrorCode(实现Code字段)
// IHasErrorDetails(实现Details字段)
// IHasLogLevel(当前异常实现自定义日志等级)
public class BusinessException : Exception,IBusinessException,IHasErrorCode,IHasErrorDetails,IHasLogLevel
{public string Code { get; set; }public string Details { get; set; }public LogLevel LogLevel { get; set; }public BusinessException(string code = null,string message = null,string details = null,Exception innerException = null,LogLevel logLevel = LogLevel.Warning): base(message, innerException){Code = code;Details = details;LogLevel = logLevel;}/// <summary>/// Constructor for serializing./// </summary>public BusinessException(SerializationInfo serializationInfo, StreamingContext context): base(serializationInfo, context){}public BusinessException WithData(string name, object value){Data[name] = value;return this;}
}

本地化资源(实现多语言)

不知道大家没有接触过Abp的多语言设计,Abp通过读取不同国家的语言包Json实现多语言设计

这个是Abp源码中使用多语言的案例,可以看到我们会统一定义一个文件夹保存不同国家的多语言Json

多语言Json结构案例:

culture是语言

texts是Key-Value

{"culture": "zh-Hans","texts": {"Volo.Abp.Http.DynamicProxying:10001": "业务异常"}
}

然后在模块中将语言包文件夹中的Json,添加到本地化中

Configure<AbpLocalizationOptions>(options =>{options.Resources.Add<HttpClientTestResource>("en").AddVirtualJson("/Volo/Abp/Http/Localization");});

设置异常本地化配置(不同的解决方案一定要进行注册,如果没注册就找不到对应的错误码Key)

Configure<AbpExceptionLocalizationOptions>(options =>{// 设置映射解决方案名称,因为考虑到不同的语言包,需要区分模块设计options.MapCodeNamespace("Volo.Abp.Http.DynamicProxying", typeof(HttpClientTestResource));});

结构如下:
我们的Key可以通过解决方案加Code的方式(Volo.Abp.Http.DynamicProxying为解决方案:10001是返回给前端的错误Code)

{"culture": "sl","texts": {"Volo.Abp.Http.DynamicProxying:10001": "Poslovna izjema s podatki","Volo.Abp.Http.TestProxying:10002": "Poslovna izjema s podatki"}
}

然后可以使用错误代码抛出业务异常:

// QaDomainErrorCodes.CanNotVoteYourOwnAnswer="Volo.Abp.Http.DynamicProxying:10001"
// 这样通过一个常量管理异常就简洁明了。
throw new BusinessException(QaDomainErrorCodes.CanNotVoteYourOwnAnswer);

HTTP 状态码映射

ABP 尝试按照以下规则自动确定最适合常见异常类型的 HTTP 状态代码:

  • 对于AbpAuthorizationException:
    • 401如果用户尚未登录,则返回(未经授权)。
    • 如果用户已登录,则返回403(禁止)。
  • 的返回400(错误请求)AbpValidationException。
  • 返回404(未找到)EntityNotFoundException。
  • (并且因为它扩展了)返回403(禁止)。IBusinessExceptionIUserFriendlyExceptionIBusinessException
  • 的返回501(未实现)NotImplementedException。
  • 500其他异常(假定为基础设施异常)的返回(内部服务器错误)。

IHttpExceptionStatusCodeFinder用于自动确定 HTTP 状态码。默认实现是DefaultHttpExceptionStatusCodeFinder类。它可以根据需要更换或扩展。

自定义映射

自定义映射可以覆盖自动 HTTP 状态代码确定。例如:

services.Configure<AbpExceptionHttpStatusCodeOptions>(options =>
{options.Map("Volo.Qa:010002", HttpStatusCode.Conflict);
});

异常事件订阅(ExceptionSubscriber)

下面我们会涉及到处理异常,Abp框架的处理异常给我们提供通知入口ExceptionSubscriber

[ExposeServices(typeof(IExceptionSubscriber))]
// 继承IExceptionSubscriber接口,注入周期Transient(瞬态)
public abstract class ExceptionSubscriber : IExceptionSubscriber, ITransientDependency
{public abstract Task HandleAsync(ExceptionNotificationContext context);
}

我们只需要继承ExceptionSubscriber抽象类,然后Abp将自动注入,一对多的形式进行注入。
触发通知的代码在ExceptionNotifier源码

ExceptionNotifier(异常通知)

下面的代码就是实现异常通知发生事件的代码,我们只需要在异常过滤器中获取ExceptionNotifier然后调用NotifyAsync方法就可以啦

// 异常通知
public class ExceptionNotifier : IExceptionNotifier, ITransientDependency
{public ILogger<ExceptionNotifier> Logger { get; set; }protected IServiceScopeFactory ServiceScopeFactory { get; }public ExceptionNotifier(IServiceScopeFactory serviceScopeFactory){ServiceScopeFactory = serviceScopeFactory;Logger = NullLogger<ExceptionNotifier>.Instance;}// 通知入口public virtual async Task NotifyAsync([NotNull] ExceptionNotificationContext context){Check.NotNull(context, nameof(context));using (var scope = ServiceScopeFactory.CreateScope()){// 1.获取所有实现IExceptionSubscriber接口的实现了类var exceptionSubscribers = scope.ServiceProvider.GetServices<IExceptionSubscriber>();// 2.批量调用实现类的HandleAsync方法foreach (var exceptionSubscriber in exceptionSubscribers){try{await exceptionSubscriber.HandleAsync(context);}catch (Exception e){Logger.LogWarning($"Exception subscriber of type {exceptionSubscriber.GetType().AssemblyQualifiedName} has thrown an exception!");Logger.LogException(e, LogLevel.Warning);}}}}
}

AbpExceptionFilter异常拦截器源码

我们首先可以看到AbpExceptionFilter继承我们的异常拦截器,依赖注入的生命周期是瞬态的

// 我们首先可以看到AbpExceptionFilter继承我们的异常拦截器,依赖注入的生命周期是瞬态的
public class AbpExceptionFilter : IAsyncExceptionFilter, ITransientDependency
{·····省略代码
}

AbpExceptionFilter如果满足以下任何条件,则处理异常:

  • 异常由返回对象结果(不是视图结果)的控制器操作引发。
  • 该请求是一个 AJAX 请求(X-Requested-WithHTTP 标头值为XMLHttpRequest)。
  • 客户端明确接受application/json内容类型(通过acceptHTTP 标头)。

如果异常得到处理,它会自动记录下来,并将格式化的JSON 消息返回给客户端。

// 判断当前请求的异常是否需要自动处理protected virtual bool ShouldHandleException(ExceptionContext context){// 1.判断当前请求是否是控制器方法// 2.并且有返回结果if (context.ActionDescriptor.IsControllerAction() &&context.ActionDescriptor.HasObjectResult()){return true;}// 1.当前请求中头accept是否是application/json内容类型if (context.HttpContext.Request.CanAccept(MimeTypes.Application.Json)){return true;}// 1.当前请求是否是AJAX 请求if (context.HttpContext.Request.IsAjax()){return true;}return false;}

如果ShouldHandleException()方法返回 true就会进入HandleAndWrapException() 自动格式化处理异常方法

// 自动格式化处理异常protected virtual async Task HandleAndWrapException(ExceptionContext context){//TODO: Trigger an AbpExceptionHandled event or something like that.// 1.首先还是老样子读取当前模块的配置信息var exceptionHandlingOptions = context.GetRequiredService<IOptions<AbpExceptionHandlingOptions>>().Value;// 2.获取异常格式转换器,因为需要将我们的异常格式化,多语言实现也是在这个格式化转换器中实现的var exceptionToErrorInfoConverter = context.GetRequiredService<IExceptionToErrorInfoConverter>();// 3.通过格式化转换器,将异常信息转换成为前端展示数据(这里就会使用到我们的配置信息)var remoteServiceErrorInfo = exceptionToErrorInfoConverter.Convert(context.Exception, options =>{// 是否向客户端发送异常详细信息(默认是false)options.SendExceptionsDetailsToClients = exceptionHandlingOptions.SendExceptionsDetailsToClients;// 发送堆栈跟踪到客户端(默认是true)options.SendStackTraceToClients = exceptionHandlingOptions.SendStackTraceToClients;});// 4.获取我们业务异常日志等级var logLevel = context.Exception.GetLogLevel();// 5.创建一个StringBuilder对象拼接异常信息var remoteServiceErrorInfoBuilder = new StringBuilder();remoteServiceErrorInfoBuilder.AppendLine($"---------- {nameof(RemoteServiceErrorInfo)} ----------");remoteServiceErrorInfoBuilder.AppendLine(context.GetRequiredService<IJsonSerializer>().Serialize(remoteServiceErrorInfo, indented: true));// 6.获取日志信息var logger = context.GetService<ILogger<AbpExceptionFilter>>(NullLogger<AbpExceptionFilter>.Instance);logger.LogWithLevel(logLevel, remoteServiceErrorInfoBuilder.ToString());logger.LogException(context.Exception, logLevel);// 7.获取注入IExceptionNotifier接口的实现类,给IExceptionSubscriber实现类接口批量发送事件await context.GetRequiredService<IExceptionNotifier>().NotifyAsync(new ExceptionNotificationContext(context.Exception));// 8.判断当前异常是不是身份认证异常if (context.Exception is AbpAuthorizationException){await context.HttpContext.RequestServices.GetRequiredService<IAbpAuthorizationExceptionHandler>().HandleAsync(context.Exception.As<AbpAuthorizationException>(), context.HttpContext);}else{// 9.添加请求头标识_AbpErrorFormat(给告诉调用者,这次的异常已经是被我们格式化的)context.HttpContext.Response.Headers.Add(AbpHttpConsts.AbpErrorFormat, "true");// 10.设置返回状态码context.HttpContext.Response.StatusCode = (int)context.GetRequiredService<IHttpExceptionStatusCodeFinder>().GetStatusCode(context.HttpContext, context.Exception);// 11.将我们序列化好的错误信息放入请求返回结果中context.Result = new ObjectResult(new RemoteServiceErrorResponse(remoteServiceErrorInfo));}// 12.清空当前请求的异常context.Exception = null; //Handled!}

参考资料

  • Abp 官方文档
  • Abp 源码

http://chatgpt.dhexx.cn/article/D0ZtQoYo.shtml

相关文章

abp快速入门#3

abp快速入门#3 使用AbpHelper.CLI快速实现crud 使用AbpHelper.CLI快速实现crud https://github.com/EasyAbp/AbpHelper.CLI 按照使用说明安装abphelper dotnet tool install -g EasyAbp.AbpHelper参照例子创建Todo实体对象。 执行abphelper自动生成代码&#xff0c;-d 参数为…

abp官网下载的项目如何跑起来

目录 前言 一、pandas是什么&#xff1f; 二、使用步骤 1.下载项目 2.解压压缩包运行文件 3.在项目路径里面找到这两个文件&#xff0c;把数据库位置写上去&#xff0c;例如本地就local host 4.在工具里面找到程序包管理控制台 5.运行成功 6.设置启动项 7.运行成功就会有相…

【转】ABP源码分析三十三:ABP.Web

ABP.Web模块并不复杂&#xff0c;主要完成ABP系统的初始化和一些基础功能的实现。 AbpWebApplication : 继承自ASP.Net的HttpApplication类&#xff0c;主要完成下面三件事 一&#xff0c;在Application_Start完成AbpBootstrapper的初始化。整个ABP系统的初始化就是通过AbpBoo…

ABP学习 之 准备

学习ABP准备 ABP准备数据库准备后端启动前端启动 ABP准备 到ABP官网下载Free Template 将下载到的压缩包解压到目录 由于当时选择使用vue为前端&#xff0c;所以vue子目录是前端代码&#xff0c;aspnet-core为后端代码 数据库准备 准备一个数据库&#xff0c;设置好用户的o…

LoRa及LoRaWAN简介

目录 1、什么是LoRa和LoRaWAN 1.1 LoRa和LoRaWAN的区别 1.2 LoRa扩频技术介绍 1.2.1 什么是扩频技术 1.2.2 扩频技术的作用 1.2.3 扩频技术常用术语介绍 1.3 LoRaWAN帧结构 1.4 硬件方案介绍 1.4.1 终端设备方案 1.4.2 网关方案 2、LoRaWAN网络架构 3、LoRaWAN终端设备分类 …

vue之table表格的合并

目录 vue之table表格的动态合并vue之table表格的动态合并 后台给数据 处理断层vue之table表格的动态合并 后台给数据 前端处理断层根据后台数据合并前三列数据 vue之table表格的动态合并 vue之table表格的动态合并 后台给数据 处理断层 转载于&#xff1a;https://blog.csdn.…

HTML-table表格详解

文章目录 表格表格的说明(创建一个表格)1. 说明2. 设置 表头和表格边框样式设置1. 设置 长表格结构和补充1. 说明2. 结构3. 补充 表格布局(被淘汰)1. 表格布局(已经被 CSS 淘汰) 表格 表格的说明(创建一个表格) <!DOCTYPE html> <html><head><meta char…

HTML中的table表格

表格标签 分为行&#xff08;tr&#xff09;和列&#xff08;td&#xff09;&#xff0c;行及列都可以进行合并操作 table:定义表格 tr:定义行 td:定义列 //先有行&#xff0c;后有列 th:多用于表头&#xff0c;定义表格中…

elementUi——Table表格

Table表格 1、基础表格 在 Table 组件中&#xff0c;每⼀个表格由⼀个 Table-Column 组件构成&#xff0c;也就是表格的列 2、表格常用属性 3、常用属性 4、通过v-for封装适⽤性更好的表格 <el-table :data"tableData"><!-- 循环显示列 --><el-…

table表格的使用(表格的嵌套、合并)

表格基本格式 <table> //第一行 <tr> <td>第一列</td> <td>第二列</td> </tr> //第二行 <tr> <td>第一列</td> <td>第二列</td> </tr> </table> 1.<table></table>&#x…

vue3 antd table表格样式修改——ant design vue table表格的行高调整

vue3 antd项目实战——修改ant design vue table组件的默认样式&#xff08;调整每行行高&#xff09; 知识调用场景复现实际操作解决a-table表格padding过宽 知识调用 文章中可能会用到的知识链接vue3ant design vuets实战【ant-design-vue组件库引入】css样式穿透&#xff0…

web第十课:table表格标签

1.table表格介绍 table是表格标签&#xff0c;里面包含有行tr&#xff0c;行tr里包含td单元格 注意&#xff1a;table里只能包含tr&#xff0c;tr里面也只能包含td&#xff0c;td里面可以包含其他标签 代码演示&#xff1a; 这里在码标签的时候别忘了我们的快捷方式&#xff…

table表格基础

1.table主要用于呈现格式化数据。由行和列组成。 格式&#xff1a; <table><tr><th>表头</th><th>表头</th><th>表头</th></tr><tr><td>列1</td><td>列2</td><td>列3</td>…

手写table表格

<template> <div class"table" style"overflow-y: auto; height:15em; width:100%"> //overflow-y控制y轴滚动<table border"0" width"100%" height"16em" cellpadding"4.8" align"center&qu…

el-table表格还可以这么玩

六年代码两茫茫&#xff0c;不思量&#xff0c;自难忘 6年资深前端主管一枚&#xff0c;只分享技术干货&#xff0c;教你如何优雅地写代码 关注博主不迷路~ 文章目录 前言案例介绍案例截图效果预览template模板script部分css总结 前言 我们都知道表格是横向渲染的&#xff0c;一…

html使table整体居中,如何让整个table表格居中?

早期做网站都离开表格的使用&#xff0c;表格的对齐方式居中又是常用属性。居中最简章的方法就是直接在作用域写上&#xff1a;text-align:center&#xff0c;表格的话直接在table里写上aligncenter就可以了。不过表格已不是当下布局主流&#xff0c;如果习惯于表格布局的同学们…

Html——之table表格

表格&#xff08;table&#xff09;的应用&#xff1a; 表格的创建&#xff1a; <!doctype html> <html> <head> <meta charset"utf-8"> <title>test1</title> <--表格标记--> </head> <body><table>…

前端table表格的用法

文章目录 一、table的基础用法二、table属性三、合并单元格拓展总结 一、table的基础用法 代码如下&#xff1a; <table><tr>//第一行<td>第一行第1列</td><td>第一行第2列</td></tr><tr>//第二行<td>第二行第1列<…

HTML table表格详解

一、表格属性 表格属性 用法 border 代表表格边框厚度 width height 表格宽高 align table tr td 设置水平方向对齐方式 默认值left center right cellspacing 单元格到单元格距离 cellpadding 单元格文字到单元格边框距离 bgcolor 表格背景颜色 table tr td 都可以…

element-ui实现table表格的嵌套(table表格嵌套)功能实现

最近在做电商类型的官网&#xff0c;希望实现的布局如下&#xff1a;有表头和表身&#xff0c;所以我首先想到的就是table表格组件。 表格组件中常见的就是&#xff1a;标题和内容一一对应&#xff1a; 像效果图中的效果&#xff0c;只用基础的表格布局是不行的&#xff0c;因…