ABP

2023-06-29 18:00:33

1. 事件匯流排

在我們的一個應用中,經常會出現一個邏輯執行之後要跟隨執行另一個邏輯的情況,例如一個使用者建立了後續還需要傳送郵件進行通知,或者需要初始化相應的許可權等。面對這樣的情況,我們當然可以順序進行相應的邏輯程式碼的編寫,但這樣會導致各種業務邏輯全部集中耦合在一個類中,違背了 "單一職責原則"。

在 ABP 框架中,對於上面的業務場景的處理,我們可以通過事件匯流排來解耦,使得程式碼邏輯實現更加清晰。事件匯流排的本質就是中介者模式,利用一箇中介角色在傳送方和接受方之間進行訊息的傳遞,接受方單獨實現關注的獨立的小功能點,從而達到各塊業務邏輯清晰,程式碼鬆散耦合的目的。

ABP 框架中的事件匯流排分為 本地事件匯流排 和 分散式事件匯流排 兩種,兩種使用的方式基本類似,只是分散式事件匯流排需要藉助 RabbitMQ、Kafaka 等第三方訊息佇列中介軟體。本章先講本地事件匯流排相關知識點。

2. 本地事件匯流排

本地事件匯流排實現程序內的事件的釋出訂閱功能,通常運用於單體應用架構或微服務架構中的一個服務內部。使用方式比較簡單,以下是演示,也將通過控制檯程式來進行。

2.1. 事件匯流排基本使用

本地事件匯流排的實現包含在 Volo.Abp.EventBus Nuget 包中,我們可以通過以下方式來整合。

通過以下命令建立一個控制檯專案:

abp new AbpEventBus -t console

在 AbpEventBusSample.csproj 執行以下命令:

Abp add-package Volo.Abp.EventBus

如果是 Web 應用的話,在通過 ABP CLI 初始化啟動模板的時候就已經整合了事件匯流排模組,無須再自己進行整合。

2.1.1 釋出

ABP 提供了 ILocalEventBus 介面來滿足我們對本地事件匯流排的使用。我們只需要在要進行事件釋出的類注入該介面即可,之後就能通過以下的方式進行事件的釋出了。

public class HelloWorldService : ITransientDependency
{
	private readonly ILocalEventBus _localEventBus;
	public HelloWorldService(ILocalEventBus localEventBus)
	{
		_localEventBus = localEventBus;
	}

	public Task SayHelloAsync()
	{
		// 當前業務邏輯
		Console.WriteLine("Hello Jerry!");
		// 關聯業務邏輯
		_localEventBus.PublishAsync(new HelloEventData
		{
			Who = "Tom",
			ToWho = "Jerry",
			Where = "廣州天河正佳廣場",
			When = DateTime.Now
		});

		return Task.CompletedTask;
	}
}

在進行事件釋出之前需要先定義一個事件物件,這是一個普通類,是事件相關的各種資料的一個包裝類,例如上面使用到的 HelloEventData 。

public class HelloEventData
{
	public string Who { get; set; }

  public string ToWho { get; set; }

	public string Where { get; set; }

	public DateTime When { get; set; }
}

就算在事件釋出過程中不需要傳輸任何資料也需要建立一個類,在這種情況下為空類,這是因為事件匯流排是通過這個事件物件的型別來確定其對於的訂閱者,從而執行相應的處理方法的。

通過原始碼可以看到,當我們呼叫 PublishAsync 方法時,最終時呼叫了 TriggerHandlersAsync 方法,該方法中從 HandlerFactories 中找到相應的的 HandlerFactory,然後通過 IEventHandlerInvoker 執行相應的事件執行器。

最終,就是反射建立了對於的 IEventHandlerMethodExecutor 物件,傳入執行器和事件物件,再通過委託呼叫執行器的 HandleEventAsync 方法。

那麼 HandlerFactories 是怎麼來的呢?它實際上就是一個事件物件和執行器對應的集合。這裡的工廠實際上並不是執行器工廠,它不負責執行器的建立,而是通過執行器生成執行器的包裝類(為了型別物件能夠釋放銷燬)。

在我們通過 PublishAsync 方法釋出事件的時候,還可以通過 onUnitOfWorkComplete 引數設定事件釋出是否和工作單元掛鉤,實現事件和其他業務的原子性。其實這也很簡單,就是結合工作單元的時候,只是將事件儲存起來,沒有立刻觸發。

等到工作單元提交了,再通過 IUnitOfWorkEventPublisher 物件釋出,而該介面在事件匯流排模組中有對於的實現類UnitOfWorkEventPublisher,其實就是工作單元提交時再次釋出一次不結合工作單元的事件而已。

