0.簡介
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)
{
// ...省略掉的其他代碼
// 配置 MVC
services.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 的事件總線實現。