0.簡介 Abp 框架本身針對內部拋出異常進行了統一攔截,並且針對不同的異常也會採取不同的處理策略。在 Abp 當中主要提供了以下幾種異常類型: | 異常類型 | 描述 | | : : | : : | | | Abp 框架定義的基本異常類型,Abp 所有內部定義的異常類型都繼承自本類。 | | | ...
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 的事件匯流排實現。