2.1.2 訂閱

事件的訂閱有多種方式。

(1) 實現 ILocalEventHandler<TEvent> 介面,並設定到容器

public class HelloEventHandler : ILocalEventHandler<HelloEventData>, ITransientDependency
{
	public Task HandleEventAsync(HelloEventData eventData)
	{
		Console.WriteLine($"{eventData.Who} Say Hello To { eventData.ToWho } in { eventData.When } at { eventData.When }");
		return Task.CompletedTask;
	}
}

這種方式是最簡便的方式,ABP 框架中的事件匯流排模組會在服務設定到容器時自動方向這些訂閱者執行器。

從原始碼中可以看到,事件匯流排模組中註冊了容器中依賴關係設定的攔截器(auto Ioc 容器的功能),在應用啟動向容器中設定依賴關係的時候,這裡的事件會觸發,對每一個設定進行檢查,通過介面型別查詢到相應的實現類之後,會被儲存到 選項 當中。

在 事件匯流排 建構函式會根據選項中儲存的執行器註冊訂閱

實際上這裡就是通過執行器範例建立了一個工廠類,並且將其新增到上面講到的事件物件和執行器對應的集合 HandlerFactories 中,維護好事件物件型別與執行器的對於關係。

(2) 手動呼叫 ILocalEventBus 介面進行訂閱

public Task SayHelloAsync()
{
	// 當前業務邏輯
	Console.WriteLine("Hello Jerry!");
	
	_localEventBus.Subscribe<HelloEventData, HelloLogEventHandler>();
	//_localEventBus.Subscribe(new HelloLogEventHandler());
	//_localEventBus.Subscribe<HelloEventData>((eventData) => {  return Task.CompletedTask; });

	// 關聯業務邏輯
	_localEventBus.PublishAsync(new HelloEventData
	{
		Who = "Tom",
		ToWho = "Jerry",
		Where = "廣州天河正佳廣場",
		When = DateTime.Now
	});

	return Task.CompletedTask;
}

public class HelloLogEventHandler : ILocalEventHandler<HelloEventData>
{
	public Task HandleEventAsync(HelloEventData eventData)
	{
		Console.WriteLine($"Log: {eventData.Who} Say Hello To { eventData.ToWho } in { eventData.When } at { eventData.When }");
		return Task.CompletedTask;
	}
}

手動註冊的訂閱者會在呼叫註冊程式碼之後全域性生效,應該儲存只有一次的訂閱註冊。

手動註冊訂閱者的時候,其實和上面自動註冊的方式沒太大區別,事件匯流排會根據我們註冊訂閱者的方式進行一定的包裝,最終也是新增到事件物件和執行器對照的集合。

這些訂閱者會在我們呼叫 ILocalEventBus 的 PublishAsync 方法對相關的事件進行釋出之後觸發。事件的釋出訂閱有以下特點:

  • 事件可以由0個或多個處理程式訂閱.

  • 一個事件處理程式可以訂閱多個事件,但是需要為每個事件實現 ILocalEventHandler 介面.

  • 如果需要在訂閱者執行器中執行資料庫操作並且使用到倉儲,那可能需要使用工作單元。因為一些儲存庫方法需要在活動的工作單元中工作。應確保處理方法設定為 virtual,併為該方法新增一個 [UnitOfWork] 特性,或者手動使用 IUnitOfWorkManager 建立一個工作單元範圍。

  • 當一個事件釋出,訂閱的事件處理程式將立即執行,而同時 PublishAsync 如果通過 await 關鍵字轉同步的話,它將阻塞,直到事件處理程式執行完成。換句話說,本地事件匯流排事件釋出與處理實際是立即觸發,順序執行的。

    這意味著如果處理程式丟擲一個異常,它會影響釋出該事件的程式碼,我們可以在 PublishAsync 呼叫上捕捉異常。 如果想要隱藏錯誤,可以在事件處理程式中使用 try-catch。 如果在一個工作單元範圍內進行事件釋出,那麼相應的事件處理程式也會被工作單元覆蓋. 這意味著,如果你的 UOW 是事務和處理程式丟擲一個異常,事務會回滾。

這從上面列出來的原始碼中也可以看出來,本地事件匯流排本質上就還是通過在維護好事件物件型別與執行器對照集合中通過事件物件查詢執行器,然後呼叫執行器中的方法的過程。

2.2 預定義事件

