[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
,本文的阅读前提是建立在你已经阅读过该指南,并有一定的使用经验。

二、源码分析

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
的默认实现还是基于 BlobContainer
的。那么为啥会有个泛型的 Container,从简介中可以看到 OSS 里面对应的 Bucket 其实就是一个 IBlobContainer
。假如你会针对某云的多个 Bucket 进行操作,那么就需要类型化的 BlobContainer 了。

在这里可以看到, IBlobContainer
的实现是一个工厂方法,这一点在后面会进行解释。

2.2 BLOB 容器

2.2.1 容器的定义

每个容器就是一个 OSS 的 Bucket,开发人员在对 BLOB 进行操作时,会注入 IBlobContainer
/ IBlobContainer
,通过接口提供的 5 种方法进行操作,这五个方法分别是 保存对象
删除对象
判断对象是否存在
获取对象
获取对象(不存在返回 NULL)

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 容器也是集成自该接口,内部没有任何特殊的方法。

public interface IBlobContainer : 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();

            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 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 也是在这个时候进行构造赋值。

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(
        this IBlobContainerFactory blobContainerFactory
    )
    {
        // 通过 GetContainerName 方法获取容器的名字。
        return blobContainerFactory.Create(
            BlobContainerNameAttribute.GetContainerName()
        );
    }
}

GetContainerName()
方法也很简单,如果容器类型没有指定 BlobContainerNameAttribute
特性,那么就会默认使用类型的 FullName
作为名称。

public static string GetContainerName(Type type)
{
    var nameAttribute = type.GetCustomAttribute();

    if (nameAttribute == null)
    {
        return type.FullName;
    }

    return nameAttribute.GetName(type);
}

最后的最后,看一下这个类型化的 BLOB 容器。

public class BlobContainer : IBlobContainer
    where TContainer : class
{
    private readonly IBlobContainer _container;

    public BlobContainer(IBlobContainerFactory blobContainerFactory)
    {
        _container = blobContainerFactory.Create();
    }

    // ... 其他代码。
}

对应的是模块初始化的工厂方法:

context.Services.AddTransient(
    typeof(IBlobContainer),
    serviceProvider => serviceProvider
        .GetRequiredService>()

这里的 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 options)
    {
        Options = options.Value;
    }
    
    public virtual BlobContainerConfiguration Get(string name)
    {
        return Options.Containers.GetConfiguration(name);
    }
}

这里的 BlobContainerConfigurations
对象,核心就是一个键值对,键就是 BLOB 容器的名称,值就是容器对应的配置对象。

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
决定的。

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()
的实现。

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()
,并在里面对具体的配置进行赋值。

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
进行操作。

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

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 系统可以简化开发人员对于大量二进制文件的管理操作。最近工作相当杂乱繁忙,下半年希望有时间继续学习更新吧。

其他相关文章,请参阅
文章目录