ABP vNext 在 v 2.9.x 版本當中添加了 BLOB 系統,主要用於存儲大型二進制文件。ABP 抽象了一套通用的 BLOB 體系,開發人員在存儲或讀取二進制文件時,能夠忽略具體實現,直接使用 IBlobContainer
或 IBlobContainer<T>
進行操做。官方的 BLOB Provider 實現有 Azure、AWS、FileSystem(文件系統存儲)、Database(數據庫存儲)、阿里雲 OSS,你也能夠本身繼承 BlobProviderBase
來實現其餘的 Provider。html
BLOB 經常使用於各種二進制文件存儲和管理,基本就是對雲服務的 OSS 進行了抽象,在使用當中也會有 Bucket 和 Object Key 的概念,在 BLOB 裏面對應的就是 ContainerName 和 BlobName。git
關於 BLOB 的官方使用指南,能夠參考 https://docs.abp.io/en/abp/latest/Blob-Storing,本文的閱讀前提是創建在你已經閱讀過該指南,並有必定的使用經驗。github
看一個 ABP 的庫項目,首先從他的 Module 入手,對應的 BLOB 核心庫的 Module
就是 AbpBlobStoringModule
類,在其內部,只進行了兩個操做,注入了 IBlobContainer
與 IBlobContainer<>
的實現。數據庫
public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddTransient( typeof(IBlobContainer<>), typeof(BlobContainer<>) ); context.Services.AddTransient( typeof(IBlobContainer), serviceProvider => serviceProvider .GetRequiredService<IBlobContainer<DefaultContainer>>() ); }
從上述代碼能夠看出來,IBlobContainer
的默認實現仍是基於 BlobContainer<T>
的。那麼爲啥會有個泛型的 Container,從簡介中能夠看到 OSS 裏面對應的 Bucket 其實就是一個 IBlobContainer
。假如你會針對某雲的多個 Bucket 進行操做,那麼就須要類型化的 BlobContainer 了。框架
在這裏能夠看到,IBlobContainer
的實現是一個工廠方法,這一點在後面會進行解釋。async
每一個容器就是一個 OSS 的 Bucket,開發人員在對 BLOB 進行操做時,會注入 IBlobContainer
/IBlobContainer<T>
,經過接口提供的 5 種方法進行操做,這五個方法分別是 保存對象、刪除對象、判斷對象是否存在、獲取對象、獲取對象(不存在返回 NULL)。ide
public interface IBlobContainer { // 保存對象 Task SaveAsync( string name, Stream stream, bool overrideExisting = false, CancellationToken cancellationToken = default ); // 刪除對象 Task<bool> DeleteAsync( string name, CancellationToken cancellationToken = default ); // 判斷對象是否存在 Task<bool> ExistsAsync( string name, CancellationToken cancellationToken = default ); // 獲取對象 Task<Stream> GetAsync( string name, CancellationToken cancellationToken = default ); // 獲取對象(不存在返回 NULL) Task<Stream> GetOrNullAsync( string name, CancellationToken cancellationToken = default ); //TODO: Create shortcut extension methods: GetAsArraryAsync, GetAsStringAsync(encoding) (and null versions) }
泛型的 BLOB 容器也是集成自該接口,內部沒有任何特殊的方法。工具
public interface IBlobContainer<TContainer> : IBlobContainer where TContainer: class { }
容器的兩種實現都存放在 BlobContainer.cs
文件當中,標註容器實現內部都會有一個 ContainerName
,用於標識不一樣的容器,而且和其餘的組件做爲 關聯鍵 進行綁定。每一個容器都會關聯 BlobContainerConfiguration
、IBlobProvider
兩個組件,它們分別提供了容器的配置信息和容器的具體實現 Provider,在容器構造的時候根據 ContainerName
分別進行初始化。源碼分析
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()
方法爲例。學習
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
對象。
IBlobNamingNormalizer
(BLOB 名稱標準化對象),主要用於將一個字符串進行標準化處理,防止 Provider 沒法處理這種名稱。各大 OSS 都對容器的名稱或對象的名稱有命名要求,好比必須所有小寫,不能有哪些特殊符號等等。
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<IBlobNamingNormalizer>(); containerName = normalizer.NormalizeContainerName(containerName); blobName = normalizer.NormalizeBlobName(blobName); } return (containerName, blobName); } }
在 BLOB 裏面,ABP 分別爲每一個操做都定義了一個 ***Args
對象,它就是一個上下文對象,用於在整個調用週期中傳遞參數。
每一個 BLOB 容器都會有一個 BlobContainerConfiguration
用於存儲配置信息,它主要有如下幾個重要的屬性。
public class BlobContainerConfiguration { // 當前 BLOB 容器對應的 Provider 類型。 public Type ProviderType { get; set; } // 當前 BLOB 容器是否啓用了多租戶。 public bool IsMultiTenant { get; set; } = true; // 當前 BLOB 容器的名稱標準化對象。 public ITypeList<IBlobNamingNormalizer> NamingNormalizers { get; } // 當前 BLOB 容器的屬性。 [NotNull] private readonly Dictionary<string, object> _properties; // 當嘗試獲取某些配置屬性,可是不存在時,會從這個 Configuration 拿取數據。 [CanBeNull] private readonly BlobContainerConfiguration _fallbackConfiguration; public BlobContainerConfiguration(BlobContainerConfiguration fallbackConfiguration = null) { NamingNormalizers = new TypeList<IBlobNamingNormalizer>(); _fallbackConfiguration = fallbackConfiguration; _properties = new Dictionary<string, object>(); } [CanBeNull] public T GetConfigurationOrDefault<T>(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
字典進行操做。
BLOB 容器並非經過 IoC 容器直接解析構造的,而是經過 IBlobContainerFactory
工廠進行建立,與容器相關的配置對象和 BLOB Provider 也是在這個時候進行構造賦值。
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 容器相對應了。
public static class BlobContainerFactoryExtensions { public static IBlobContainer Create<TContainer>( this IBlobContainerFactory blobContainerFactory ) { // 經過 GetContainerName 方法獲取容器的名字。 return blobContainerFactory.Create( BlobContainerNameAttribute.GetContainerName<TContainer>() ); } }
GetContainerName()
方法也很簡單,若是容器類型沒有指定 BlobContainerNameAttribute
特性,那麼就會默認使用類型的 FullName
做爲名稱。
public static string GetContainerName(Type type) { var nameAttribute = type.GetCustomAttribute<BlobContainerNameAttribute>(); if (nameAttribute == null) { return type.FullName; } return nameAttribute.GetName(type); }
最後的最後,看一下這個類型化的 BLOB 容器。
public class BlobContainer<TContainer> : IBlobContainer<TContainer> where TContainer : class { private readonly IBlobContainer _container; public BlobContainer(IBlobContainerFactory blobContainerFactory) { _container = blobContainerFactory.Create<TContainer>(); } // ... 其餘代碼。 }
對應的是模塊初始化的工廠方法:
context.Services.AddTransient( typeof(IBlobContainer), serviceProvider => serviceProvider .GetRequiredService<IBlobContainer<DefaultContainer>>()
這裏的 DefaultContainer
就指定了該特性,因此本質上一個 IBlobContainer
就是一個類型化的容器,它的泛型參數是 DefaultContainer
。
[BlobContainerName(Name)] public class DefaultContainer { public const string Name = "default"; }
BLOB 容器工廠使用 IBlobContainerConfigurationProvider
來匹配對應容器的配置信息,實現比較簡單,直接注入了 AbpBlobStoringOptions
並嘗試從它的 BlobContainerConfigurations
中獲取配置對象。
public class DefaultBlobContainerConfigurationProvider : IBlobContainerConfigurationProvider, ITransientDependency { protected AbpBlobStoringOptions Options { get; } public DefaultBlobContainerConfigurationProvider(IOptions<AbpBlobStoringOptions> options) { Options = options.Value; } public virtual BlobContainerConfiguration Get(string name) { return Options.Containers.GetConfiguration(name); } }
這裏的 BlobContainerConfigurations
對象,核心就是一個鍵值對,鍵就是 BLOB 容器的名稱,值就是容器對應的配置對象。
public class BlobContainerConfigurations { private BlobContainerConfiguration Default => GetConfiguration<DefaultContainer>(); private readonly Dictionary<string, BlobContainerConfiguration> _containers; public BlobContainerConfigurations() { _containers = new Dictionary<string, BlobContainerConfiguration> { // 添加默認的 BLOB 容器。 [BlobContainerNameAttribute.GetContainerName<DefaultContainer>()] = new BlobContainerConfiguration() }; } // ... 其餘代碼 public BlobContainerConfigurations Configure( [NotNull] string name, [NotNull] Action<BlobContainerConfiguration> configureAction) { Check.NotNullOrWhiteSpace(name, nameof(name)); Check.NotNull(configureAction, nameof(configureAction)); configureAction( _containers.GetOrAdd( name, () => new BlobContainerConfiguration(Default) ) ); return this; } public BlobContainerConfigurations ConfigureAll(Action<string, BlobContainerConfiguration> configureAction) { foreach (var container in _containers) { configureAction(container.Key, container.Value); } return this; } // ... 其餘代碼 }
在使用過程當中,咱們在模塊裏面調用的 Configure()
方法,就會在字典添加一個新的 Item,併爲其賦值。而 ConfigureAll()
就是遍歷這個字典,爲每一個 BLOB 容器調用委託,以便進行配置。
在構造 BLOB 容器的時候,BLOB 容器工廠經過 IBlobProviderSelector
來選擇對應的 BLOB Provider,具體選擇哪個是根據 BlobContainerConfiguration
裏面的 ProviderType
決定的。
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<IBlobProvider>
對象,我還找了半天是哪一個地方進行賦值的。當 ABP 框架自動以後,會自動將已經實現的 BLOB Provider 注入到 IoC 容器中,若是某個容器在使用時指定了對應的配置參數,則會匹配對應的 BLOB Provider。
文件系統做爲 BLOB 的最簡化實現,本質就是經過文件夾進行租戶隔離動做,全部操做都會將數據持久化到硬盤上。核心代碼就一個文件 FileSystemBlobProvider
,在這個文件內部定義了具體的執行邏輯,咱們這裏大概看一下 SaveAsyn()
的實現。
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<IOException>() .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 提供的重試機制來建立文件。
數據庫 Provider 是利用數據庫的 BLOB 類型,將這些大型對象存儲到數據庫當中,不太建議這樣操做。這裏再也不進行詳細介紹,基本大同小異。
OSS 做爲雲廠商的標配,基本概念和操做都與 ABP 的 BLOB 相匹配,集成起來也仍是比較簡單,就是將各個 OSS 的 SDK 塞進來就行。這裏注意點的是,每一個 BLOB Provider 都會編寫一個基於 BlobContainerConfiguration
類型的靜態方法,取名都叫作 UseXXX()
,並在裏面對具體的配置進行賦值。
public static class TencentCloudBlobContainerConfigurationExtensions { public static TencentCloudBlobProviderConfiguration GetTencentCloudConfiguration( this BlobContainerConfiguration containerConfiguration) { return new TencentCloudBlobProviderConfiguration(containerConfiguration); } public static BlobContainerConfiguration UseTencentCloud( this BlobContainerConfiguration containerConfiguration, Action<TencentCloudBlobProviderConfiguration> tencentCloudConfigureAction) { containerConfiguration.ProviderType = typeof(TencentCloudBlobProvider); containerConfiguration.NamingNormalizers.TryAdd<TencentCloudBlobNamingNormalizer>(); tencentCloudConfigureAction(new TencentCloudBlobProviderConfiguration(containerConfiguration)); return containerConfiguration; } }
可能會對這個 TencentCloudBlobProviderConfiguration
有一些好奇,其實就是個套娃,由於直接傳入了 BlobContainerConfiguration
對象,裏面的各類屬性本質上就是對配置項的那個 Dictionary<string,object>
進行操做。
public class TencentCloudBlobProviderConfiguration { public string AppId { get => _containerConfiguration.GetConfigurationOrDefault<string>(TencentCloudBlobProviderConfigurationNames.AppId); set => _containerConfiguration.SetConfiguration(TencentCloudBlobProviderConfigurationNames.AppId, value); } public string SecretId { get => _containerConfiguration.GetConfigurationOrDefault<string>(TencentCloudBlobProviderConfigurationNames.SecretId); set => _containerConfiguration.SetConfiguration(TencentCloudBlobProviderConfigurationNames.SecretId, value); } // ... 其餘代碼 public TencentCloudBlobProviderConfiguration(BlobContainerConfiguration containerConfiguration) { _containerConfiguration = containerConfiguration; } }
騰訊雲的 BLOB Provider 倉庫:https://github.com/EasyAbp/Abp.BlobStoring.TencentCloud
ConfigureService()
階段爲全部容器或者特定容器指定參數。IBlobContainer<DefaultContainer>
容器和其餘的類型化容器實現。IBlobContainer
或 IBlobContainer<T>
。小型項目直接集成 FileSystem 便可,中大型項目可使用各類 OSS Provider,BLOB 系統能夠簡化開發人員對於大量二進制文件的管理操做。最近工做至關雜亂繁忙,下半年但願有時間繼續學習更新吧。
其餘相關文章,請參閱 文章目錄 。