1. 程式人生 > >[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 針對異常處理的具體操作了,在這裡面涉及到的 WrapResultAttributeAjaxResponseIErrorInfoBuilder 都會在後面說明,但是具體的邏輯已經在過濾器所體現了。

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 ,只不過它的 WrapOnSuccessWrapOnError 都為 fasle 狀態,也就是說無論介面呼叫結果是成功還是失敗,都不會進行結果包裝。該特性可以直接打在介面方法、控制器、介面之上,類似於這樣:

public class TestApplicationService : ApplicationService
{
    [DontWrapResult]
    public async Task<string> Get()
    {
        return await Task.FromResult("Hello World");
    }
}

那麼這個介面的返回值就不會帶有其他附加資訊,而直接會按照 Json 來序列化返回你的物件。

在攔截異常的時候,如果你沒有給介面方法打上 DontWarpResult 特性,那麼他就會直接使用 IAbpAspNetCoreConfigurationDefaultWrapResultAttribute 屬性指定的預設特性,該預設特性如果沒有顯式指定則為 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 捕獲到異常之後,會通過 IErrorInfoBuilderBuildForException() 方法來將異常轉換為 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 的事件匯流排實現。