[Abp 源码分析]十、异常处理
0.简介
Abp 框架本身针对内部抛出异常进行了统一拦截,并且针对不同的异常也会采取不同的处理策略。在 Abp 当中主要提供了以下几种异常类型:
异常类型 | 描述 |
---|---|
AbpException |
Abp 框架定义的基本异常类型,Abp 所有内部定义的异常类型都继承自本类。 |
AbpInitializationException |
Abp 框架初始化时出现错误所抛出的异常。 |
AbpDbConcurrencyException |
当 EF Core 执行数据库操作时产生了 DbUpdateConcurrencyException 异常的时候 Abp 会封装为本异常并且抛出。 |
AbpValidationException |
用户调用接口时,输入的DTO 参数有误会抛出本异常。 |
BackgroundJobException |
后台作业执行过程中产生的异常。 |
EntityNotFoundException |
当仓储执行 Get 操作时,实体未找到引发本异常。 |
UserFriendlyException |
如果用户需要将异常信息发送给前端,请抛出本异常。 |
AbpRemoteCallException |
远程调用一场,当使用 Abp 提供的 AbpWebApiClient 产生问题的时候会抛出此异常。 |
1.启动流程
Abp 框架针对异常拦截的处理主要使用了 ASP .NET CORE MVC 过滤器机制,当外部请求接口的时候,所有异常都会被 Abp 框架捕获。Abp 异常过滤器的实现名称叫做 AbpExceptionFilter
,它在注入 Abp 框架的时候就已经被注册到了 ASP .NET Core 的 MVC Filters 当中了。
1.1 流程图
1.2 代码流程
注入 Abp 框架处:
public static IServiceProvider AddAbp<TStartupModule>(this IServiceCollection services, [CanBeNull] Action<AbpBootstrapperOptions> optionsAction = null)
where TStartupModule : AbpModule
{
var abpBootstrapper = AddAbpBootstrapper<TStartupModule>(services, optionsAction);
// 配置 ASP .NET Core 参数
ConfigureAspNetCore(services, abpBootstrapper.IocManager);
return WindsorRegistrationHelper.CreateServiceProvider(abpBootstrapper.IocManager.IocContainer, services);
}
ConfigureAspNetCore()
方法内部:
private static void ConfigureAspNetCore(IServiceCollection services, IIocResolver iocResolver)
{
// ...省略掉的其他代码
// 配置 MVC
services.Configure<MvcOptions>(mvcOptions =>
{
mvcOptions.AddAbp(services);
});
// ...省略掉的其他代码
}
AbpMvcOptionsExtensions
扩展类针对 MvcOptions
提供的扩展方法 AddAbp()
:
public static void AddAbp(this MvcOptions options, IServiceCollection services)
{
AddConventions(options, services);
// 添加 VC 过滤器
AddFilters(options);
AddModelBinders(options);
}
AddFilters()
方法内部:
private static void AddFilters(MvcOptions options)
{
// 权限认证过滤器
options.Filters.AddService(typeof(AbpAuthorizationFilter));
// 审计信息过滤器
options.Filters.AddService(typeof(AbpAuditActionFilter));
// 参数验证过滤器
options.Filters.AddService(typeof(AbpValidationActionFilter));
// 工作单元过滤器
options.Filters.AddService(typeof(AbpUowActionFilter));
// 异常过滤器
options.Filters.AddService(typeof(AbpExceptionFilter));
// 接口结果过滤器
options.Filters.AddService(typeof(AbpResultFilter));
}
2.代码分析
2.1 基本定义
Abp 框架所提供的所有异常类型都继承自 AbpException
,我们可以看一下该类型的基本定义。
// Abp 基本异常定义
[Serializable]
public class AbpException : Exception
{
public AbpException()
{
}
public AbpException(SerializationInfo serializationInfo, StreamingContext context)
: base(serializationInfo, context)
{
}
// 构造函数1,接受一个异常描述信息
public AbpException(string message)
: base(message)
{
}
// 构造函数2,接受一个异常描述信息与内部异常
public AbpException(string message, Exception innerException)
: base(message, innerException)
{
}
}
类型的定义是十分简单的,基本上就是继承了原有的 Exception
类型,改了一个名字罢了。
2.2 异常拦截
Abp 本身针对异常信息的核心处理就在于它的 AbpExceptionFilter
过滤器,过滤器实现很简单。它首先继承了 IExceptionFilter
接口,实现了其 OnException()
方法,只要用户请求接口的时候出现了任何异常都会调用 OnException()
方法。而在 OnException()
方法内部,Abp 根据不同的异常类型进行了不同的异常处理。
public class AbpExceptionFilter : IExceptionFilter, ITransientDependency
{
// 日志记录器
public ILogger Logger { get; set; }
// 事件总线
public IEventBus EventBus { get; set; }
// 错误信息构建器
private readonly IErrorInfoBuilder _errorInfoBuilder;
// AspNetCore 相关的配置信息
private readonly IAbpAspNetCoreConfiguration _configuration;
// 注入并初始化内部成员对象
public AbpExceptionFilter(IErrorInfoBuilder errorInfoBuilder, IAbpAspNetCoreConfiguration configuration)
{
_errorInfoBuilder = errorInfoBuilder;
_configuration = configuration;
Logger = NullLogger.Instance;
EventBus = NullEventBus.Instance;
}
// 异常触发时会调用此方法
public void OnException(ExceptionContext context)
{
// 判断是否由控制器触发,如果不是则不做任何处理
if (!context.ActionDescriptor.IsControllerAction())
{
return;
}
// 获得方法的包装特性。决定后续操作,如果没有指定包装特性,则使用默认特性
var wrapResultAttribute =
ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(
context.ActionDescriptor.GetMethodInfo(),
_configuration.DefaultWrapResultAttribute
);
// 如果方法上面的包装特性要求记录日志,则记录日志
if (wrapResultAttribute.LogError)
{
LogHelper.LogException(Logger, context.Exception);
}
// 如果被调用的方法上的包装特性要求重新包装错误信息,则调用 HandleAndWrapException() 方法进行包装
if (wrapResultAttribute.WrapOnError)
{
HandleAndWrapException(context);
}
}
// 处理并包装异常
private void HandleAndWrapException(ExceptionContext context)
{
// 判断被调用接口的返回值是否符合标准,不符合则直接返回
if (!ActionResultHelper.IsObjectResult(context.ActionDescriptor.GetMethodInfo().ReturnType))
{
return;
}
// 设置 HTTP 上下文响应所返回的错误代码,由具体异常决定。
context.HttpContext.Response.StatusCode = GetStatusCode(context);
// 重新封装响应返回的具体内容。采用 AjaxResponse 进行封装
context.Result = new ObjectResult(
new AjaxResponse(
_errorInfoBuilder.BuildForException(context.Exception),
context.Exception is AbpAuthorizationException
)
);
// 触发异常处理事件
EventBus.Trigger(this, new AbpHandledExceptionData(context.Exception));
// 处理完成,将异常上下文的内容置为空
context.Exception = null; //Handled!
}
// 根据不同的异常类型返回不同的 HTTP 错误码
protected virtual int GetStatusCode(ExceptionContext context)
{
if (context.Exception is AbpAuthorizationException)
{
return context.HttpContext.User.Identity.IsAuthenticated
? (int)HttpStatusCode.Forbidden
: (int)HttpStatusCode.Unauthorized;
}
if (context.Exception is AbpValidationException)
{
return (int)HttpStatusCode.BadRequest;
}
if (context.Exception is EntityNotFoundException)
{
return (int)HttpStatusCode.NotFound;
}
return (int)HttpStatusCode.InternalServerError;
}
}
以上就是 Abp 针对异常处理的具体操作了,在这里面涉及到的 WrapResultAttribute
、 AjaxResponse
、 IErrorInfoBuilder
都会在后面说明,但是具体的逻辑已经在过滤器所体现了。
2.3 接口返回值包装
Abp 针对所有 API 返回的数据都会进行一次包装,使得其返回值内容类似于下面的内容。
{
"result": {
"totalCount": 0,
"items": []
},
"targetUrl": null,
"success": true,
"error": null,
"unAuthorizedRequest": false,
"__abp": true
}
其中的 result
节点才是你接口真正返回的内容,其余的 targetUrl
之类的都是属于 Abp 包装器给你进行封装的。
2.3.1 包装器特性
其中,Abp 预置的包装器有两种,第一个是 WrapResultAttribute
。它有两个 bool
类型的参数,默认均为 true
,一个叫 WrapOnSuccess
一个 叫做 WrapOnError
,分别用于确定成功或则失败后是否包装具体信息。像之前的 OnException()
方法里面就有用该值进行判断是否包装异常信息。
除了 WarpResultAttribute
特性,还有一个 DontWrapResultAttribute
的特性,该特性直接继承自 WarpResultAttribute
,只不过它的 WrapOnSuccess
与 WrapOnError
都为 fasle
状态,也就是说无论接口调用结果是成功还是失败,都不会进行结果包装。该特性可以直接打在接口方法、控制器、接口之上,类似于这样:
public class TestApplicationService : ApplicationService
{
[DontWrapResult]
public async Task<string> Get()
{
return await Task.FromResult("Hello World");
}
}
那么这个接口的返回值就不会带有其他附加信息,而直接会按照 Json 来序列化返回你的对象。
在拦截异常的时候,如果你没有给接口方法打上 DontWarpResult
特性,那么他就会直接使用 IAbpAspNetCoreConfiguration
的 DefaultWrapResultAttribute
属性指定的默认特性,该默认特性如果没有显式指定则为 WrapResultAttribute
。
public AbpAspNetCoreConfiguration()
{
DefaultWrapResultAttribute = new WrapResultAttribute();
// ...IAbpAspNetCoreConfiguration 的默认实现的构造函数
// ...省略掉了其他代码
}
2.3.2 具体包装行为
Abp 针对正常的接口数据返回与异常数据返回都是采用的 AjaxResponse
来进行封装的,转到其基类的定义可以看到在里面定义的那几个属性就是我们接口返回出来的数据。
public abstract class AjaxResponseBase
{
// 目标 Url 地址
public string TargetUrl { get; set; }
// 接口调用是否成功
public bool Success { get; set; }
// 当接口调用失败时,错误信息存放在此处
public ErrorInfo Error { get; set; }
// 是否是未授权的请求
public bool UnAuthorizedRequest { get; set; }
// 用于标识接口是否基于 Abp 框架开发
public bool __abp { get; } = true;
}
So,从刚才的 2.2 节 可以看到他是直接 new
了一个 AjaxResponse
对象,然后使用 IErrorInfoBuilder
来构建了一个 ErrorInfo
错误信息对象传入到 AjaxResponse
对象当中并且返回。
那么问题来了,这里的 IErrorInfoBuilder
是怎样来进行包装的呢?
2.3.3 异常包装器
当 Abp 捕获到异常之后,会通过 IErrorInfoBuilder
的 BuildForException()
方法来将异常转换为 ErrorInfo
对象。它的默认实现只有一个,就是 ErrorInfoBuilder
,内部结构也很简单,其 BuildForException()
方法直接通过内部的一个转换器进行转换,也就是 IExceptionToErrorInfoConverter
,直接调用的 IExceptionToErrorInfoConverter.Convert()
方法。
同时它拥有另外一个方法,叫做 AddExceptionConverter()
,可以传入你自己实现的异常转换器。
public class ErrorInfoBuilder : IErrorInfoBuilder, ISingletonDependency
{
private IExceptionToErrorInfoConverter Converter { get; set; }
public ErrorInfoBuilder(IAbpWebCommonModuleConfiguration configuration, ILocalizationManager localizationManager)
{
// 异常包装器默认使用的 DefaultErrorInfoConverter 来进行转换
Converter = new DefaultErrorInfoConverter(configuration, localizationManager);
}
// 根据异常来构建异常信息
public ErrorInfo BuildForException(Exception exception)
{
return Converter.Convert(exception);
}
// 添加用户自定义的异常转换器
public void AddExceptionConverter(IExceptionToErrorInfoConverter converter)
{
converter.Next = Converter;
Converter = converter;
}
}
2.3.4 异常转换器
Abp 要包装异常,具体的操作是由转换器来决定的,Abp 实现了一个默认的转换器,叫做 DefaultErrorInfoConverter
,在其内部,注入了 IAbpWebCommonModuleConfiguration
配置项,而用户可以通过配置该选项的 SendAllExceptionsToClients
属性来决定是否将异常输出给客户端。
我们先来看一下他的 Convert()
核心方法:
public ErrorInfo Convert(Exception exception)
{
// 封装 ErrorInfo 对象
var errorInfo = CreateErrorInfoWithoutCode(exception);
// 如果具体的异常实现有 IHasErrorCode 接口,则将错误码也封装到 ErrorInfo 对象内部
if (exception is IHasErrorCode)
{
errorInfo.Code = (exception as IHasErrorCode).Code;
}
return errorInfo;
}
核心十分简单,而 CreateErrorInfoWithoutCode()
方法内部呢也是一些具体的逻辑,根据异常类型的不同,执行不同的转换逻辑。
private ErrorInfo CreateErrorInfoWithoutCode(Exception exception)
{
// 如果要发送所有异常,则使用 CreateDetailedErrorInfoFromException() 方法进行封装
if (SendAllExceptionsToClients)
{
return CreateDetailedErrorInfoFromException(exception);
}
// 如果有多个异常,并且其内部异常为 UserFriendlyException 或者 AbpValidationException 则将内部异常拿出来放在最外层进行包装
if (exception is AggregateException && exception.InnerException != null)
{
var aggException = exception as AggregateException;
if (aggException.InnerException is UserFriendlyException ||
aggException.InnerException is AbpValidationException)
{
exception = aggException.InnerException;
}
}
// 如果一场类型为 UserFriendlyException 则直接通过 ErrorInfo 构造函数进行构建
if (exception is UserFriendlyException)
{
var userFriendlyException = exception as UserFriendlyException;
return new ErrorInfo(userFriendlyException.Message, userFriendlyException.Details);
}
// 如果为参数类一场,则使用不同的构造函数进行构建,并且在这里可以看到他通过 L 函数调用的多语言提示
if (exception is AbpValidationException)
{
return new ErrorInfo(L("ValidationError"))
{
ValidationErrors = GetValidationErrorInfos(exception as AbpValidationException),
Details = GetValidationErrorNarrative(exception as AbpValidationException)
};
}
// 如果是实体未找到的异常,则包含具体的实体类型信息与实体 ID 值
if (exception is EntityNotFoundException)
{
var entityNotFoundException = exception as EntityNotFoundException;
if (entityNotFoundException.EntityType != null)
{
return new ErrorInfo(
string.Format(
L("EntityNotFound"),
entityNotFoundException.EntityType.Name,
entityNotFoundException.Id
)
);
}
return new ErrorInfo(
entityNotFoundException.Message
);
}
// 如果是未授权的一场,一样的执行不同的操作
if (exception is Abp.Authorization.AbpAuthorizationException)
{
var authorizationException = exception as Abp.Authorization.AbpAuthorizationException;
return new ErrorInfo(authorizationException.Message);
}
// 除了以上这几个固定的异常需要处理之外,其他的所有异常统一返回内部服务器错误信息。
return new ErrorInfo(L("InternalServerError"));
}
所以整体异常处理还是比较复杂的,进行了多层封装,但是结构还是十分清晰的。
3.扩展
3.1 显示额外的异常信息
如果你需要在调用接口而产生异常的时候展示异常的详细信息,可以通过在启动模块的 PreInitialize()
(预加载方法) 当中加入 Configuration.Modules.AbpWebCommon().SendAllExceptionsToClients = true;
即可,例如:
[DependsOn(typeof(AbpAspNetCoreModule))]
public class TestWebStartupModule : AbpModule
{
public override void PreInitialize()
{
Configuration.Modules.AbpWebCommon().SendAllExceptionsToClients = true;
}
}
3.2 监听异常事件
使用 Abp 框架的时候,你可以随时通过监听 AbpHandledExceptionData
事件来使用自己的逻辑处理产生的异常。比如说产生异常时向监控服务报警,或者说将异常信息持久化到其他数据库等等。
你只需要编写如下代码即可实现监听异常事件:
public class ExceptionEventHandler : IEventHandler<AbpHandledExceptionData>, ITransientDependency
{
/// <summary>
/// Handler handles the event by implementing this method.
/// </summary>
/// <param name="eventData">Event data</param>
public void HandleEvent(AbpHandledExceptionData eventData)
{
Console.WriteLine($"当前异常信息为:{eventData.Exception.Message}");
}
}
如果你觉得看的有点吃力的话,可以跳转到 这里 了解 Abp 的事件总线实现。
4.点此跳转到总目录
[Abp 源码分析]十、异常处理的更多相关文章
- C# DateTime的11种构造函数 [Abp 源码分析]十五、自动审计记录 .Net 登陆的时候添加验证码 使用Topshelf开发Windows服务、记录日志 日常杂记——C#验证码 c#_生成图片式验证码 C# 利用SharpZipLib生成压缩包 Sql2012如何将远程服务器数据库及表、表结构、表数据导入本地数据库
C# DateTime的11种构造函数 别的也不多说没直接贴代码 using System; using System.Collections.Generic; using System.Glob ...
- ABP源码分析十:Unit Of Work
ABP以AOP的方式实现UnitOfWork功能.通过UnitOfWorkRegistrar将UnitOfWorkInterceptor在某个类被注册到IOCContainner的时候,一并添加到该类 ...
- ABP源码分析十二:本地化
本文逐个分析ABP中涉及到locaization的接口和类,以及相互之间的关系.本地化主要涉及两个方面:一个是语言(Language)的管理,这部分相对简单.另一个是语言对应得本地化资源(Locali ...
- ABP源码分析十四:Entity的设计
IEntity<TPrimaryKey>: 封装了PrimaryKey:Id,这是一个泛型类型 IEntity: 封装了PrimaryKey:Id,这是一个int类型 Entity< ...
- ABP源码分析十五:ABP中的实用扩展方法
类名 扩展的类型 方法名 参数 作用 XmlNodeExtensions XmlNode GetAttributeValueOrNull attributeName Gets an attribu ...
- ABP源码分析十六:DTO的设计
IDTO:空接口,用于标注Dto对象. ComboboxItemDto:用于combobox/list中Item的DTO NameValueDto<T>/NameValueDto:用于na ...
- ABP源码分析十八:UI Inputs
以下图中描述的接口和类都在Abp项目的Runtime/Validation, UI/Inputs目录下的.在当前版本的ABP(0.83)中这些接口和类并没有实际使用到.阅读代码时可以忽略,无需浪费时间 ...
- ABP源码分析十九:Auditing
审计跟踪(也叫审计日志)是与安全相关的按照时间顺序的记录,它们提供了活动序列的文档证据,这些活动序列可以在任何时间影响一个特定的操作. AuditInfo:定义如下图中需要被Audit的信息. Aud ...
- [Abp 源码分析]十二、多租户体系与权限验证
0.简介 承接上篇文章我们会在这篇文章详细解说一下 Abp 是如何结合 IPermissionChecker 与 IFeatureChecker 来实现一个完整的多租户系统的权限校验的. 1.多租户的 ...
随机推荐
- virtualenv Mac版
环境 MAC python 3.6.7 安装python python官网下载3.6.7版本,默认安装 安装完成后检查是否安装成功: python3.6 确认安装目录:which python3.6 ...
- C# Post方式下,取得其它端传过来的数据
// Post方式下,取得java端传过来的数据 if ("post".Equals(context.Request.HttpMethod.ToLower())) { contex ...
- Delphi中DataSet和JSON的互转
//1)数据集转换为JSON字符串://需USES System.JSON; function DataSetToJson(ADataset: TDataSet): string; // [{&quo ...
- 难受的ESlint语法检测
相信写过vue的各位小白都有过这样的体验,明明引入的文件语法是对的,明明自己写的代码是对的,但是总会报语法错误,没错,就是ESlint代码检测搞的鬼, 就算你在注释后面多打一个空格,它都会去搞事情,简 ...
- 关于<软件>的定义
百度百科: 软件是一系列按照特定顺序组织的计算机数据和指令的集合.一般来讲软件被划分为系统软件.应用软件和介于这两者之间的中间件. 国标中的定义: 与计算机系统操作有关的计算机程序.规程.规则,以及可 ...
- scrapy的持久化相关
终端指令的持久化存储 保证爬虫文件的parse方法中有可迭代类型对象(通常为列表or字典)的返回,该返回值可以通过终端指令的形式写入指定格式的文件中进行持久化操作. 需求是:将糗百首页中段子的内容和标 ...
- JAVA基础复习与总结<四> 抽象类与接口
抽象类(Abstract Class) 是一种模版模式.抽象类为所有子类提供了一个通用模版,子类可以在这个模版基础上进行扩展.通过抽象类,可以避免子类设计的随意性.通过抽象类,我们就可以做到严格限制子 ...
- Django合集
Django基础 Django--简介 Django--web框架简介 浅析uWSGI.uwsgi.wsgi Django--url(路由)配置 Django--模板层 Django--视图层 Dja ...
- BZOJ3497 : Pa2009 Circular Game
令先手为$A$,后手为$B$,将相邻同色棋子合并成块,首先特判一些情况: 如果所有格子都是满的,那么显然$A$必败. 否则如果所有块都只有一个棋子,那么显然平局. 枚举$A$的第一步操作,如果可以使得 ...
- QT—QTextEdit控件显示日志
功能:利用QTextEdit开发一个日志显示窗口.没有太多操作,需要实现的是日志自动向上滚动,总体的日志量可以控制在x行(比如300行)以内:其他的应用功能我后面继续添加 #include <Q ...