首页 科技正文

大发888在线:[Abp vNext 源码剖析] - 23. 二进制大工具系统(BLOB)

admin 科技 2020-07-28 36 0

一、简介

ABP vNext 在 v 2.9.x 版本当中添加了 BLOB 系统,主要用于存储大型二进制文件。ABP 抽象了一套通用的 BLOB 系统,开发人员在存储或读取二进制文件时,可以忽略详细实现,直接使用 IBlobContainerIBlobContainer<T> 举行操作。官方的 BLOB Provider 实现有 AzureAWSFileSystem(文件系统存储)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 类,在其内部,只举行了两个操作,注入了 IBlobContainerIBlobContainer<> 的实现。

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,用于标识差别的容器,而且和其他的组件作为 关联键 举行绑定。每个容器都市关联 BlobContainerConfigurationIBlobProvider 两个组件,它们划分提供了容器的设置信息和容器的详细实现 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 回首

  1. 开发人员可以在模块的 ConfigureService() 阶段为所有容器或者特定容器指定参数。
  2. ABP vNext 框架会注入所有的 BLOB Provider,并注入默认的 IBlobContainer<DefaultContainer> 容器和其他的类型化容器实现。
  3. 当需要使用 BLOB 时,开发人员注入了 IBlobContainerIBlobContainer<T>
  4. BLOB 容器的工厂会凭据容器的名称匹配对应的 BLOB Provider 和设置工具。
  5. BLOB Provider 凭据 **Args 参数内部附带的设置工具,读取对应的设置信息举行自界说的操作。

三、总结

小型项目直接集成 FileSystem 即可,中大型项目可以使用种种 OSS Provider,BLOB 系统可以简化开发人员对于大量二进制文件的治理操作。最近事情相当杂乱忙碌,下半年希望有时间继续学习更新吧。

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

,

www.allbetgaming.net

欢迎进入欧博平台网站(www.aLLbetgame.us),www.aLLbetgame.us开放欧博平台网址、欧博注册、欧博APP下载、欧博客户端下载、欧博游戏等业务。

版权声明

本文仅代表作者观点,
不代表本站Allbet的立场。
本文系作者授权发表,未经许可,不得转载。

评论