天天看點

[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 流程圖

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

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

4.點此跳轉到總目錄