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

来源:https://www.cnblogs.com/myzony/archive/2018/08/11/9460021.html
-Advertisement-
Play Games

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 針對異常處理的具體操作了,在這裡面涉及到的 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 的事件匯流排實現。

4.點此跳轉到總目錄


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 原文:https://www.cnblogs.com/ppes/p/9461246.html 一、官網的定義: https://docs.scipy.org/doc/numpy/user/quickstart.html In NumPy dimensions are called axes. For ...
  • 前言: 現在很多人即將畢業或者換工作面臨找工作,為了幫助大家,遂寫下這篇文章。如果你想進入BAT,抑或拿到高工資,無論你的基礎如何,你至少要花三個月時間來準備簡歷、筆試題、面試題。對於沒有項目經驗,沒有電腦專業背景,甚至沒有學歷背景的朋友,更需要花時間來準備了,建議半年以上。 脫穎而出的簡歷,一份 ...
  • 1 Trying this option worked for me. 2 3 library(httr) 4 with_config(use_proxy(...), install_github(...)) 5 6 OR 7 8 library(httr) 9 set_config(use_pro... ...
  • 一、註釋: 用 # 標註的文本 二、數字: 1. 整數,不區分long和int (1)進位:0x 0o 0b (2)Bool: True False 2. 浮點數:如3.14,-0.45,1.23e6 3. 複數:1+2j 三、字元串: 1. 單、雙引號引起來的字元序列 2. 單/雙三引號,可以跨行 ...
  • 5771. 【NOIP2008模擬】遨游 (File IO): input:trip.in output:trip.out Time Limits: 2000 ms Memory Limits: 262144 KB Detailed Limits Goto ProblemSet 5771. 【NOI ...
  • 概念:野指針指向了一塊隨機記憶體空間,不受程式控制。如指針指向已經被刪除的對象或者指向一塊沒有訪問許可權的記憶體空間,之後如果對其再解引用的話,就會出現問題。 野指針產生的原因: 1、指針定義時未被初始化:指針在被定義的時候,如果程式不對其進行初始化的話,它會指向隨機區域,因為任何指針變數(除了stati ...
  • 這幾天因為一個需求,要不斷重覆一個用特定代碼段去包圍不同代碼的需求。 這個要不斷移動滑鼠以及重覆敲打相同代碼的體力活,實在讓我老眼昏花,體內的懶人之力迫使我想一個快捷的方法來代替之。 之前就知道Snippet能夠自定義代碼段,藉此機會正好研究了下,接下來我會簡單介紹一個自定義Snippet的例子。 ...
  • 一、簡介 最近因為工作需要,使用了一些單機版Redis的界面化管理工具,使用過程中那慘痛的體驗真的只有用過的人才能體會;為此本人和小伙伴準備動手一個Redis可視化工具,但是因為小伙伴最近工作比較忙,搞了一大半沒有時間繼續(會有後續,界面不敢說,使用體驗上面肯定要比現有的好);本人對wpf不是很熟, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...