[Abp 源码分析]异常处理
點(diǎn)擊上方藍(lán)字關(guān)注我們
Abp 框架本身針對(duì)內(nèi)部拋出異常進(jìn)行了統(tǒng)一攔截,并且針對(duì)不同的異常也會(huì)采取不同的處理策略。在 Abp 當(dāng)中主要提供了以下幾種異常類型:
| AbpException | Abp 框架定義的基本異常類型,Abp 所有內(nèi)部定義的異常類型都繼承自本類。 |
| AbpInitializationException | Abp 框架初始化時(shí)出現(xiàn)錯(cuò)誤所拋出的異常。 |
| AbpDbConcurrencyException | 當(dāng) EF Core 執(zhí)行數(shù)據(jù)庫操作時(shí)產(chǎn)生了?DbUpdateConcurrencyException?異常 的時(shí)候 Abp 會(huì)封裝為本異常并且拋出。 |
| AbpValidationException | 用戶調(diào)用接口時(shí),輸入的DTO 參數(shù)有誤會(huì)拋出本異常。 |
| BackgroundJobException | 后臺(tái)作業(yè)執(zhí)行過程中產(chǎn)生的異常。 |
| EntityNotFoundException | 當(dāng)倉儲(chǔ)執(zhí)行 Get 操作時(shí),實(shí)體未找到引發(fā)本異常。 |
| UserFriendlyException | 如果用戶需要將異常信息發(fā)送給前端,請(qǐng)拋出本異常。 |
| AbpRemoteCallException | 遠(yuǎn)程調(diào)用一場(chǎng),當(dāng)使用 Abp 提供的?AbpWebApiClient?產(chǎn)生問題的時(shí)候 會(huì)拋出此異常。 |
1.啟動(dòng)流程
Abp 框架針對(duì)異常攔截的處理主要使用了 ASP .NET CORE MVC 過濾器機(jī)制,當(dāng)外部請(qǐng)求接口的時(shí)候,所有異常都會(huì)被 Abp 框架捕獲。Abp 異常過濾器的實(shí)現(xiàn)名稱叫做?AbpExceptionFilter,它在注入 Abp 框架的時(shí)候就已經(jīng)被注冊(cè)到了 ASP .NET Core 的 MVC Filters 當(dāng)中了。
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 參數(shù)ConfigureAspNetCore(services, abpBootstrapper.IocManager);return WindsorRegistrationHelper.CreateServiceProvider(abpBootstrapper.IocManager.IocContainer, services); }ConfigureAspNetCore()?方法內(nèi)部:
private static void ConfigureAspNetCore(IServiceCollection services, IIocResolver iocResolver) {// ...省略掉的其他代碼// 配置 MVCservices.Configure<MvcOptions>(mvcOptions =>{mvcOptions.AddAbp(services);});// ...省略掉的其他代碼 }AbpMvcOptionsExtensions?擴(kuò)展類針對(duì)?MvcOptions?提供的擴(kuò)展方法?AddAbp()?:
public static void AddAbp(this MvcOptions options, IServiceCollection services) {AddConventions(options, services);// 添加 VC 過濾器AddFilters(options);AddModelBinders(options); }AddFilters()?方法內(nèi)部:
private static void AddFilters(MvcOptions options) {// 權(quán)限認(rèn)證過濾器options.Filters.AddService(typeof(AbpAuthorizationFilter));// 審計(jì)信息過濾器options.Filters.AddService(typeof(AbpAuditActionFilter));// 參數(shù)驗(yàn)證過濾器options.Filters.AddService(typeof(AbpValidationActionFilter));// 工作單元過濾器options.Filters.AddService(typeof(AbpUowActionFilter));// 異常過濾器options.Filters.AddService(typeof(AbpExceptionFilter));// 接口結(jié)果過濾器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){}// 構(gòu)造函數(shù)1,接受一個(gè)異常描述信息public AbpException(string message): base(message){}// 構(gòu)造函數(shù)2,接受一個(gè)異常描述信息與內(nèi)部異常public AbpException(string message, Exception innerException): base(message, innerException){} }類型的定義是十分簡(jiǎn)單的,基本上就是繼承了原有的?Exception?類型,改了一個(gè)名字罷了。
2.2 異常攔截
Abp 本身針對(duì)異常信息的核心處理就在于它的?AbpExceptionFilter?過濾器,過濾器實(shí)現(xiàn)很簡(jiǎn)單。它首先繼承了?IExceptionFilter?接口,實(shí)現(xiàn)了其?OnException()?方法,只要用戶請(qǐng)求接口的時(shí)候出現(xiàn)了任何異常都會(huì)調(diào)用?OnException()?方法。而在?OnException()?方法內(nèi)部,Abp 根據(jù)不同的異常類型進(jìn)行了不同的異常處理。
public class AbpExceptionFilter : IExceptionFilter, ITransientDependency {// 日志記錄器public ILogger Logger { get; set; }// 事件總線public IEventBus EventBus { get; set; }// 錯(cuò)誤信息構(gòu)建器private readonly IErrorInfoBuilder _errorInfoBuilder;// AspNetCore 相關(guān)的配置信息private readonly IAbpAspNetCoreConfiguration _configuration;// 注入并初始化內(nèi)部成員對(duì)象public AbpExceptionFilter(IErrorInfoBuilder errorInfoBuilder, IAbpAspNetCoreConfiguration configuration){_errorInfoBuilder = errorInfoBuilder;_configuration = configuration;Logger = NullLogger.Instance;EventBus = NullEventBus.Instance;}// 異常觸發(fā)時(shí)會(huì)調(diào)用此方法public void OnException(ExceptionContext context){// 判斷是否由控制器觸發(fā),如果不是則不做任何處理if (!context.ActionDescriptor.IsControllerAction()){return;}// 獲得方法的包裝特性。決定后續(xù)操作,如果沒有指定包裝特性,則使用默認(rèn)特性var wrapResultAttribute =ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(context.ActionDescriptor.GetMethodInfo(),_configuration.DefaultWrapResultAttribute);// 如果方法上面的包裝特性要求記錄日志,則記錄日志if (wrapResultAttribute.LogError){LogHelper.LogException(Logger, context.Exception);}// 如果被調(diào)用的方法上的包裝特性要求重新包裝錯(cuò)誤信息,則調(diào)用 HandleAndWrapException() 方法進(jìn)行包裝if (wrapResultAttribute.WrapOnError){HandleAndWrapException(context);}}// 處理并包裝異常private void HandleAndWrapException(ExceptionContext context){// 判斷被調(diào)用接口的返回值是否符合標(biāo)準(zhǔn),不符合則直接返回if (!ActionResultHelper.IsObjectResult(context.ActionDescriptor.GetMethodInfo().ReturnType)){return;}// 設(shè)置 HTTP 上下文響應(yīng)所返回的錯(cuò)誤代碼,由具體異常決定。context.HttpContext.Response.StatusCode = GetStatusCode(context);// 重新封裝響應(yīng)返回的具體內(nèi)容。采用 AjaxResponse 進(jìn)行封裝context.Result = new ObjectResult(new AjaxResponse(_errorInfoBuilder.BuildForException(context.Exception),context.Exception is AbpAuthorizationException));// 觸發(fā)異常處理事件EventBus.Trigger(this, new AbpHandledExceptionData(context.Exception));// 處理完成,將異常上下文的內(nèi)容置為空context.Exception = null; //Handled!}// 根據(jù)不同的異常類型返回不同的 HTTP 錯(cuò)誤碼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 針對(duì)異常處理的具體操作了,在這里面涉及到的?WrapResultAttribute?、?AjaxResponse?、?IErrorInfoBuilder?都會(huì)在后面說明,但是具體的邏輯已經(jīng)在過濾器所體現(xiàn)了。
2.3 接口返回值包裝
Abp 針對(duì)所有 API 返回的數(shù)據(jù)都會(huì)進(jìn)行一次包裝,使得其返回值內(nèi)容類似于下面的內(nèi)容。
{"result": {"totalCount": 0,"items": []},"targetUrl": null,"success": true,"error": null,"unAuthorizedRequest": false,"__abp": true }其中的?result?節(jié)點(diǎn)才是你接口真正返回的內(nèi)容,其余的?targetUrl?之類的都是屬于 Abp 包裝器給你進(jìn)行封裝的。
2.3.1 包裝器特性
其中,Abp 預(yù)置的包裝器有兩種,第一個(gè)是?WrapResultAttribute?。它有兩個(gè)?bool?類型的參數(shù),默認(rèn)均為?true?,一個(gè)叫?WrapOnSuccess?一個(gè) 叫做?WrapOnError?,分別用于確定成功或則失敗后是否包裝具體信息。像之前的?OnException()?方法里面就有用該值進(jìn)行判斷是否包裝異常信息。
除了?WarpResultAttribute?特性,還有一個(gè)?DontWrapResultAttribute?的特性,該特性直接繼承自?WarpResultAttribute?,只不過它的?WrapOnSuccess?與?WrapOnError?都為?fasle?狀態(tài),也就是說無論接口調(diào)用結(jié)果是成功還是失敗,都不會(huì)進(jìn)行結(jié)果包裝。該特性可以直接打在接口方法、控制器、接口之上,類似于這樣:
public class TestApplicationService : ApplicationService {[DontWrapResult]public async Task<string> Get(){return await Task.FromResult("Hello World");} }那么這個(gè)接口的返回值就不會(huì)帶有其他附加信息,而直接會(huì)按照 Json 來序列化返回你的對(duì)象。
在攔截異常的時(shí)候,如果你沒有給接口方法打上?DontWarpResult?特性,那么他就會(huì)直接使用?IAbpAspNetCoreConfiguration?的?DefaultWrapResultAttribute?屬性指定的默認(rèn)特性,該默認(rèn)特性如果沒有顯式指定則為?WrapResultAttribute?。
public AbpAspNetCoreConfiguration() {DefaultWrapResultAttribute = new WrapResultAttribute();// ...IAbpAspNetCoreConfiguration 的默認(rèn)實(shí)現(xiàn)的構(gòu)造函數(shù)// ...省略掉了其他代碼 }2.3.2 具體包裝行為
Abp 針對(duì)正常的接口數(shù)據(jù)返回與異常數(shù)據(jù)返回都是采用的?AjaxResponse?來進(jìn)行封裝的,轉(zhuǎn)到其基類的定義可以看到在里面定義的那幾個(gè)屬性就是我們接口返回出來的數(shù)據(jù)。
public abstract class AjaxResponseBase {// 目標(biāo) Url 地址public string TargetUrl { get; set; }// 接口調(diào)用是否成功public bool Success { get; set; }// 當(dāng)接口調(diào)用失敗時(shí),錯(cuò)誤信息存放在此處public ErrorInfo Error { get; set; }// 是否是未授權(quán)的請(qǐng)求public bool UnAuthorizedRequest { get; set; }// 用于標(biāo)識(shí)接口是否基于 Abp 框架開發(fā)public bool __abp { get; } = true; }So,從剛才的?2.2 節(jié)?可以看到他是直接?new?了一個(gè)?AjaxResponse?對(duì)象,然后使用?IErrorInfoBuilder?來構(gòu)建了一個(gè)?ErrorInfo?錯(cuò)誤信息對(duì)象傳入到?AjaxResponse?對(duì)象當(dāng)中并且返回。
那么問題來了,這里的?IErrorInfoBuilder?是怎樣來進(jìn)行包裝的呢?
2.3.3 異常包裝器
當(dāng) Abp 捕獲到異常之后,會(huì)通過?IErrorInfoBuilder?的?BuildForException()?方法來將異常轉(zhuǎn)換為?ErrorInfo?對(duì)象。它的默認(rèn)實(shí)現(xiàn)只有一個(gè),就是?ErrorInfoBuilder?,內(nèi)部結(jié)構(gòu)也很簡(jiǎn)單,其?BuildForException()?方法直接通過內(nèi)部的一個(gè)轉(zhuǎn)換器進(jìn)行轉(zhuǎn)換,也就是?IExceptionToErrorInfoConverter,直接調(diào)用的?IExceptionToErrorInfoConverter.Convert()?方法。
同時(shí)它擁有另外一個(gè)方法,叫做?AddExceptionConverter(),可以傳入你自己實(shí)現(xiàn)的異常轉(zhuǎn)換器。
public class ErrorInfoBuilder : IErrorInfoBuilder, ISingletonDependency {private IExceptionToErrorInfoConverter Converter { get; set; }public ErrorInfoBuilder(IAbpWebCommonModuleConfiguration configuration, ILocalizationManager localizationManager){// 異常包裝器默認(rèn)使用的 DefaultErrorInfoConverter 來進(jìn)行轉(zhuǎn)換Converter = new DefaultErrorInfoConverter(configuration, localizationManager);}// 根據(jù)異常來構(gòu)建異常信息public ErrorInfo BuildForException(Exception exception){return Converter.Convert(exception);}// 添加用戶自定義的異常轉(zhuǎn)換器public void AddExceptionConverter(IExceptionToErrorInfoConverter converter){converter.Next = Converter;Converter = converter;} }2.3.4 異常轉(zhuǎn)換器
Abp 要包裝異常,具體的操作是由轉(zhuǎn)換器來決定的,Abp 實(shí)現(xiàn)了一個(gè)默認(rèn)的轉(zhuǎn)換器,叫做?DefaultErrorInfoConverter,在其內(nèi)部,注入了?IAbpWebCommonModuleConfiguration?配置項(xiàng),而用戶可以通過配置該選項(xiàng)的?SendAllExceptionsToClients?屬性來決定是否將異常輸出給客戶端。
我們先來看一下他的?Convert()?核心方法:
public ErrorInfo Convert(Exception exception) {// 封裝 ErrorInfo 對(duì)象var errorInfo = CreateErrorInfoWithoutCode(exception);// 如果具體的異常實(shí)現(xiàn)有 IHasErrorCode 接口,則將錯(cuò)誤碼也封裝到 ErrorInfo 對(duì)象內(nèi)部if (exception is IHasErrorCode){errorInfo.Code = (exception as IHasErrorCode).Code;}return errorInfo; }核心十分簡(jiǎn)單,而?CreateErrorInfoWithoutCode()?方法內(nèi)部呢也是一些具體的邏輯,根據(jù)異常類型的不同,執(zhí)行不同的轉(zhuǎn)換邏輯。
private ErrorInfo CreateErrorInfoWithoutCode(Exception exception) {// 如果要發(fā)送所有異常,則使用 CreateDetailedErrorInfoFromException() 方法進(jìn)行封裝if (SendAllExceptionsToClients){return CreateDetailedErrorInfoFromException(exception);}// 如果有多個(gè)異常,并且其內(nèi)部異常為 UserFriendlyException 或者 AbpValidationException 則將內(nèi)部異常拿出來放在最外層進(jìn)行包裝if (exception is AggregateException && exception.InnerException != null){var aggException = exception as AggregateException;if (aggException.InnerException is UserFriendlyException ||aggException.InnerException is AbpValidationException){exception = aggException.InnerException;}}// 如果一場(chǎng)類型為 UserFriendlyException 則直接通過 ErrorInfo 構(gòu)造函數(shù)進(jìn)行構(gòu)建if (exception is UserFriendlyException){var userFriendlyException = exception as UserFriendlyException;return new ErrorInfo(userFriendlyException.Message, userFriendlyException.Details);}// 如果為參數(shù)類一場(chǎng),則使用不同的構(gòu)造函數(shù)進(jìn)行構(gòu)建,并且在這里可以看到他通過 L 函數(shù)調(diào)用的多語言提示if (exception is AbpValidationException){return new ErrorInfo(L("ValidationError")){ValidationErrors = GetValidationErrorInfos(exception as AbpValidationException),Details = GetValidationErrorNarrative(exception as AbpValidationException)};}// 如果是實(shí)體未找到的異常,則包含具體的實(shí)體類型信息與實(shí)體 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);}// 如果是未授權(quán)的一場(chǎng),一樣的執(zhí)行不同的操作if (exception is Abp.Authorization.AbpAuthorizationException){var authorizationException = exception as Abp.Authorization.AbpAuthorizationException;return new ErrorInfo(authorizationException.Message);}// 除了以上這幾個(gè)固定的異常需要處理之外,其他的所有異常統(tǒng)一返回內(nèi)部服務(wù)器錯(cuò)誤信息。return new ErrorInfo(L("InternalServerError")); }所以整體異常處理還是比較復(fù)雜的,進(jìn)行了多層封裝,但是結(jié)構(gòu)還是十分清晰的。
3.擴(kuò)展
3.1 顯示額外的異常信息
如果你需要在調(diào)用接口而產(chǎn)生異常的時(shí)候展示異常的詳細(xì)信息,可以通過在啟動(dòng)模塊的?PreInitialize()?(預(yù)加載方法) 當(dāng)中加入?Configuration.Modules.AbpWebCommon().SendAllExceptionsToClients = true;?即可,例如:
[DependsOn(typeof(AbpAspNetCoreModule))] public class TestWebStartupModule : AbpModule {public override void PreInitialize(){Configuration.Modules.AbpWebCommon().SendAllExceptionsToClients = true;} }3.2 監(jiān)聽異常事件
使用 Abp 框架的時(shí)候,你可以隨時(shí)通過監(jiān)聽?AbpHandledExceptionData?事件來使用自己的邏輯處理產(chǎn)生的異常。比如說產(chǎn)生異常時(shí)向監(jiān)控服務(wù)報(bào)警,或者說將異常信息持久化到其他數(shù)據(jù)庫等等。
你只需要編寫如下代碼即可實(shí)現(xiàn)監(jiān)聽異常事件:
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($"當(dāng)前異常信息為:{eventData.Exception.Message}");} }如果你覺得看的有點(diǎn)吃力的話,可以跳轉(zhuǎn)到?這里?了解 Abp 的事件總線實(shí)現(xiàn)。
作者:myzony
出處:https://www.cnblogs.com/myzony/p/9460021.html
公眾號(hào)“碼俠江湖”所發(fā)表內(nèi)容注明來源的,版權(quán)歸原出處所有(無法查證版權(quán)的或者未注明出處的均來自網(wǎng)絡(luò),系轉(zhuǎn)載,轉(zhuǎn)載的目的在于傳遞更多信息,版權(quán)屬于原作者。如有侵權(quán),請(qǐng)聯(lián)系,筆者會(huì)第一時(shí)間刪除處理!
掃描二維碼
獲取更多精彩
碼俠江湖
總結(jié)
以上是生活随笔為你收集整理的[Abp 源码分析]异常处理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [Abp 源码分析]权限验证
- 下一篇: 微创社001期:从0开始创作第一本技术书