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

之前吐槽Abp的用戶/租戶管理模塊!今天我又來了,這次我給Abp官方repo提了一個issue。
目前Website使用Abp vNext開發(fā),免不了要全局處理異常、提示服務器異常信息。
1. Abp官方異常處理
Abp項目默認會啟動內(nèi)置的異常處理,默認不將異常信息發(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
}
}
經(jīng)過幾天倒騰,發(fā)現(xiàn)Abp vNext的異常處理有幾個問題。
2.Abp異常處理存在的缺陷
-
并沒有如官方所述:自動處理所有異常,實際需要滿足官方所說的某個條件:
這就導致當Controller Action方法返回的不是object result時,則根本捕獲不到異常(我們暫時不說middleware產(chǎn)生的異常),這應該算Abp的一個Bug
-
輸出的異常沒有TraceId, 不利于日志排查 -
發(fā)送到客戶端的日志字段 message,detail過于詳細冗長,不適合前端顯示
也可以配置SendExceptionsDetailsToClients,不將異常信息發(fā)送到客戶端,但這樣就因噎廢食了。
3. 異常處理的目標
雖然Abp的異常處理有缺陷, 但只是異常信息應用上的缺陷,
Abp異常處理①對異常的劃分、②異常信息的本地化、③出現(xiàn)異常時寫日志 支持的還是相當好。
基于Abp的異常處理現(xiàn)狀,考慮做一些改進:
-
對所有Controller-Action方法捕獲異常, [修復Abp Bug] -
在Abp的異常處理結(jié)果中添加 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)測是否應該處理異常,據(jù)查該函數(shù)確實存在我上文說的問題:并不能捕獲所有的Action方法的異常。 -
HandleAndWrapException(context):異常處理步驟: -
根據(jù)Abp內(nèi)置的異常類型,自動確定狀態(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,內(nèi)容拷貝自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
再見,VIP,臥槽又來一個看片神器!
對抗流氓app開屏廣告,你需要這款神器!
