[Abp 源码分析]异常处理

点击上方蓝字关注我们

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

异常类型描述
AbpExceptionAbp 框架定义的基本异常类型,Abp 所有内部定义的异常类型都继承自本类。
AbpInitializationExceptionAbp 框架初始化时出现错误所抛出的异常。
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)
{// ...省略掉的其他代码// 配置 MVCservices.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 的事件总线实现。

作者:myzony

出处:https://www.cnblogs.com/myzony/p/9460021.html

公众号“码侠江湖”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!

扫描二维码

获取更多精彩

码侠江湖

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/305051.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

phpstorm设置 打开文件所在目录_PDF文件在线分享并设置打开次数

公司有一些重要的PDF文档&#xff0c;怎么才能防止外泄&#xff1f;如何加密PDF文件防止被人复制了&#xff1f;怎样让PDF不能被别人拷贝啊&#xff1f; 这些问题困扰了一部分想让别人看pdf内容又担心别人泄露的人。本文用maipdf网站的例子来进行设置。首先我们打开maipdf的网站…

荐号 | 11个人工智能与大数据相关的个人、企业优质号

AlphaGo Zero都会自学了&#xff0c;作为刚刚步入AI大门的我们&#xff0c;应该如何选择合适自己的知识平台呢&#xff1f;今天小编为你甄选了几个高质量的技术公众号。 这些号更多的不是讲授枯燥的理论&#xff0c;而是从行业资讯、一线技术、应用案例、职场发展等多个角度分享…

以表达式作为参数传入SQL的存储过程中去

在开发过程中&#xff0c;需要把一句Sql 的expression作为一个参数传入Procedure中去。 在asp.net中&#xff0c;一个Search的动作&#xff0c;把用户所操作的搜索条件写成了一句表达式&#xff0c;现只需要把这句表达式传入存储过程中去&#xff0c;与存储过程中的Sql的Select…

围棋中的数学原理

围棋一向被誉为是人类大脑智慧的专利&#xff0c;围棋的走法&#xff0c;几乎和宇宙中原子数量相同&#xff0c;甚至更多&#xff0c;每回合有250种可能&#xff0c;一盘棋可以长达150个回合&#xff0c;共有1后面再加360个0种下法&#xff0c;这样的计算量&#xff0c;对计算机…

spring boot 缓存_Spring Boot 集成 Redis 实现数据缓存

Spring Boot 集成 Redis 实现数据缓存&#xff0c;只要添加一些注解方法&#xff0c;就可以动态的去操作缓存了&#xff0c;减少代码的操作。在这个例子中我使用的是 Redis&#xff0c;其实缓存类型还有很多&#xff0c;例如 Ecache、Mamercache、Caffeine 等。Redis 简介Redi…

.md是什么文件_Element-UI源码阅读之md显示到页面

入口文件首先&#xff0c;看一个项目的入口&#xff0c;可以从package.json中去看它的运行命令 可以看到dev那行&#xff0c;执行了很多脚本&#xff1a; npm run bootstrap && npm run build:file && cross-env NODE_ENVdevelopment webpack-dev-server --con…

linux误删ssh不上,误删openssh-server删除,复原操作

在别的节点上寻找openssh的程序有哪些&#xff1a;而我的这个节点上只有一个openssh-clines&#xff0c;所以&#xff0c;就先利用了yum install openssh-server进行安装&#xff0c;但是这个安装的是最新的 openssh-server-5.3p1-122.el6.x86_64.rpm&#xff0c;担心集群节点之…

我用 MySQL 干掉了一摞简历

临近过年&#xff0c;疫情又趋于平稳&#xff0c;最近出来面试的人也多了起来&#xff0c;我们公司也在招人。我发现&#xff0c;不少候选人&#xff0c;对数据库的认知还处在比较基础的阶段&#xff0c;以为会写“增删改查”、做表关联就足够了&#xff0c;那些工作中经常出现…

为什么AI工程师成为当前薪资最高的技术岗位

今年无疑是“人工智能”话题火热指数最高的一年。从腾讯西雅图AI实验室&#xff0c;百度斥资超过200亿投入人工智能研发&#xff0c;再到最近 Google 打算在中国进行AI领域的市场扩张&#xff0c;其母公司 Alphabet 更是在7月便成立专注AI领域的风投机构...全球都在风生水起、将…

spring boot jar包_「Spring Boot 新特性」 jar 大小自动瘦身

自动分析瘦身Spring Boot 项目最终构建处理 JAR 包大小一直是个诟病&#xff0c;需要把所有依赖包内置最终输出可运行的 jar。当然可以使用其他的插件扩展 实现依赖 JAR 和 可运行 jar 分离可以参考 slot-maven-plugin[1], 但此种方法治标不治本并不能减少原有依赖的 JAR 的大小…

设计模式在项目中的应用案例_设计模式在项目中的应用(初学者版)

文章首发链接&#xff1a;设计模式在项目中的实际应用&#xff08;应试版&#xff09;​mp.weixin.qq.com本文适用于设计模式初学者。很多人学习了设计模式&#xff0c;但在项目开发中仍然不知道如何使用&#xff1b;很多小伙伴在课堂上跟着老师稀里糊涂的听了&#xff0c;懂了…

微创社001期:从0开始创作第一本技术书

互联网已经成为了人们生活中如饮水空气一般的基础设施&#xff0c;它不仅影响着当下我们的生活&#xff0c;也必将在更加普遍的领域影响着我们的未来。而在大浪潮中&#xff0c;坚守一个以自我为中心的知识体系&#xff0c;不仅有助于我们作为个体更好的从互联网汲取养分&#…

关于虚拟机下linux共享Windows文件的解决方案

先跟大家说一下在windows下装的vmware&#xff0c;就是又在vmware的系统里虚拟了一个Linux的系统的话&#xff0c;我们有些朋友就是想法把物理机的东西拷贝到Linux虚拟机里时很是伤神&#xff0c;当然windows就很简单了&#xff0c;直接拖进去&#xff08;如果你安装了vmware t…

10分钟让你快速掌握Excel的16项重要技巧

大家在日常生活、工作使用Excel时&#xff0c;是不是总会遇到很多问题呢? 不小心将文件命名错了&#xff0c;要一个个改&#xff1f; 文件里只需要数据却不知道怎么提取&#xff1f; 工作汇报要做数据汇总、需要插入大量图片怎么办&#xff1f; 甚至在耗费相当多的时间与精力…

linux中wine yum安装,分享|在基于RedHat或Debian的系统上安装 Wine 1.7

Wine,Linux上最流行也是最有力的软件, 可以顺利地在Linux平台上运行Windows程序和游戏。这篇文章教你怎么在像CentOS, Fedora, Ubuntu, Linux Mint一样基于Red Hat和Debian的系统上安装最新的Wine 1.7。在Linux安装 Wine 1.7不幸的, 在基于Red Hat的系统上没有官方的 Wine 仓库…

还不知道这 11 个超酷的编程新工具你就 out 了!

工具对开发人员来说至关重要。工具可以让一个开发人员的日常工作更高效&#xff0c;并且只需要关注最重要的事情。对于开发人员来讲&#xff0c;寻找更好的替代工具往往比坚持使用熟悉过时的工具更困难。 在这篇文章中&#xff0c;我们将列出你在日常工作中能够使用的新的开发工…

Linux装多个apache,windows linux如何安装多个apache?

富国沪深Windows下安装多个Apache服务&#xff1a;1.安装好Apache以后&#xff0c;可以在浏览器中输入http://localhost测试&#xff1b;2.更改第一个站点的根目录&#xff1a;在文件Apache2.2\conf\httpd.conf中查找 DocumentRoot 属性&#xff0c;将后面的路径改为你的主站点…

[Abp 源码分析]DTO 自动验证

点击上方蓝字关注我们0.简介在平时开发 API 接口的时候需要对前端传入的参数进行校验之后才能进入业务逻辑进行处理&#xff0c;否则一旦前端传入一些非法/无效数据到 API 当中&#xff0c;轻则导致程序报错&#xff0c;重则导致整个业务流程出现问题。用过传统 ASP.NET MVC 数…

五分钟搞懂并查集

并查集是我暑假从高手那里学到的一招&#xff0c;觉得真是太精妙的设计了。来看一个实例&#xff0c;杭电1232畅通工程。首先在地图上给你若干个城镇&#xff0c;这些城镇都可以看作点&#xff0c;然后告诉你哪些对城镇之间是有道路直接相连的。最后要解决的是整幅图的连通性问…

[Abp 源码分析]多语言(本地化)处理

点击上方蓝字关注我们0.简介如果你所开发的需要走向世界的话&#xff0c;那么肯定需要针对每一个用户进行不同的本地化处理&#xff0c;有可能你的客户在日本&#xff0c;需要使用日语作为显示文本&#xff0c;也有可能你的客户在美国&#xff0c;需要使用英语作为显示文本。如…