Abp vNext異常處理的缺陷/改造方案

之前吐槽Abp的用戶/租戶管理模塊!今天我又來了,這次我給Abp官方repo提了一個issue。
目前Website使用Abp vNext開發(fā),免不了要全局處理異常、提示服務器異常信息。
1. Abp官方異常處理
Abp項目默認會啟動內置的異常處理,默認不將異常信息發(fā)送到客戶端。
在AppModule文件ConfigureServices方法中使用以下代碼:
Configure(options?=>
{
????options.SendExceptionsDetailsToClients?=?true;
});
可將異常信息發(fā)送到客戶端:如下圖:
{
"error":?{
"code":?null,
"message":?"ERROR?[42000]?[Cloudera][ImpalaODBC]?(360)?Syntax?error?occurred?during?query?execution:?[HY000]?:?AnalysisException:?Could?not?resolve?column/field?reference:?'ug_fed89221846a42dc8427932b2965a020'\n",
"details":?"OdbcException:?ERROR?[42000]?[Cloudera][ImpalaODBC]?(360)?Syntax?error?occurred?during?query?execution:?[HY000]?:?AnalysisException:?Could?not?resolve?column/field?reference:?'ug_fed89221846a42dc8427932b2965a020'\n\nSTACK?TRACE:?at?Gridsum.EAP.Olap.ExecuteQueryLayer.HandleQueryAsync(QueryContext?queryContext,?CancellationToken?cancellationToken)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.DataQuery/Olap/ExecuteQueryLayer.cs:line?81\n?at?Gridsum.EAP.Olap.DistributedCacheLayer.HandleQueryAsync(QueryContext?queryContext,?CancellationToken?cancellationToken)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.DataQuery/Olap/DistributedCacheLayer.cs:line?50\n?at?Gridsum.EAP.DataQuery.AbstractQueryExecutor`1.ExecuteQueryAsync(TQuery?query,?DistributedCacheEntryOptions?options,?CancellationToken?cancellationToken)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.DataQuery/AbstractQueryExecutor.cs:line?60\n?at?Gridsum.EAP.DataQuery.AbstractQueryExecutor`1.ExecuteQueryAsync(TQuery?query,?CancellationToken?token)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.DataQuery/AbstractQueryExecutor.cs:line?47\n?at?Gridsum.EAP.Application.UserGroupService.ClearUserGroupUserAsync(UserGroupUpdateUserDto?updateDto,?String?idshort,?CancellationToken?cancelToken)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.Application/UserGroup/UserGroupService.cs:line?332\n?at?Gridsum.EAP.Application.UserGroupService.UpdateUserGroupUserAsync(UserGroupUpdateUserDto?updateDto,?CancellationToken?cancelToken)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.Application/UserGroup/UserGroupService.cs:line?370\n?at?Castle.DynamicProxy.AsyncInterceptorBase.ProceedAsynchronous[TResult](IInvocation?invocation,?IInvocationProceedInfo?proceedInfo?"42000]?[Cloudera][ImpalaODBC]?(360?"42000]?[Cloudera][ImpalaODBC]?(360)?Syntax?error?occurred?during?query?execution:?[HY000]?:?AnalysisException:?Could?not?resolve?column/field?reference:?'ug_fed89221846a42dc8427932b2965a020'\n\nSTACK?TRACE:?at?Gridsum.EAP.Olap.ExecuteQueryLayer.HandleQueryAsync(QueryContext?queryContext,?CancellationToken?cancellationToken)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.DataQuery/Olap/ExecuteQueryLayer.cs:line?81\n?at?Gridsum.EAP.Olap.DistributedCacheLayer.HandleQueryAsync(QueryContext?queryContext,?CancellationToken?cancellationToken)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.DataQuery/Olap/DistributedCacheLayer.cs:line?50\n?at?Gridsum.EAP.DataQuery.AbstractQueryExecutor`1.ExecuteQueryAsync(TQuery?query,?DistributedCacheEntryOptions?options,?CancellationToken?cancellationToken)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.DataQuery/AbstractQueryExecutor.cs:line?60\n?at?Gridsum.EAP.DataQuery.AbstractQueryExecutor`1.ExecuteQueryAsync(TQuery?query,?CancellationToken?token)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.DataQuery/AbstractQueryExecutor.cs:line?47\n?at?Gridsum.EAP.Application.UserGroupService.ClearUserGroupUserAsync(UserGroupUpdateUserDto?updateDto,?String?idshort,?CancellationToken?cancelToken)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.Application/UserGroup/UserGroupService.cs:line?332\n?at?Gridsum.EAP.Application.UserGroupService.UpdateUserGroupUserAsync(UserGroupUpdateUserDto?updateDto,?CancellationToken?cancelToken)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.Application/UserGroup/UserGroupService.cs:line?370\n?at?Castle.DynamicProxy.AsyncInterceptorBase.ProceedAsynchronous[TResult")?Syntax?error?occurred?during?query?execution:?[HY000]?:?AnalysisException:?Could?not?resolve?column/field?reference:?'ug_fed89221846a42dc8427932b2965a020'\n\nSTACK?TRACE:?at?Gridsum.EAP.Olap.ExecuteQueryLayer.HandleQueryAsync(QueryContext?queryContext,?CancellationToken?cancellationToken)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.DataQuery/Olap/ExecuteQueryLayer.cs:line?81\n?at?Gridsum.EAP.Olap.DistributedCacheLayer.HandleQueryAsync(QueryContext?queryContext,?CancellationToken?cancellationToken)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.DataQuery/Olap/DistributedCacheLayer.cs:line?50\n?at?Gridsum.EAP.DataQuery.AbstractQueryExecutor`1.ExecuteQueryAsync(TQuery?query,?DistributedCacheEntryOptions?options,?CancellationToken?cancellationToken)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.DataQuery/AbstractQueryExecutor.cs:line?60\n?at?Gridsum.EAP.DataQuery.AbstractQueryExecutor`1.ExecuteQueryAsync(TQuery?query,?CancellationToken?token)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.DataQuery/AbstractQueryExecutor.cs:line?47\n?at?Gridsum.EAP.Application.UserGroupService.ClearUserGroupUserAsync(UserGroupUpdateUserDto?updateDto,?String?idshort,?CancellationToken?cancelToken)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.Application/UserGroup/UserGroupService.cs:line?332\n?at?Gridsum.EAP.Application.UserGroupService.UpdateUserGroupUserAsync(UserGroupUpdateUserDto?updateDto,?CancellationToken?cancelToken)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.Application/UserGroup/UserGroupService.cs:line?370\n?at?Castle.DynamicProxy.AsyncInterceptorBase.ProceedAsynchronous[TResult")\n?at?Volo.Abp.Castle.DynamicProxy.CastleAbpMethodInvocationAdapterWithReturnValue`1.ProceedAsync()\n?at?Volo.Abp.Validation.ValidationInterceptor.InterceptAsync(IAbpMethodInvocation?invocation)\n?at?Volo.Abp.Castle.DynamicProxy.CastleAsyncAbpInterceptorAdapter`1.InterceptAsync[TResult](IInvocation?invocation,?IInvocationProceedInfo?proceedInfo,?Func`3?proceed?"TResult")\n?at?Castle.DynamicProxy.AsyncInterceptorBase.ProceedAsynchronous[TResult](IInvocation?invocation,?IInvocationProceedInfo?proceedInfo?"TResult")\n?at?Volo.Abp.Castle.DynamicProxy.CastleAbpMethodInvocationAdapterWithReturnValue`1.ProceedAsync()\n?at?Volo.Abp.Auditing.AuditingInterceptor.InterceptAsync(IAbpMethodInvocation?invocation)\n?at?Volo.Abp.Castle.DynamicProxy.CastleAsyncAbpInterceptorAdapter`1.InterceptAsync[TResult](IInvocation?invocation,?IInvocationProceedInfo?proceedInfo,?Func`3?proceed?"TResult")\n?at?Castle.DynamicProxy.AsyncInterceptorBase.ProceedAsynchronous[TResult](IInvocation?invocation,?IInvocationProceedInfo?proceedInfo?"TResult")\n?at?Volo.Abp.Castle.DynamicProxy.CastleAbpMethodInvocationAdapterWithReturnValue`1.ProceedAsync()\n?at?Volo.Abp.Uow.UnitOfWorkInterceptor.InterceptAsync(IAbpMethodInvocation?invocation)\n?at?Volo.Abp.Castle.DynamicProxy.CastleAsyncAbpInterceptorAdapter`1.InterceptAsync[TResult](IInvocation?invocation,?IInvocationProceedInfo?proceedInfo,?Func`3?proceed?"TResult")\n?at?Gridsum.EAP.Controllers.UserGroupController.UpdateUserGroupUserAsync(String?id,?CancellationToken?cancelToken)?in?/home/gitlab-runner/builds/ttRjAPVA/0/eap/website/app/src/Gridsum.EAP.HttpApi/Controllers/UserGroupController.cs:line?320\n?at?lambda_method3440(Closure?,?Object?)\n?at?Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper?mapper,?ObjectMethodExecutor?executor,?Object?controller,?Object[]?arguments)\n?at?Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|12_0(ControllerActionInvoker?invoker,?ValueTask`1?actionResultValueTask)\n?at?Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|10_0(ControllerActionInvoker?invoker,?Task?lastTask,?State?next,?Scope?scope,?Object?state,?Boolean?isCompleted)\n?at?Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed?context)\n?at?Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State&?next,?Scope&?scope,?Object&?state,?Boolean&?isCompleted)\n?at?Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|13_0(ControllerActionInvoker?invoker,?Task?lastTask,?State?next,?Scope?scope,?Object?state,?Boolean?isCompleted)\n?at?Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|25_0(ResourceInvoker?invoker,?Task?lastTask,?State?next,?Scope?scope,?Object?state,?Boolean?isCompleted)\n" ,
"data":?null,
"validationErrors":?null
}
}
經過幾天倒騰,發(fā)現(xiàn)Abp vNext的異常處理有幾個問題。
2.Abp異常處理存在的缺陷
并沒有如官方所述:自動處理所有異常,實際需要滿足官方所說的某個條件: 
這就導致當Controller Action方法返回的不是object result時,則根本捕獲不到異常(我們暫時不說middleware產生的異常),這應該算Abp的一個Bug
輸出的異常沒有TraceId, 不利于日志排查 發(fā)送到客戶端的日志字段 message,detail過于詳細冗長,不適合前端顯示
也可以配置SendExceptionsDetailsToClients,不將異常信息發(fā)送到客戶端,但這樣就因噎廢食了。
3. 異常處理的目標
雖然Abp的異常處理有缺陷, 但只是異常信息應用上的缺陷,
Abp異常處理①對異常的劃分、②異常信息的本地化、③出現(xiàn)異常時寫日志 ?支持的還是相當好。
基于Abp的異常處理現(xiàn)狀,考慮做一些改進:
對所有Controller-Action方法捕獲異常, [修復Abp Bug] 在Abp的異常處理結果中添加 TraceId 希望將服務端異常分類,簡化后給到前端;同時也不妨礙開發(fā)者查看詳細異常信息。
4. 揪出Abp異常處理缺陷的根源
Abp異常處理的核心對象AbpExceptionFilter,實現(xiàn)IAsyncExceptionFilter過濾器, ITransientDependency瞬時注入接口。
這是一個ServiceFilterAttribute, 你可以理解有個特性作用在每一個Controller的Action方法上:
[ServiceFilter(typeof(AbpExceptionFilter))]一旦某個Controller的Action方法發(fā)生異常, 會執(zhí)行如下代碼:
public?async?Task?OnExceptionAsync(ExceptionContext?context)
{
???if?(!ShouldHandleException(context))
???{
????????return;
???}
???await?HandleAndWrapException(context);
}
ShouldHandleException(context):監(jiān)測是否應該處理異常,據查該函數(shù)確實存在我上文說的問題:并不能捕獲所有的Action方法的異常。HandleAndWrapException(context):異常處理步驟:根據Abp內置的異常類型,自動確定狀態(tài)碼 (這個在Abp官方文檔有講) 序列化異常對象,并向客戶端輸出如下格式:
{
"error":?{
"code":?null,
"message":?"ERROR?[42000]?[Cloudera][ImpalaODBC]?(360)?Syntax?error?occurred?during?query?execution:?[HY000]?:?AnalysisException:?Could?not?resolve?column/field?reference:?'ug_fed89221846a42dc8427932b2965a020'\n",
"details":xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx......,
"data":?null,
"validationErrors":?null
}
}
① 輸出的信息,從一開始就沒有包含TraceId;
② Abp標準格式化后的異常信息,過于冗長,message,details字段均不適合前端顯示。
寫日志,默認異常級別為Error
掌握以上源碼,我們可以針對性的改造Abp的核心異常處理類AbpExceptionFilter。
5. Abp異常處理: 缺陷修復方案
光說不練假把式
Abp的
AbpExceptionFilter不是抽象類,沒法重載,為達到我們設定的3個目標。
考慮使用針對性的ExceptionFilter替換默認有缺陷的AbpExceptionFilter。
①.? 新建EapExceptionFilter,內容拷貝自AbpExceptionFilter, 并做出如下針對性修改:


