ABP框架中的事件總線功能介紹

事件總線

關於事件總線

ABP 中,為瞭方便進程間通訊,給開發者提供瞭一個叫 事件總線 的功能,事件總線分為 本地事件總線分佈式事件總線,本篇文章講的是 本地事件總線,系列教程中暫時不考慮講解 分佈式事件總線

事件總線 需要使用 Volo.Abp.EventBus 庫,ABP 包中自帶,不需要額外引入。

事件總線是通過 訂閱-發佈 形式使用的,某一方隻需要按照格式推送事件,而不需要關註是誰接收瞭事件和如何處理事件。

你可以參考官方文檔:https://docs.abp.io/zh-Hans/abp/latest/Local-Event-Bus

為什麼需要這個東西

首先列舉一下,你工作開發的項目中,編寫 控制器時,是不是有這幾種代碼。

// 記錄日志 1
Task.Run(()=>
{
	_apiLog.Info($"xxxxxxxx");
});
// 記錄日志 2
catch(Exception ex)
{
	_apiLog.Error(ex);
}
// 記錄日志 3
_apiLog.Info($"登陸信息:用戶 [{userName}({clientAdrress})]\);

筆者認為,改善的上述問的方法之一是將函數的功能跟記錄日志分開,函數執行任務時,隻需要把狀態和信息通過事件總線推送,而不需要瞭關註應該如何處理這些內容。

另外,還有當函數執行某些步驟時,產生瞭事件,開發者喜歡 new Thread 一個新的線程去執行別的任務,或者 Task.Run

其實,通過事件總線,我們更加好地隔離代碼,遵從 單一職責原則 。當然還有很多方面值得使用事件總線,這裡我們就不再扯淡瞭。

前面,我們編寫瞭全局異常攔截器,還有日志組件,這一篇我們將通過事件總線,將 Web 程序的一些部件組合起來。

事件總線創建過程

訂閱事件

創建一個服務來訂閱事件,當程序中發生某種事件時,此服務將被調用。

事件服務必須繼承 ILocalEventHandler<in TEvent> 接口,並實現以下函數:

Task HandleEventAsync(TEvent eventData);

一個系統中,事件服務可以有多個,每個服務的 TEvent 類型不能相同,因為 TEvent 的類型是調用服務的標識。當發生 TEvent 事件後,系統通過 TEvent 去找到這個服務。

事件服務創建完畢後,需要加入到依賴註入中,你可以多繼承一個 ITransientDependency 接口,然後統一掃描程序集加入到 依賴註入容器中(第三篇提到過)。

事件

即上面提到的 TEvent

假設有一個系統中所有的事件服務都放到一個容器中,發佈者隻能傳遞一個事件,而不能指定誰來提供響應服務。

容器是通過 TEvent 來查找服務的。

事件就是一個模型類,也可以使用 int或者 string 等簡單類型(請不要用簡單類型做事件),用於傳遞信息。

一般使用 Event 做後綴。

發佈事件

如果需要發佈一個事件,隻需要註入 ILocalEventBus 即可。

        private readonly ILocalEventBus _localEventBus;

        public MyService(ILocalEventBus localEventBus)
        {
            _localEventBus = localEventBus;
        }

然後發佈事件:

            await _localEventBus.PublishAsync(
                new TEvent
                {
					... ...
                }
            );

全局異常加入事件總線功能

創建事件

在 AbpBase.Web 中,創建一個 Handlers 目錄,再在 Handlers 目錄下,創建 HandlerEvents 目錄。

然後在 HandlerEvents 目錄,創建一個 CustomerExceptionEvent.cs 文件。

CustomerExceptionEvent 作為一個異常事件,用於傳遞異常的信息,而不僅僅是將 Exception ex 記錄就瞭事。

其文件內容如下:

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;

namespace AbpBase.Application.Handlers.HandlerEvents
{
    /// <summary>
    /// 全局異常推送事件
    /// </summary>
    public class CustomerExceptionEvent
    {
        /// <summary>
        /// 隻記錄異常
        /// </summary>
        /// <param name="ex"></param>
        public CustomerExceptionEvent(Exception ex)
        {
            Exception = ex;
        }

        /// <summary>
        /// 此異常發生時,用戶請求的路由地址
        /// </summary>
        /// <param name="ex"></param>
        /// <param name="actionRoute"></param>
        public CustomerExceptionEvent(Exception ex, string actionRoute)
        {
            Exception = ex;
            Action = actionRoute;
        }

        /// <summary>
        /// 此異常發生在哪個類型的方法中
        /// </summary>
        /// <param name="ex"></param>
        /// <param name="method"></param>
        public CustomerExceptionEvent(Exception ex, MethodBase method)
        {
            Exception = ex;
            MethodInfo = (MethodInfo)method;
        }

        /// <summary>
        /// 記錄異常信息
        /// </summary>
        /// <param name="ex"></param>
        /// <param name="actionRoute"></param>
        /// <param name="method"></param>
        public CustomerExceptionEvent(Exception ex, string actionRoute, MethodBase method)
        {
            Exception = ex;
            Action = actionRoute;
            MethodInfo = (MethodInfo)method;
        }

        /// <summary>
        /// 當前出現位置
        /// <example>
        /// <code>
        /// MethodInfo = (MethodInfo)MethodBase.GetCurrentMethod();
        /// </code>
        /// </example>
        /// </summary>
        public MethodInfo MethodInfo { get; private set; }

        /// <summary>
        /// 發生異常的 Action
        /// </summary>
        public string Action { get; private set; }

        /// <summary>
        /// 具體異常
        /// </summary>
        public Exception Exception { get; private set; }
    }
}

訂閱事件

訂閱事件,即將其定義為事件的響應者、服務提供者。

當異常發生後,異常的位置,推送異常信息,那麼誰來處理這些信息呢?是訂閱者。

這裡我們定義一個異常日志處理類,來處理程序推送的異常信息。

在 AbpBase.Web 項目的 Handlers 目錄中,添加一個 CustomerExceptionHandler 類,繼承:

public class CustomerExceptionHandler : ILocalEventHandler<CustomerExceptionEvent>, ITransientDependency

服務要處理事件,必須繼承 ILocalEventHandler<T>,而 ITransientDependency 是為瞭此服務可以可以自動註入到容器中。

其文件內容如下:

using AbpBase.Application.Handlers.HandlerEvents;
using Serilog;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;

namespace AbpBase.Application.Handlers
{
    /// <summary>
    /// 全局異常記錄日志
    /// </summary>
    public class CustomerExceptionHandler : ILocalEventHandler<CustomerExceptionEvent>, ITransientDependency
    {
        private readonly ILogger _ILogger;

        public CustomerExceptionHandler(ILogger logger)
        {
            _ILogger = logger;
        }

        public async Task HandleEventAsync(CustomerExceptionEvent eventData)
        {
            StringBuilder stringBuilder = new StringBuilder(256);
            stringBuilder.AppendLine();
            stringBuilder.Append("Action:    ");
            stringBuilder.AppendLine(eventData.Action);
            if (eventData.MethodInfo != null)
            {
                stringBuilder.Append("Class-Method:    ");
                stringBuilder.Append(eventData.MethodInfo?.DeclaringType.FullName);
                stringBuilder.AppendLine(eventData.MethodInfo?.Name);
            }

            stringBuilder.Append("Source:    ");
            stringBuilder.AppendLine(eventData.Exception.Source);
            stringBuilder.Append("TargetSite:    ");
            stringBuilder.AppendLine(eventData.Exception.TargetSite?.ToString());
            stringBuilder.Append("InnerException:    ");
            stringBuilder.AppendLine(eventData.Exception.InnerException?.ToString());
            stringBuilder.Append("Message:    ");
            stringBuilder.AppendLine(eventData.Exception.Message);
            stringBuilder.Append("HelpLink:    ");
            stringBuilder.AppendLine(eventData.Exception.HelpLink);
            _ILogger.Fatal(stringBuilder.ToString());
            await Task.CompletedTask;
        }
    }
}

這樣寫,記錄的日志可以有很好的層次結構。

發佈事件

定義瞭事件的格式和定義服務來訂閱事件後,我們來創建一個發佈者。

我們修改一下 WebGlobalExceptionFilter

增加依賴註入:

        private readonly ILocalEventBus _localEventBus;

        public WebGlobalExceptionFilter(ILocalEventBus localEventBus)
        {
            _localEventBus = localEventBus;
        }

發佈事件:

        public async Task OnExceptionAsync(ExceptionContext context)
        {

            if (!context.ExceptionHandled)
            {
                await _localEventBus.PublishAsync(new CustomerExceptionEvent(context.Exception,
                    context.ActionDescriptor?.DisplayName));
                    ...
                    ...

測試

創建一個 Action :

        [HttpGet("/T4")]
        public string MyWebApi4()
        {
            int a = 1;
            int b = 0;
            int c = a / b;
            return c.ToString();
        }

然後訪問 https://localhost:5001/T4 ,會發現請求後報錯

在 AbpBase.Web 的 Logs 目錄中,打開 -Fatal.txt 文件。

可以看到:

2020-09-16 18:49:27.750 +08:00 [FTL] 
Action:    ApbBase.HttpApi.Controllers.TestController.MyWebApi4 (ApbBase.HttpApi)
Source:    ApbBase.HttpApi
TargetSite:    System.String MyWebApi4()
InnerException:    
Message:    Attempted to divide by zero.
HelpLink:

除瞭異常信息外,我們還可以很方便的知道異常發生在 TestController.MyWebApi4 這個位置。

記錄事件

如果在普通方法裡面出現異常,我們這樣這樣記錄:

            catch (Exception ex)
            {
                ...
                new CustomerExceptionEvent(ex, MethodBase.GetCurrentMethod());
                ...
            }

MethodBase.GetCurrentMethod() 可以獲取當前正在運行的方法,獲得信息後將此參數傳遞給異常記錄服務,會自動解析出具體是哪個地方發生異常。

由於目前 Web 程序中還沒有編寫什麼服務,因此我們先結合到異常日志功能中,後面編寫服務時,會再次用到事件總線。

完整代碼參考:https://github.com/whuanle/AbpBaseStruct/tree/master/src/4/AbpBase

源碼地址:https://github.com/whuanle/AbpBaseStruct

到此這篇關於ABP框架中的事件總線功能的文章就介紹到這瞭。希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。

推薦閱讀: