1. 程式人生 > >[Abp vNext 原始碼分析] - 23. 二進位制大物件系統(BLOB)

[Abp vNext 原始碼分析] - 23. 二進位制大物件系統(BLOB)

## 一、簡介 ABP vNext 在 v 2.9.x 版本當中添加了 BLOB 系統,主要用於儲存大型二進位制檔案。ABP 抽象了一套通用的 BLOB 體系,開發人員在儲存或讀取二進位制檔案時,可以忽略具體實現,直接使用 `IBlobContainer` 或 `IBlobContainer` 進行操作。官方的 BLOB Provider 實現有 **Azure**、**AWS**、**FileSystem(檔案系統儲存)**、**Database(資料庫儲存)**、**阿里雲 OSS**,你也可以自己繼承 `BlobProviderBase` 來實現其他的 Provider。 BLOB 常用於各類二進位制檔案儲存和管理,基本就是對雲服務的 OSS 進行了抽象,在使用當中也會有 Bucket 和 Object Key 的概念,在 BLOB 裡面對應的就是 ContainerName 和 BlobName。 關於 BLOB 的官方使用指南,可以參考 [https://docs.abp.io/en/abp/latest/Blob-Storing](https://docs.abp.io/en/abp/latest/Blob-Storing),本文的閱讀前提是建立在你已經閱讀過該指南,並有一定的使用經驗。 ## 二、原始碼分析 ### 2.1 模組分析 看一個 ABP 的庫專案,首先從他的 Module 入手,對應的 BLOB 核心庫的 `Module` 就是 `AbpBlobStoringModule` 類,在其內部,只進行了兩個操作,注入了 `IBlobContainer` 與 `IBlobContainer<>` 的實現。 ```csharp public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddTransient( typeof(IBlobContainer<>), typeof(BlobContainer<>) ); context.Services.AddTransient( typeof(IBlobContainer), serviceProvider => serviceProvider .GetRequiredService>() ); } ``` 從上述程式碼可以看出來,`IBlobContainer` 的預設實現還是基於 `BlobContainer` 的。那麼為啥會有個泛型的 Container,從簡介中可以看到 OSS 裡面對應的 Bucket 其實就是一個 `IBlobContainer`。假如你會針對某雲的多個 Bucket 進行操作,那麼就需要型別化的 BlobContainer 了。 在這裡可以看到,`IBlobContainer` 的實現是一個工廠方法,這一點在後面會進行解釋。 ### 2.2 BLOB 容器 #### 2.2.1 容器的定義 每個容器就是一個 OSS 的 Bucket,開發人員在對 BLOB 進行操作時,會注入 `IBlobContainer`/`IBlobContainer`,通過介面提供的 5 種方法進行操作,這五個方法分別是 **儲存物件**、**刪除物件**、**判斷物件是否存在**、**獲取物件**、**獲取物件(不存在返回 NULL)**。 ```csharp public interface IBlobContainer { // 儲存物件 Task SaveAsync( string name, Stream stream, bool overrideExisting = false, CancellationToken cancellationToken = default ); // 刪除物件 Task DeleteAsync( string name, CancellationToken cancellationToken = default ); // 判斷物件是否存在 Task ExistsAsync( string name, CancellationToken cancellationToken = default ); // 獲取物件 Task GetAsync( string name, CancellationToken cancellationToken = default ); // 獲取物件(不存在返回 NULL) Task GetOrNullAsync( string name, CancellationToken cancellationToken = default ); //TODO: Create shortcut extension methods: GetAsArraryAsync, GetAsStringAsync(encoding) (and null versions) } ``` 泛型的 BLOB 容器也是整合自該介面,內部沒有任何特殊的方法。 ```csharp public interface IBlobContainer : IBlobContainer where TContainer: class { } ``` #### 2.2.2 容器的實現 容器的兩種實現都存放在 `BlobContainer.cs` 檔案當中,標註容器實現內部都會有一個 `ContainerName`,用於標識不同的容器,並且和其他的元件作為 **關聯鍵** 進行繫結。每個容器都會關聯 `BlobContainerConfiguration`、`IBlobProvider` 兩個元件,它們分別提供了容器的配置資訊和容器的具體實現 Provider,在容器構造的時候根據 `ContainerName` 分別進行初始化。 ```csharp public class BlobContainer : IBlobContainer { protected string ContainerName { get; } protected BlobContainerConfiguration Configuration { get; } protected IBlobProvider Provider { get; } protected ICurrentTenant CurrentTenant { get; } protected ICancellationTokenProvider CancellationTokenProvider { get; } protected IServiceProvider ServiceProvider { get; } // ... 其他程式碼。 } ``` 可以看到這裡還注入了 `ICurrentTenant`,注入該物件的主要作用是用來處理多租戶的情況,如果當前容器啟用了多租戶,那麼會手動 `Change()`。下面以 `SaveAsync()` 方法為例。 ```csharp public virtual async Task SaveAsync( string name, Stream stream, bool overrideExisting = false, CancellationToken cancellationToken = default) { // 變更當前租戶資訊,當啟用了多租戶時,會使用當前租戶進行變更。 using (CurrentTenant.Change(GetTenantIdOrNull())) { // 根據 ContainerName 取得對應的標準化容器名稱和物件名稱。 var (normalizedContainerName, normalizedBlobName) = NormalizeNaming(ContainerName, name); // 使用 ContainerName 匹配的 Provider 儲存物件資料。 await Provider.SaveAsync( new BlobProviderSaveArgs( normalizedContainerName, Configuration, normalizedBlobName, stream, overrideExisting, CancellationTokenProvider.FallbackToProvider(cancellationToken) ) ); } } ``` 這裡有兩個地方需要單獨分析,第一個是 `NormalizeNaming()` 的作用,第二個是 `BlobProviderSaveArgs` 物件。 ##### 2.2.3.1 名稱標準化物件 `IBlobNamingNormalizer`(BLOB 名稱標準化物件),主要用於將一個字串進行標準化處理,防止 Provider 無法處理這種名稱。各大 OSS 都對容器的名稱或物件的名稱有命名要求,比如必須全部小寫,不能有哪些特殊符號等等。 ```csharp protected virtual (string, string) NormalizeNaming(string containerName, string blobName) { // 從當前的配置資訊中獲取對應的標準化器,如果不存在任何標準化工具物件,則直接返回原始名稱。 if (!Configuration.NamingNormalizers.Any()) { return (containerName, blobName); } using (var scope = ServiceProvider.CreateScope()) { // 獲取所有的標準化器,並依次進行名稱的標準化處理。 foreach (var normalizerType in Configuration.NamingNormalizers) { var normalizer = scope.ServiceProvider .GetRequiredService(normalizerType) .As(); containerName = normalizer.NormalizeContainerName(containerName); blobName = normalizer.NormalizeBlobName(blobName); } return (containerName, blobName); } } ``` ##### 2.2.3.2 BLOB 上下文 在 BLOB 裡面,ABP 分別為每個操作都定義了一個 `***Args` 物件,它就是一個上下文物件,用於在整個呼叫週期中傳遞引數。 ##### 2.2.3.3 BLOB 配置資訊 每個 BLOB 容器都會有一個 `BlobContainerConfiguration` 用於儲存配置資訊,它主要有以下幾個重要的屬性。 ```csharp public class BlobContainerConfiguration { // 當前 BLOB 容器對應的 Provider 型別。 public Type ProviderType { get; set; } // 當前 BLOB 容器是否啟用了多租戶。 public bool IsMultiTenant { get; set; } = true; // 當前 BLOB 容器的名稱標準化物件。 public ITypeList NamingNormalizers { get; } // 當前 BLOB 容器的屬性。 [NotNull] private readonly Dictionary _properties; // 當嘗試獲取某些配置屬性,但是不存在時,會從這個 Configuration 拿取資料。 [CanBeNull] private readonly BlobContainerConfiguration _fallbackConfiguration; public BlobContainerConfiguration(BlobContainerConfiguration fallbackConfiguration = null) { NamingNormalizers = new TypeList(); _fallbackConfiguration = fallbackConfiguration; _properties = new Dictionary(); } [CanBeNull] public T GetConfigurationOrDefault(string name, T defaultValue = default) { return (T) GetConfigurationOrNull(name, defaultValue); } [CanBeNull] public object GetConfigurationOrNull(string name, object defaultValue = null) { return _properties.GetOrDefault(name) ?? _fallbackConfiguration?.GetConfigurationOrNull(name, defaultValue) ?? defaultValue; } // ... 其他程式碼。 } ``` 在後續各種 Provider 裡面定義的配置項,本質上就是對 `_properties` 字典進行操作。 #### 2.2.3 容器的構造與初始化 BLOB 容器並不是通過 IoC 容器直接解析構造的,而是通過 `IBlobContainerFactory` 工廠進行建立,與容器相關的配置物件和 BLOB Provider 也是在這個時候進行構造賦值。 ```csharp public class BlobContainerFactory : IBlobContainerFactory, ITransientDependency { protected IBlobProviderSelector ProviderSelector { get; } protected IBlobContainerConfigurationProvider ConfigurationProvider { get; } protected ICurrentTenant CurrentTenant { get; } protected ICancellationTokenProvider CancellationTokenProvider { get; } protected IServiceProvider ServiceProvider { get; } public BlobContainerFactory( IBlobContainerConfigurationProvider configurationProvider, ICurrentTenant currentTenant, ICancellationTokenProvider cancellationTokenProvider, IBlobProviderSelector providerSelector, IServiceProvider serviceProvider) { ConfigurationProvider = configurationProvider; CurrentTenant = currentTenant; CancellationTokenProvider = cancellationTokenProvider; ProviderSelector = providerSelector; ServiceProvider = serviceProvider; } public virtual IBlobContainer Create(string name) { // 根據容器的名稱,獲取對應的配置。 var configuration = ConfigurationProvider.Get(name); // 構造一個新的容器物件。 return new BlobContainer( name, configuration, // 一樣的是根據容器名稱,獲得匹配的 Provider 型別。 ProviderSelector.Get(name), CurrentTenant, CancellationTokenProvider, ServiceProvider ); } } ``` 那麼這個工廠方法是在什麼時候呼叫的呢?跳轉到工廠方法的實現,發現會被一個靜態擴充套件方法所呼叫,重要的是這個方法是一個泛型方法,這樣就與開頭的型別化 BLOB 容器相對應了。 ```csharp public static class BlobContainerFactoryExtensions { public static IBlobContainer Create( this IBlobContainerFactory blobContainerFactory ) { // 通過 GetContainerName 方法獲取容器的名字。 return blobContainerFactory.Create( BlobContainerNameAttribute.GetContainerName() ); } } ``` `GetContainerName()` 方法也很簡單,如果容器型別沒有指定 `BlobContainerNameAttribute` 特性,那麼就會預設使用型別的 `FullName` 作為名稱。 ```csharp public static string GetContainerName(Type type) { var nameAttribute = type.GetCustomAttribute(); if (nameAttribute == null) { return type.FullName; } return nameAttribute.GetName(type); } ``` 最後的最後,看一下這個型別化的 BLOB 容器。 ```csharp public class BlobContainer : IBlobContainer where TContainer : class { private readonly IBlobContainer _container; public BlobContainer(IBlobContainerFactory blobContainerFactory) { _container = blobContainerFactory.Create(); } // ... 其他程式碼。 } ``` 對應的是模組初始化的工廠方法: ```csharp context.Services.AddTransient( typeof(IBlobContainer), serviceProvider => serviceProvider .GetRequiredService>() ``` 這裡的 `DefaultContainer` 就指定了該特性,所以本質上一個 `IBlobContainer` 就是一個型別化的容器,它的泛型引數是 `DefaultContainer`。 ```csharp [BlobContainerName(Name)] public class DefaultContainer { public const string Name = "default"; } ``` ##### 2.2.3.1 BLOB 的配置提供者 BLOB 容器工廠使用 `IBlobContainerConfigurationProvider` 來匹配對應容器的配置資訊,實現比較簡單,直接注入了 `AbpBlobStoringOptions` 並嘗試從它的 `BlobContainerConfigurations` 中獲取配置物件。 ```csharp public class DefaultBlobContainerConfigurationProvider : IBlobContainerConfigurationProvider, ITransientDependency { protected AbpBlobStoringOptions Options { get; } public DefaultBlobContainerConfigurationProvider(IOptions options) { Options = options.Value; } public virtual BlobContainerConfiguration Get(string name) { return Options.Containers.GetConfiguration(name); } } ``` 這裡的 `BlobContainerConfigurations` 物件,核心就是一個鍵值對,鍵就是 BLOB 容器的名稱,值就是容器對應的配置物件。 ```csharp public class BlobContainerConfigurations { private BlobContainerConfiguration Default => GetConfiguration(); private readonly Dictionary _containers; public BlobContainerConfigurations() { _containers = new Dictionary { // 新增預設的 BLOB 容器。 [BlobContainerNameAttribute.GetContainerName()] = new BlobContainerConfiguration() }; } // ... 其他程式碼 public BlobContainerConfigurations Configure( [NotNull] string name, [NotNull] Action configureAction) { Check.NotNullOrWhiteSpace(name, nameof(name)); Check.NotNull(configureAction, nameof(configureAction)); configureAction( _containers.GetOrAdd( name, () => new BlobContainerConfiguration(Default) ) ); return this; } public BlobContainerConfigurations ConfigureAll(Action configureAction) { foreach (var container in _containers) { configureAction(container.Key, container.Value); } return this; } // ... 其他程式碼 } ``` 在使用過程中,我們在模組裡面呼叫的 `Configure()` 方法,就會在字典新增一個新的 Item,併為其賦值。而 `ConfigureAll()` 就是遍歷這個字典,為每個 BLOB 容器呼叫委託,以便進行配置。 ##### 2.2.3.2 BLOB 的 Provider 選擇器 在構造 BLOB 容器的時候,BLOB 容器工廠通過 `IBlobProviderSelector` 來選擇對應的 BLOB Provider,具體選擇哪一個是根據 `BlobContainerConfiguration` 裡面的 `ProviderType` 決定的。 ```csharp public virtual IBlobProvider Get([NotNull] string containerName) { Check.NotNull(containerName, nameof(containerName)); // 獲得當前 BLOB 容器對應的配置資訊。 var configuration = ConfigurationProvider.Get(containerName); if (!BlobProviders.Any()) { throw new AbpException("No BLOB Storage provider was registered! At least one provider must be registered to be able to use the Blog Storing System."); } foreach (var provider in BlobProviders) { // 通過配置資訊匹配對應的 Provider。 if (ProxyHelper.GetUnProxiedType(provider).IsAssignableTo(configuration.ProviderType)) { return provider; } } throw new AbpException( $"Could not find the BLOB Storage provider with the type ({configuration.ProviderType.AssemblyQualifiedName}) configured for the container {containerName} and no default provider was set." ); } ``` 上面的 `BlobProviders` 其實就是直接從 IoC 解析的 `IEnumerable` 物件,我還找了半天是哪個地方進行賦值的。當 ABP 框架自動之後,會自動將已經實現的 BLOB Provider 注入到 IoC 容器中,如果某個容器在使用時指定了對應的配置引數,則會匹配對應的 BLOB Provider。 ### 2.3 Provider 的實現 #### 2.3.1 File System 檔案系統作為 BLOB 的最簡化實現,本質就是通過資料夾進行租戶隔離動作,所有操作都會將資料持久化到硬碟上。核心程式碼就一個檔案 `FileSystemBlobProvider`,在這個檔案內部定義了具體的執行邏輯,我們這裡大概看一下 `SaveAsyn()` 的實現。 ```csharp public override async Task SaveAsync(BlobProviderSaveArgs args) { var filePath = FilePathCalculator.Calculate(args); if (!args.OverrideExisting && await ExistsAsync(filePath)) { throw new BlobAlreadyExistsException($"Saving BLOB '{args.BlobName}' does already exists in the container '{args.ContainerName}'! Set {nameof(args.OverrideExisting)} if it should be overwritten."); } DirectoryHelper.CreateIfNotExists(Path.GetDirectoryName(filePath)); var fileMode = args.OverrideExisting ? FileMode.Create : FileMode.CreateNew; await Policy.Handle() .WaitAndRetryAsync(2, retryCount => TimeSpan.FromSeconds(retryCount)) .ExecuteAsync(async () => { using (var fileStream = File.Open(filePath, fileMode, FileAccess.Write)) { await args.BlobStream.CopyToAsync( fileStream, args.CancellationToken ); await fileStream.FlushAsync(); } }); } ``` 很簡單,通過 `FilePathCalculator `計算出來檔案的具體路徑,然後結合配置引數來判斷檔案是否存在,以及是否進入後續操作。通過 **Polly** 提供的重試機制來建立檔案。 #### 2.3.2 DataBase 資料庫 Provider 是利用資料庫的 BLOB 型別,將這些大型物件儲存到資料庫當中,不太建議這樣操作。這裡不再進行詳細介紹,基本大同小異。 #### 2.3.3 各類 OSS (騰訊云為例) OSS 作為雲廠商的標配,基本概念和操作都與 ABP 的 BLOB 相匹配,整合起來也還是比較簡單,就是將各個 OSS 的 SDK 塞進來就行。這裡注意點的是,每個 BLOB Provider 都會編寫一個基於 `BlobContainerConfiguration` 型別的靜態方法,取名都叫做 `UseXXX()`,並在裡面對具體的配置進行賦值。 ```csharp public static class TencentCloudBlobContainerConfigurationExtensions { public static TencentCloudBlobProviderConfiguration GetTencentCloudConfiguration( this BlobContainerConfiguration containerConfiguration) { return new TencentCloudBlobProviderConfiguration(containerConfiguration); } public static BlobContainerConfiguration UseTencentCloud( this BlobContainerConfiguration containerConfiguration, Action tencentCloudConfigureAction) { containerConfiguration.ProviderType = typeof(TencentCloudBlobProvider); containerConfiguration.NamingNormalizers.TryAdd(); tencentCloudConfigureAction(new TencentCloudBlobProviderConfiguration(containerConfiguration)); return containerConfiguration; } } ``` 可能會對這個 `TencentCloudBlobProviderConfiguration` 有一些好奇,其實就是個套娃,因為直接傳入了 `BlobContainerConfiguration` 物件,裡面的各種屬性本質上就是對配置項的那個 `Dictionary` 進行操作。 ```csharp public class TencentCloudBlobProviderConfiguration { public string AppId { get => _containerConfiguration.GetConfigurationOrDefault(TencentCloudBlobProviderConfigurationNames.AppId); set => _containerConfiguration.SetConfiguration(TencentCloudBlobProviderConfigurationNames.AppId, value); } public string SecretId { get => _containerConfiguration.GetConfigurationOrDefault(TencentCloudBlobProviderConfigurationNames.SecretId); set => _containerConfiguration.SetConfiguration(TencentCloudBlobProviderConfigurationNames.SecretId, value); } // ... 其他程式碼 public TencentCloudBlobProviderConfiguration(BlobContainerConfiguration containerConfiguration) { _containerConfiguration = containerConfiguration; } } ``` 騰訊雲的 BLOB Provider 倉庫:[https://github.com/EasyAbp/Abp.BlobStoring.TencentCloud](https://github.com/EasyAbp/Abp.BlobStoring.TencentCloud) ### 2.4 回顧 1. 開發人員可以在模組的 `ConfigureService()` 階段為所有容器或者特定容器指定引數。 2. ABP vNext 框架會注入所有的 BLOB Provider,並注入預設的 `IBlobContainer` 容器和其他的型別化容器實現。 3. 當需要使用 BLOB 時,開發人員注入了 `IBlobContainer` 或 `IBlobContainer`。 4. BLOB 容器的工廠會根據容器的名稱匹配對應的 BLOB Provider 和配置物件。 5. BLOB Provider 根據 **Args 引數內部附帶的配置物件,讀取對應的配置資訊進行自定義的操作。 ## 三、總結 小型專案直接整合 FileSystem 即可,中大型專案可以使用各種 OSS Provider,BLOB 系統可以簡化開發人員對於大量二進位制檔案的管理操作。最近工作相當雜亂繁忙,下半年希望有時間繼續學習更新吧。 其他相關文章,請參閱 **[文章目錄](https://www.cnblogs.com/myzony/p/10722506.html)** 。