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

_小碼甲發表於2020-12-23

吐槽Abp Vnext異常處理! 哎呀,是一個噴子

目前專案使用Abp VNext開發,免不了要全域性處理異常、提示伺服器異常資訊。

1. Abp官方異常處理

Abp專案預設會啟動內建的異常處理,預設不將異常資訊傳送到客戶端。
在AppModule檔案ConfigureServices方法中使用以下程式碼:

 Configure<AbpExceptionHandlingOptions>(options =>
{
      options.SendExceptionsDetailsToClients = true;
});

可將異常資訊傳送到客戶端:如下圖:

{
"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.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>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.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)\n at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextExceptionFilterAsync>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)\n",
"data": null,
"validationErrors": null
}
}

經過幾天倒騰,發現Abp VNext的異常處理有幾個問題。

2.Abp異常處理存在的缺陷

  1. 並沒有如官方所述:自動處理所有異常,需要滿足官方所說的任意一個條件

這就導致當Controller Action方法返回型別不是object result(並有異常丟擲)時,則根本捕獲不到異常,這應該算Abp的一個Bug

  1. 輸出的異常沒有TraceId, 不利於日誌排查
  2. 傳送到客戶端的日誌欄位message,detail欄位過於詳細冗長,不能用於前端顯示

當然你也可以使用上面所說的配置:不將異常資訊傳送到客戶端,但這樣就因噎廢食了。

3. 異常處理的目標

雖然Abp的異常處理有缺陷, 但只是異常資訊應用上的缺陷,
Abp異常處理①對異常的劃分、②異常資訊的本地化、③出現異常時寫日誌 支援的還是相當好。

我們考慮基於Abp的異常處理做一些擴充套件,(若擴充套件不了做一些替換 ..

  1. 對所有Controller-Action方法捕獲異常, [修復Abp Bug]
  2. 在Abp的異常處理結果中新增 TraceId
  3. 希望將服務端異常劃分為幾大類,簡化前端顯示;同時也不妨礙開發者檢視詳細異常資訊。

4. 揪出Abp異常處理缺陷的根源

Abp異常處理的核心物件AbpExceptionFilter,實現IAsyncExceptionFilter過濾器, ITransientDependency瞬時注入介面。

這是一個ServiceFilterAttribute, 你可以認為有如下

[ServiceFilter(typeof(AbpExceptionFilter))]

特性,作用在每一個Controller的Action方法上。

一旦某個Controller的Action方法發生異常, 會先後執行如下程式碼:

public async Task OnExceptionAsync(ExceptionContext context)
{
     if (!ShouldHandleException(context))
     {
          return;
     }
     await HandleAndWrapException(context);
}
  1. ShouldHandleException(context) :監測是否應該處理異常,據查該函式確實存在我上文說的問題:並不能捕獲所有的Action方法的異常。
  2. HandleAndWrapException(context): 異常處理步驟:
    • 根據Abp內建的異常型別,自動確定狀態碼 (這個在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;
② 這個格式的日誌過於詳細,messagedetails欄位均不能用於前端顯示。

  • 寫日誌,預設異常級別為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點提出的幾個目標也是企業級異常處理 要解決的痛點。

此異常處理的思路也可推及到其他非Abp專案.

改造方案在Abp官方github issue上: https://github.com/abpframework/abp/issues/6761
本人經驗之談,如果問題或看法,請不吝賜教!

  1. https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs
  2. https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-5.0

相關文章