[Abp 源碼分析]10、異常處理

0.簡介

Abp 框架自己針對內部拋出異常進行了統一攔截,而且針對不一樣的異常也會採起不一樣的處理策略。在 Abp 當中主要提供瞭如下幾種異常類型:html

異常類型 描述
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() 方法內部:json

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() 方法內部:mvc

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 ,咱們能夠看一下該類型的基本定義。app

// 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 根據不一樣的異常類型進行了不一樣的異常處理。async

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 都會在後面說明,可是具體的邏輯已經在過濾器所體現了。ide

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 的事件總線實現。

4.點此跳轉到總目錄

相關文章
相關標籤/搜索