實體的增、刪、改是非常常見的操作,有些時候一些實體的增、刪、改之後需要關聯一些其他的業務邏輯,這時候我們可以通過事件匯流排來解決。ABP框架會為所有的實體自動釋出這些事件,我們只需要訂閱相關的事件。

sing System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.EventBus;

namespace AbpDemo
{
	public class MyHandler
		: ILocalEventHandler<EntityCreatedEventData<IdentityUser>>,
		  ITransientDependency
	{
		public async Task HandleEventAsync(
			EntityCreatedEventData<IdentityUser> eventData)
		{
			var userName = eventData.Entity.UserName;
			var email = eventData.Entity.Email;
			//...
		}
	}
}

上面的例子是 ABP 官方的範例,訂閱了 EntityCreatedEventData<Entity> 介面的事件處理程式會在相應的實體建立之後觸發。這種和領域物件(實體、聚合根)操作相關的預定義事件有兩類:

用過去時態事件

當相關工作單元完成且實體更改成功儲存到資料庫時,將釋出帶有過去時態的事件. 如果在這些事件處理程式上丟擲異常,則無法回滾事務,因為事務已經提交.
事件型別;

  • EntityCreatedEventData<T> 當實體建立成功後釋出.
  • EntityUpdatedEventData<T> 當實體更新成功後釋出.
  • EntityDeletedEventData<T> 當實體刪除成功後釋出.
  • EntityChangedEventData<T> 當實體建立,更新,刪除後釋出. 如果你需要監聽任何型別的更改,它是一種快捷方式 - 而不是訂閱單個事件.

用於進行時態事件(6.0版本可用,7.0版本已移除)

帶有進行時態的事件在完成事務之前釋出(如果資料庫事務由所使用的資料庫提供程式支援). 如果在這些事件處理程式上丟擲異常,它會回滾事務,因為事務還沒有完成,更改也沒有儲存到資料庫中.
事件型別;

  • EntityCreatingEventData<T> 當新實體儲存到資料庫前釋出.
  • EntityUpdatingEventData<T> 當已存在實體更新到資料庫前釋出.
  • EntityDeletingEventData<T> 刪除實體前釋出.
  • EntityChangingEventData<T> 當實體建立,更新,刪除前釋出. 如果你需要監聽任何型別的更改,它是一種快捷方式 - 而不是訂閱單個事件.

它們是在將更改儲存到資料庫時釋出預構建事件;

  • 對於 EF Core, 他們在 DbContext.SaveChanges 釋出.
  • 對於 MongoDB, 在你呼叫倉儲的 InsertAsync, UpdateAsync 或 DeleteAsync 方法釋出(因為MongoDB沒有更改追蹤系統).

領域物件中是不能夠通過依賴注入注入服務,在聚合根類中我們可以通過 AddLocalEvent 新增本地事件,實體類中則不行,這裡新增的事件將在聚合根物件持久化操作的時候釋出。

using System;
using Volo.Abp.Domain.Entities;

namespace AbpDemo
{
	public class Product : AggregateRoot<Guid>
	{
		public string Name { get; set; }
		
		public int StockCount { get; private set; }

		private Product() { }

		public Product(Guid id, string name)
			: base(id)
		{
			Name = name;
		}

		public void ChangeStockCount(int newCount)
		{
			StockCount = newCount;
			
			//ADD an EVENT TO BE PUBLISHED
			AddLocalEvent(
				new StockCountChangedEvent
				{
					ProductId = Id,
					NewCount = newCount
				}
			);
		}
	}
}

聚合根中可以通過 AddLocalEvent 方法新增事件,是因為 ABP 框架中的聚合根基礎類別實現了 IGeneratesDomainEvents 介面,如果我們的實體類中也需要釋出事件,也可以實現 IGeneratesDomainEvents 介面。但是 ABP 並不建議隨意地普通的實體類實現該介面,因為基於 IGeneratesDomainEvents 的事件釋出是基於特定的資料庫提供程式的,目前 ABP 框架中僅支援 EF Core 、MongoDB 的實現。

通過原始碼可以看到,這些事件的釋出是重寫了 SaveChangeAsync 等資料持久化的方法,在其中根據 IGeneratesDomainEvents 介面新增了相應的領域物件的更改操作事件。

最終還是和上面的釋出事件時的工作單元操作一樣,先新增到工作單元中,在工作單元提交的時候由 IUnitOfWorkEventPublisher 物件釋出。



參考檔案:
ABP 官方檔案 - 本地事件匯流排



ABP 系列總結:
目錄:ABP 系列總結
上一篇:ABP - 快取模組(2)