Abp 异常处理
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 异常处理 第四篇
1.ABP异常处理机制是通过过滤器实现的,我们查看的webAPI的异常处理,我们来看看他的源码,AbpApiExceptionFilterAttribute 继承ExceptionFilterAttr ...
- ABP异常处理
1.编译器错误消息: CS0012: 类型“System.Object”在未被引用的程序集中定义.必须添加对程序集“System.Runtime, Version=4.0.0.0, Culture=n ...
- Abp vNext异常处理的缺陷/改造方案
吐槽Abp Vnext异常处理! 哎呀,是一个喷子 目前项目使用Abp VNext开发,免不了要全局处理异常.提示服务器异常信息. 1. Abp官方异常处理 Abp项目默认会启动内置的异常处理,默认不 ...
- 基于ABP实现DDD--聚合和聚合根实践
在下面的例子中涉及Repository.Issue.Label.User这4个聚合根,接下来以Issue聚合为例进行分析,其中Issue聚合是由Issue[聚合根].Comment[实体].Iss ...
- ABP(现代ASP.NET样板开发框架)系列之23、ABP展现层——异常处理
点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之23.ABP展现层——异常处理 ABP是“ASP.NET Boilerplate Project (ASP.NET ...
- ABP源码分析四十七:ABP中的异常处理
ABP 中异常处理的思路是很清晰的.一共五种类型的异常类. AbpInitializationException用于封装ABP初始化过程中出现的异常,只要抛出AbpInitializationExce ...
- ABP理论学习之异常处理
返回总目录 本篇目录 介绍 开启错误处理 非Ajax请求 展示异常信息 UserFriendlyException Error模型 Ajax请求 异常事件 介绍 在一个web应用中,异常通常是在MVC ...
- ABP官方文档翻译 6.1.3 异常处理
处理异常 介绍 启用错误处理 Non-Ajax请求 显示异常 UserFriendlyException Error模型 AJAX请求 异常事件 介绍 此文档是与ASP.NET MVC和Web API ...
- [Abp 源码分析]十、异常处理
0.简介 Abp 框架本身针对内部抛出异常进行了统一拦截,并且针对不同的异常也会采取不同的处理策略.在 Abp 当中主要提供了以下几种异常类型: 异常类型 描述 AbpException Abp 框架 ...
随机推荐
- Keil MDK STM32系列(三) 基于标准外设库SPL的STM32F407开发
Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ...
- Solon 开发,八、注入依赖与初始化
Solon 开发 一.注入或手动获取配置 二.注入或手动获取Bean 三.构建一个Bean的三种方式 四.Bean 扫描的三种方式 五.切面与环绕拦截 六.提取Bean的函数进行定制开发 七.自定义注 ...
- linux简单命令汇总
ls [选项] [文件或目录] -a 显示所有文件,包括隐藏文件 -l 显示详细信息 -d 查看目录属性 -h 人性化显示文件大小 -i 显示inode mkdir [选项] 目录名 -p 递归创建 ...
- JUC并发编程与高性能内存队列disruptor实战-下
并发理论 JMM 概述 Java Memory Model缩写为JMM,直译为Java内存模型,定义了一套在多线程读写共享数据时(成员变量.数组)时,对数据的可见性.有序性和原子性的规则和保障:JMM ...
- IoC容器-Bean管理XML方式(注入内部bean和级联赋值)
注入属性-内部bean和级联赋值 (1)一对多关系:部分和员工 一个部门有多个员工,一个员工属于一个部门 部门是一,员工是多 (2)在实体类之间表示一对多关系 (3)在spring配置文件中进行配置 ...
- react diff算法浅析
diff算法作为Virtual DOM的加速器,其算法的改进优化是React整个界面渲染的基础和性能的保障,同时也是React源码中最神秘的,最不可思议的部分 1.传统diff算法计算一棵树形结构转换 ...
- ZooKeeper 授权访问
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件.它是一个为分布式应用提供一致性服务的软件,提供的功 ...
- linux用户密码过期导致命令执行失败
背景介绍: 使用zabbix调用系统命令,检查时间同步,发现一直在报错,root 用户执行无异常,问题还是出现zabbix用户上面. [zabbix@test-10-12 ~]$ sudo ntpda ...
- [HZOI] 山海经 题解
0.题目大意 给出一个序列,每次查询一个区间的最大子段和的端点和值.序列长度 \(n \le 10^{5}\) . 1.思路 显然应该使用线段树.题目要求每次求一个区间的最大子段和,那么在线段树节点中 ...
- ApacheCN PHP 译文集 20211101 更新
PHP 入门指南 零.序言 一.PHP 入门 二.数组和循环 三.函数和类 四.数据操作 五.构建 PHP Web 应用 六.搭建 PHP 框架 七.认证与用户管理 八.建立联系人管理系统 使用 PH ...