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 异常处理的更多相关文章

  1. ABP 异常处理 第四篇

    1.ABP异常处理机制是通过过滤器实现的,我们查看的webAPI的异常处理,我们来看看他的源码,AbpApiExceptionFilterAttribute 继承ExceptionFilterAttr ...

  2. ABP异常处理

    1.编译器错误消息: CS0012: 类型“System.Object”在未被引用的程序集中定义.必须添加对程序集“System.Runtime, Version=4.0.0.0, Culture=n ...

  3. Abp vNext异常处理的缺陷/改造方案

    吐槽Abp Vnext异常处理! 哎呀,是一个喷子 目前项目使用Abp VNext开发,免不了要全局处理异常.提示服务器异常信息. 1. Abp官方异常处理 Abp项目默认会启动内置的异常处理,默认不 ...

  4. 基于ABP实现DDD--聚合和聚合根实践

      在下面的例子中涉及Repository.Issue.Label.User这4个聚合根,接下来以Issue聚合为例进行分析,其中Issue聚合是由Issue[聚合根].Comment[实体].Iss ...

  5. ABP(现代ASP.NET样板开发框架)系列之23、ABP展现层——异常处理

    点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之23.ABP展现层——异常处理 ABP是“ASP.NET Boilerplate Project (ASP.NET ...

  6. ABP源码分析四十七:ABP中的异常处理

    ABP 中异常处理的思路是很清晰的.一共五种类型的异常类. AbpInitializationException用于封装ABP初始化过程中出现的异常,只要抛出AbpInitializationExce ...

  7. ABP理论学习之异常处理

    返回总目录 本篇目录 介绍 开启错误处理 非Ajax请求 展示异常信息 UserFriendlyException Error模型 Ajax请求 异常事件 介绍 在一个web应用中,异常通常是在MVC ...

  8. ABP官方文档翻译 6.1.3 异常处理

    处理异常 介绍 启用错误处理 Non-Ajax请求 显示异常 UserFriendlyException Error模型 AJAX请求 异常事件 介绍 此文档是与ASP.NET MVC和Web API ...

  9. [Abp 源码分析]十、异常处理

    0.简介 Abp 框架本身针对内部抛出异常进行了统一拦截,并且针对不同的异常也会采取不同的处理策略.在 Abp 当中主要提供了以下几种异常类型: 异常类型 描述 AbpException Abp 框架 ...

随机推荐

  1. Keil MDK STM32系列(三) 基于标准外设库SPL的STM32F407开发

    Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ...

  2. Solon 开发,八、注入依赖与初始化

    Solon 开发 一.注入或手动获取配置 二.注入或手动获取Bean 三.构建一个Bean的三种方式 四.Bean 扫描的三种方式 五.切面与环绕拦截 六.提取Bean的函数进行定制开发 七.自定义注 ...

  3. linux简单命令汇总

    ls [选项] [文件或目录] -a 显示所有文件,包括隐藏文件 -l 显示详细信息 -d 查看目录属性 -h 人性化显示文件大小 -i 显示inode mkdir [选项] 目录名 -p 递归创建 ...

  4. JUC并发编程与高性能内存队列disruptor实战-下

    并发理论 JMM 概述 Java Memory Model缩写为JMM,直译为Java内存模型,定义了一套在多线程读写共享数据时(成员变量.数组)时,对数据的可见性.有序性和原子性的规则和保障:JMM ...

  5. IoC容器-Bean管理XML方式(注入内部bean和级联赋值)

    注入属性-内部bean和级联赋值 (1)一对多关系:部分和员工 一个部门有多个员工,一个员工属于一个部门 部门是一,员工是多 (2)在实体类之间表示一对多关系 (3)在spring配置文件中进行配置 ...

  6. react diff算法浅析

    diff算法作为Virtual DOM的加速器,其算法的改进优化是React整个界面渲染的基础和性能的保障,同时也是React源码中最神秘的,最不可思议的部分 1.传统diff算法计算一棵树形结构转换 ...

  7. ZooKeeper 授权访问

    ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件.它是一个为分布式应用提供一致性服务的软件,提供的功 ...

  8. linux用户密码过期导致命令执行失败

    背景介绍: 使用zabbix调用系统命令,检查时间同步,发现一直在报错,root 用户执行无异常,问题还是出现zabbix用户上面. [zabbix@test-10-12 ~]$ sudo ntpda ...

  9. [HZOI] 山海经 题解

    0.题目大意 给出一个序列,每次查询一个区间的最大子段和的端点和值.序列长度 \(n \le 10^{5}\) . 1.思路 显然应该使用线段树.题目要求每次求一个区间的最大子段和,那么在线段树节点中 ...

  10. ApacheCN PHP 译文集 20211101 更新

    PHP 入门指南 零.序言 一.PHP 入门 二.数组和循环 三.函数和类 四.数据操作 五.构建 PHP Web 应用 六.搭建 PHP 框架 七.认证与用户管理 八.建立联系人管理系统 使用 PH ...