一、簡介
ABP vNext 在 v 2.9.x 版本當中新增了 BLOB 系統,主要用於儲存大型二進位制檔案。ABP 抽象了一套通用的 BLOB 體系,開發人員在儲存或讀取二進位制檔案時,可以忽略具體實現,直接使用 IBlobContainer
或 IBlobContainer<T>
進行操作。官方的 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,本文的閱讀前提是建立在你已經閱讀過該指南,並有一定的使用經驗。
二、原始碼分析
2.1 模組分析
看一個 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
的實現是一個工廠方法,這一點在後面會進行解釋。
2.2 BLOB 容器
2.2.1 容器的定義
每個容器就是一個 OSS 的 Bucket,開發人員在對 BLOB 進行操作時,會注入 IBlobContainer
/IBlobContainer<T>
,通過介面提供的 5 種方法進行操作,這五個方法分別是 儲存物件、刪除物件、判斷物件是否存在、獲取物件、獲取物件(不存在返回 NULL)。
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
{
}
2.2.2 容器的實現
容器的兩種實現都存放在 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
物件。
2.2.3.1 名稱標準化物件
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);
}
}
2.2.3.2 BLOB 上下文
在 BLOB 裡面,ABP 分別為每個操作都定義了一個 ***Args
物件,它就是一個上下文物件,用於在整個呼叫週期中傳遞引數。
2.2.3.3 BLOB 配置資訊
每個 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
字典進行操作。
2.2.3 容器的構造與初始化
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";
}
2.2.3.1 BLOB 的配置提供者
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 容器呼叫委託,以便進行配置。
2.2.3.2 BLOB 的 Provider 選擇器
在構造 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。
2.3 Provider 的實現
2.3.1 File System
檔案系統作為 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 提供的重試機制來建立檔案。
2.3.2 DataBase
資料庫 Provider 是利用資料庫的 BLOB 型別,將這些大型物件儲存到資料庫當中,不太建議這樣操作。這裡不再進行詳細介紹,基本大同小異。
2.3.3 各類 OSS (騰訊云為例)
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
2.4 回顧
- 開發人員可以在模組的
ConfigureService()
階段為所有容器或者特定容器指定引數。 - ABP vNext 框架會注入所有的 BLOB Provider,並注入預設的
IBlobContainer<DefaultContainer>
容器和其他的型別化容器實現。 - 當需要使用 BLOB 時,開發人員注入了
IBlobContainer
或IBlobContainer<T>
。 - BLOB 容器的工廠會根據容器的名稱匹配對應的 BLOB Provider 和配置物件。
- BLOB Provider 根據 **Args 引數內部附帶的配置物件,讀取對應的配置資訊進行自定義的操作。
三、總結
小型專案直接整合 FileSystem 即可,中大型專案可以使用各種 OSS Provider,BLOB 系統可以簡化開發人員對於大量二進位制檔案的管理操作。最近工作相當雜亂繁忙,下半年希望有時間繼續學習更新吧。
其他相關文章,請參閱 文章目錄 。