blob类型_Abp vNext 二进制大对象系统(BLOB)

d5334afb036f73cf4195317adb13dbb6.png

一、简介

ABP vNext 在 v 2.9.x 版本当中添加了 BLOB 系统,主要用于存储大型二进制文件。ABP 抽象了一套通用的 BLOB 体系,开发人员在存储或读取二进制文件时,可以忽略具体实现,直接使用IBlobContainer 或 IBlobContainer 进行操作。官方的 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 类,在其内部,只进行了两个操作,注入了 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,用于标识不同的容器,并且和其他的组件作为 关联键 进行绑定。每个容器都会关联 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();            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 系统可以简化开发人员对于大量二进制文件的管理操作。最近工作相当杂乱繁忙,下半年希望有时间继续学习更新吧。

相关文章:

  • 我和ABP vNext 的故事

  • 基于 abp vNext 和 .NET Core 开发博客项目 - 终结篇之发布项目

  • [Abp vNext 源码分析] - 19. 多租户

  • abp vnext2.0核心组件之DDD组件之实体结构源码解析

  • 基于Abp VNext框架设计 - Masstransit分布式消息

  • ABP vNext中使用开源日志面板 LogDashboard

af43de0f6b5bd5c43eadb65575345b61.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值