②. 在AppModule中,替換默認的AbpExceptionFilter為新的EapExceptionFilter過濾器:
context.Services.AddMvc(options?=>
{
????options.Filters.ReplaceOne(x=>?(x?as?ServiceFilterAttribute)?.ServiceType?.Name==nameof(AbpExceptionFilter),?new?ServiceFilterAttribute(typeof(EapExceptionFilter)));??
})
改造的效果如下:
That's? All
如果大家真切使用了Abp vNext最新版,
相信我在第2點提到的Abp異常處理的缺陷,Abp使用者會感同身受;
第3點提出的幾個目標也是企業(yè)級異常處理要解決的痛點。
此異常處理的思路也可推及到其他非Abp項目.
改造方案在Abp官方github issue上:? https://github.com/abpframework/abp/issues/6761
一家之言,如有其他看法,請不吝賜教!
(btw,公眾號文章發(fā)布之后,限制修改;若有后續(xù),請 [閱讀原文]!)
Reference
https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs https://docs.abp.io/zh-Hans/abp/latest/Exception-Handling https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-5.0 閱讀更多
吐槽一下Abp的用戶和租戶管理模塊 臨近年關,修復ASP.NET Core因瀏覽器內核版本引發(fā)的單點登錄故障 一套標準的ASP.NET Core容器化應用日志收集分析方案 Oh my God, Swagger API文檔竟然可以這樣寫? ASP.NET Core應用注意這一點,CTO會對你刮目相看

關注并星標我們
更多干貨及最佳實踐分享
