[Abp 源码分析]异常处理
點擊上方藍字關注我們
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) {// ...省略掉的其他代碼// 配置 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
公眾號“碼俠江湖”所發表內容注明來源的,版權歸原出處所有(無法查證版權的或者未注明出處的均來自網絡,系轉載,轉載的目的在于傳遞更多信息,版權屬于原作者。如有侵權,請聯系,筆者會第一時間刪除處理!
掃描二維碼
獲取更多精彩
碼俠江湖
總結
以上是生活随笔為你收集整理的[Abp 源码分析]异常处理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [Abp 源码分析]权限验证
- 下一篇: 微创社001期:从0开始创作第一本技术书