[Abp vNext 源码分析] - 11. 用户的自定义参数与配置_abp vnext配置configure-程序员宅基地

技术标签: 技术  

一、简要说明

文章信息:

基于的 ABP vNext 版本:1.0.0

创作日期:2019 年 10 月 23 日晚

更新日期:2019 年 10 月 24 日

ABP vNext 针对用户可编辑的配置,提供了单独的 Volo.Abp.Settings 模块,本篇文章的后面都将这种用户可变更的配置,叫做 参数。所谓可编辑的配置,就是我们在系统页面上,用户可以动态更改的参数值。

例如你做的系统是一个门户网站,那么前端页面上展示的 Title ,你可以在后台进行配置。这个时候你就可以将网站这种全局配置作为一个参数,在程序代码中进行定义。通过 GlobalSettingValueProvider(后面会讲) 作为这个参数的值提供者,用户就可以随时对 Title 进行更改。又或者是某些通知的开关,你也可以定义一堆参数,让用户可以动态的进行变更。

二、源码分析

模块启动流程

AbpSettingsModule 模块干的事情只有两件,第一是扫描所有 ISettingDefinitionProvider (参数定义提供者),第二则是往配置参数添加一堆参数值提供者(ISettingValueProvider)。

public class AbpSettingsModule : AbpModule
{
    public override void PreConfigureServices(ServiceConfigurationContext context)
    {
        // 自动扫描所有实现了 ISettingDefinitionProvider 的类型。
        AutoAddDefinitionProviders(context.Services);
    }

    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        // 配置默认的一堆参数值提供者。
        Configure<AbpSettingOptions>(options =>
        {
            options.ValueProviders.Add<DefaultValueSettingValueProvider>();
            options.ValueProviders.Add<GlobalSettingValueProvider>();
            options.ValueProviders.Add<TenantSettingValueProvider>();
            options.ValueProviders.Add<UserSettingValueProvider>();
        });
    }

    private static void AutoAddDefinitionProviders(IServiceCollection services)
    {
        var definitionProviders = new List<Type>();

        services.OnRegistred(context =>
        {
            if (typeof(ISettingDefinitionProvider).IsAssignableFrom(context.ImplementationType))
            {
                definitionProviders.Add(context.ImplementationType);
            }
        });

        // 将扫描到的数据添加到 Options 中。
        services.Configure<AbpSettingOptions>(options =>
        {
            options.DefinitionProviders.AddIfNotContains(definitionProviders);
        });
    }
}

参数的定义

参数的基本定义

ABP vNext 关于参数的定义在类型 SettingDefinition 可以找到,内部的结构与 PermissionDefine 类似。。开发人员需要先定义有哪些可配置的参数,然后 ABP vNext 会自动进行管理,在网站运行期间,用户、租户可以根据自己的需要随时变更参数值。

public class SettingDefinition
{
    /// <summary>
    /// 参数的唯一标识。
    /// </summary>
    [NotNull]
    public string Name { get; }

    // 参数的显示名称,是一个多语言字符串。
    [NotNull]
    public ILocalizableString DisplayName
    {
        get => _displayName;
        set => _displayName = Check.NotNull(value, nameof(value));
    }
    private ILocalizableString _displayName;

    // 参数的描述信息,也是一个多语言字符串。
    [CanBeNull]
    public ILocalizableString Description { get; set; }

    /// <summary>
    /// 参数的默认值。
    /// </summary>
    [CanBeNull]
    public string DefaultValue { get; set; }

    /// <summary>
    /// 指定参数与其参数的值,是否能够在客户端进行显示。对于某些密钥设置来说是很危险的,默认值为 Fasle。
    /// </summary>
    public bool IsVisibleToClients { get; set; }

    /// <summary>
    /// 允许更改本参数的值提供者,为空则允许所有提供者提供参数值。
    /// </summary>
    public List<string> Providers { get; } //TODO: 考虑重命名为 AllowedProviders。

    /// <summary>
    /// 当前参数是否能够继承父类的 Scope 信息,默认值为 True。
    /// </summary>
    public bool IsInherited { get; set; }

    /// <summary>
    /// 参数相关连的一些扩展属性,通过一个字典进行存储。
    /// </summary>
    [NotNull]
    public Dictionary<string, object> Properties { get; }

    /// <summary>
    /// 参数的值是否以加密的形式存储,默认值为 False。
    /// </summary>
    public bool IsEncrypted { get; set; }

    public SettingDefinition(
        string name,
        string defaultValue = null,
        ILocalizableString displayName = null,
        ILocalizableString description = null,
        bool isVisibleToClients = false,
        bool isInherited = true,
        bool isEncrypted = false)
    {
        Name = name;
        DefaultValue = defaultValue;
        IsVisibleToClients = isVisibleToClients;
        DisplayName = displayName ?? new FixedLocalizableString(name);
        Description = description;
        IsInherited = isInherited;
        IsEncrypted = isEncrypted;

        Properties = new Dictionary<string, object>();
        Providers = new List<string>();
    }

    // 设置附加数据值。
    public virtual SettingDefinition WithProperty(string key, object value)
    {
        Properties[key] = value;
        return this;
    }

    // 设置 Provider 属性的值。
    public virtual SettingDefinition WithProviders(params string[] providers)
    {
        if (!providers.IsNullOrEmpty())
        {
            Providers.AddRange(providers);
        }

        return this;
    }
}

上面的参数定义值得注意的就是 DefaultValueIsVisibleToClientsIsEncrypted 这三个属性。默认值一般适用于某些系统配置,例如当前系统的默认语言。后面两个属性则更加注重于 安全问题,因为某些参数存储的是一些重要信息,这个时候就需要进行特殊处理了。

如果参数值是加密的,那么在获取参数值的时候就会进行解密操作,例如下面的代码。

SettingProvider 类中的相关代码:

// ...
public class SettingProvider : ISettingProvider, ITransientDependency
{
    // ...
    public virtual async Task<string> GetOrNullAsync(string name)
    {
        // ...
        var value = await GetOrNullValueFromProvidersAsync(providers, setting);
        // 对值进行解密处理。
        if (setting.IsEncrypted)
        {
            value = SettingEncryptionService.Decrypt(setting, value);
        }

        return value;
    }

    // ...
}

参数不对客户端可见的话,在默认的 AbpApplicationConfigurationAppService 服务类中,获取参数值的时候就会跳过。

private async Task<ApplicationSettingConfigurationDto> GetSettingConfigAsync()
{
    var result = new ApplicationSettingConfigurationDto
    {
        Values = new Dictionary<string, string>()
    };

    foreach (var settingDefinition in _settingDefinitionManager.GetAll())
    {
        // 不会展示这些属性为 False 的参数。
        if (!settingDefinition.IsVisibleToClients)
        {
            continue;
        }

        result.Values[settingDefinition.Name] = await _settingProvider.GetOrNullAsync(settingDefinition.Name);
    }

    return result;
}
参数定义的扫描

跟权限定义类似,所有的参数定义都被放在了 SettingDefinitionProvider 里面,如果你需要定义一堆参数,只需要继承并实现 Define(ISettingDefinitionContext) 抽象方法就可以了。

public class TestSettingDefinitionProvider : SettingDefinitionProvider
{
    public override void Define(ISettingDefinitionContext context)
    {
        context.Add(
            new SettingDefinition(TestSettingNames.TestSettingWithoutDefaultValue),
            new SettingDefinition(TestSettingNames.TestSettingWithDefaultValue, "default-value"),
            new SettingDefinition(TestSettingNames.TestSettingEncrypted, isEncrypted: true)
        );
    }
}

因为我们的 SettingDefinitionProvider 实现了 ISettingDefinitionProviderITransientDependency 接口,所以这些 Provider 都会在组件注册的时候(模块里面有定义),添加到对应的 AbpSettingOptions 内部,方便后续进行调用。

参数定义的管理

我们的 参数定义提供者参数值提供者 都赋值给 AbpSettingOptions 了,首先看有哪些地方使用到了 参数定义提供者

第二个我们已经看过,是在模块启动时有用到。第一个则是有一个 SettingDefinitionManager ,顾名思义就是管理所有的 SettingDefinition 的管理器。这个管理器提供了三个方法,都是针对 SettingDefinition 的查询功能。

public interface ISettingDefinitionManager
{
    // 根据参数定义的标识查询,不存在则抛出 AbpException 异常。
    [NotNull]
    SettingDefinition Get([NotNull] string name);

    // 获得所有的参数定义。
    IReadOnlyList<SettingDefinition> GetAll();

    // 根据参数定义的标识查询,如果不存在则返回 null。
    SettingDefinition GetOrNull(string name);
}

接下来我们看一下它的默认实现 SettingDefinitionManager ,它的内部没什么说的,只是注意 SettingDefinitions 的填充方式,这里使用了线程安全的 懒加载模式。只有当用到的时候,才会调用 CreateSettingDefinitions() 方法填充数据。

public class SettingDefinitionManager : ISettingDefinitionManager, ISingletonDependency
{
    protected Lazy<IDictionary<string, SettingDefinition>> SettingDefinitions { get; }

    protected AbpSettingOptions Options { get; }

    protected IServiceProvider ServiceProvider { get; }

    public SettingDefinitionManager(
        IOptions<AbpSettingOptions> options,
        IServiceProvider serviceProvider)
    {
        ServiceProvider = serviceProvider;
        Options = options.Value;

        // 填充的时候,调用 CreateSettingDefinitions 方法进行填充。
        SettingDefinitions = new Lazy<IDictionary<string, SettingDefinition>>(CreateSettingDefinitions, true);
    }

    // ...

    protected virtual IDictionary<string, SettingDefinition> CreateSettingDefinitions()
    {
        var settings = new Dictionary<string, SettingDefinition>();

        using (var scope = ServiceProvider.CreateScope())
        {
            // 从 Options 中得到类型,然后通过 IoC 进行实例化。
            var providers = Options
                .DefinitionProviders
                .Select(p => scope.ServiceProvider.GetRequiredService(p) as ISettingDefinitionProvider)
                .ToList();

            // 执行每个 Provider 的 Define 方法填充数据。
            foreach (var provider in providers)
            {
                provider.Define(new SettingDefinitionContext(settings));
            }
        }

        return settings;
    }
}

参数值的管理

当我们构建好参数的定义之后,我们要设置某个参数的值,或者说获取某个参数的值应该怎么操作呢?查看相关的单元测试,看到了 ABP vNext 自身是注入 ISettingProvider ,调用它的 GetOrNullAsync() 获取参数值。

private readonly ISettingProvider _settingProvider;

var settingValue = await _settingProvider.GetOrNullAsync("WebSite.Title")

跳转到接口,发现它有两个实现,这里我们只讲解一下 SettingProvider 类的实现。

获取参数值

直奔主题,来看一下 ISettingProvider.GetOrNullAsync(string) 方法是怎么来获取参数值的。

public class SettingProvider : ISettingProvider, ITransientDependency
{
    protected ISettingDefinitionManager SettingDefinitionManager { get; }
    protected ISettingEncryptionService SettingEncryptionService { get; }
    protected ISettingValueProviderManager SettingValueProviderManager { get; }

    public SettingProvider(
        ISettingDefinitionManager settingDefinitionManager,
        ISettingEncryptionService settingEncryptionService,
        ISettingValueProviderManager settingValueProviderManager)
    {
        SettingDefinitionManager = settingDefinitionManager;
        SettingEncryptionService = settingEncryptionService;
        SettingValueProviderManager = settingValueProviderManager;
    }

    public virtual async Task<string> GetOrNullAsync(string name)
    {
        // 根据名称获取参数定义。
        var setting = SettingDefinitionManager.Get(name);

        // 从参数值提供者管理器,获得一堆参数值提供者。
        var providers = Enumerable
            .Reverse(SettingValueProviderManager.Providers);

        // 过滤符合参数定义的提供者,这里就是用到了之前参数定义的 List<string> Providers 属性。
        if (setting.Providers.Any())
        {
            providers = providers.Where(p => setting.Providers.Contains(p.Name));
        }

        //TODO: How to implement setting.IsInherited?
        //TODO: 如何实现 setting.IsInherited 功能?

        var value = await GetOrNullValueFromProvidersAsync(providers, setting);
        // 如果参数是加密的,则需要进行解密操作。
        if (setting.IsEncrypted)
        {
            value = SettingEncryptionService.Decrypt(setting, value);
        }

        return value;
    }

    protected virtual async Task<string> GetOrNullValueFromProvidersAsync(IEnumerable<ISettingValueProvider> providers,
    SettingDefinition setting)
    {
        // 只要从任意 Provider 中,读取到了参数值,就直接进行返回。
        foreach (var provider in providers)
        {
            var value = await provider.GetOrNullAsync(setting);
            if (value != null)
            {
                return value;
            }
        }

        return null;
    }

    // ...
}

所以真正干活的还是 ISettingValueProviderManager 里面存放的一堆 ISettingValueProvider ,这个 参数值管理器 的接口很简单,只提供了一个 List<ISettingValueProvider> Providers { get; } 的定义。

它会从模块配置的 ValueProviders 属性内部,通过 IoC 实例化对应的参数值提供者。

_lazyProviders = new Lazy<List<ISettingValueProvider>>(
    () => Options
        .ValueProviders
        .Select(type => serviceProvider.GetRequiredService(type) as ISettingValueProvider)
        .ToList(),
    true
参数值提供者

参数值提供者的接口定义是 ISettingValueProvider,它定义了一个名称和 GetOrNullAsync(SettingDefinition) 方法,后者可以通过参数定义获取存储的值。

public interface ISettingValueProvider
{
    string Name { get; }

    Task<string> GetOrNullAsync([NotNull] SettingDefinition setting);
}

注意这里的返回值是 Task<string> ,也就是说我们的参数值类型必须是 string 类型的,如果需要存储其他的类型可能就需要从 string 进行类型转换了。

在这里的 SettingValueProvider 其实类似于我们之前讲过的 权限提供者。因为 ABP vNext 考虑到了多种情况,我们的参数值有可能是根据用户获取的,同时也有可能是根据不同的租户进行获取的。所以 ABP vNext 为我们预先定义了四种参数值提供器,他们分别是 DefaultValueSettingValueProviderGlobalSettingValueProviderTenantSettingValueProviderUserSettingValueProvider

下面我们就来讲讲这几个不同的参数提供者有啥不一样。

DefaultValueSettingValueProvider

顾名思义,默认值参数提供者就是使用的参数定义里面的 DefaultValue 属性,当你查询某个参数值的时候,就直接返回了。

public override Task<string> GetOrNullAsync(SettingDefinition setting)
{
    return Task.FromResult(setting.DefaultValue);
}

GlobalSettingValueProvider

这是一种全局的提供者,它没有对应的 Key,也就是说如果数据库能查到 ProviderNameG 的记录,就直接返回它的值了。

public class GlobalSettingValueProvider : SettingValueProvider
{
    public const string ProviderName = "G";

    public override string Name => ProviderName;

    public GlobalSettingValueProvider(ISettingStore settingStore) 
        : base(settingStore)
    {
    }

    public override Task<string> GetOrNullAsync(SettingDefinition setting)
    {
        return SettingStore.GetOrNullAsync(setting.Name, Name, null);
    }
}

TenantSettingValueProvider

租户提供者,则是会将当前登录租户的 Id 结合 T 进行查询,也就是参数值是按照不同的租户进行隔离的。

public class TenantSettingValueProvider : SettingValueProvider
{
    public const string ProviderName = "T";

    public override string Name => ProviderName;

    protected ICurrentTenant CurrentTenant { get; }
    
    public TenantSettingValueProvider(ISettingStore settingStore, ICurrentTenant currentTenant)
        : base(settingStore)
    {
        CurrentTenant = currentTenant;
    }

    public override async Task<string> GetOrNullAsync(SettingDefinition setting)
    {
        return await SettingStore.GetOrNullAsync(setting.Name, Name, CurrentTenant.Id?.ToString());
    }
}

UserSettingValueProvider

用户提供者,则是会将当前用户的 Id 作为查询条件,结合 U 在数据库进行查询匹配的参数值,参数值是根据不同的用户进行隔离的。

public class UserSettingValueProvider : SettingValueProvider
{
    public const string ProviderName = "U";

    public override string Name => ProviderName;

    protected ICurrentUser CurrentUser { get; }

    public UserSettingValueProvider(ISettingStore settingStore, ICurrentUser currentUser)
        : base(settingStore)
    {
        CurrentUser = currentUser;
    }

    public override async Task<string> GetOrNullAsync(SettingDefinition setting)
    {
        if (CurrentUser.Id == null)
        {
            return null;
        }

        return await SettingStore.GetOrNullAsync(setting.Name, Name, CurrentUser.Id.ToString());
    }
}
参数值的存储

除了 DefaultValueSettingValueProvider 是直接从参数定义获取值以外,其他的参数值提供者都是通过 ISettingStore 读取参数值的。在该模块的默认实现当中,是直接返回 null 的,只有当你使用了 Volo.Abp.SettingManagement 模块,你的参数值才是存储到数据库当中的。

我这里不再详细解析 Volo.Abp.SettingManagement 模块的其他实现,只说一下 ISettingStore 在它内部的实现 SettingStore

public class SettingStore : ISettingStore, ITransientDependency
{
    protected ISettingManagementStore ManagementStore { get; }

    public SettingStore(ISettingManagementStore managementStore)
    {
        ManagementStore = managementStore;
    }

    public Task<string> GetOrNullAsync(string name, string providerName, string providerKey)
    {
        return ManagementStore.GetOrNullAsync(name, providerName, providerKey);
    }
}

我们可以看到它也只是个包装,真正的操作类型是 ISettingManagementStore

参数值的设置

在 ABP vNext 的核心模块当中,是没有提供对参数值的变更的。只有在 Volo.Abp.SettingManagement 模块内部,它提供了 ISettingManager 管理器,可以进行参数值的变更。原理很简单,就是对数据库对应的表进行修改而已。

public async Task SetAsync(string name, string value, string providerName, string providerKey)
{
    // 操作仓储,查询记录。
    var setting = await SettingRepository.FindAsync(name, providerName, providerKey);
    
    // 新增或者更新记录。
    if (setting == null)
    {
        setting = new Setting(GuidGenerator.Create(), name, value, providerName, providerKey);
        await SettingRepository.InsertAsync(setting);
    }
    else
    {
        setting.Value = value;
        await SettingRepository.UpdateAsync(setting);
    }
}

三、总结

ABP vNext 提供了多种参数值提供者,我们可以根据自己的需要灵活选择。如果不能够满足你的需求,你也可以自己实现一个参数值提供者。我建议对于用户在界面可更改的参数,都可以使用 SettingDefinition 定义成参数,可以根据不同的情况进行配置读取。

ABP vNext 其他模块用到的许多参数,也都是使用的 SettingDefinition 进行定义。例如 Identity 模块用到的密码验证规则,就是通过 ISettingProvider 进行读取的,还有当前程序的默认语言。

需要看其他的 ABP vNext 相关文章?点击我 即可跳转到总目录。

下面附上 E2Home 的总结,很详细:

  1. 在各个模块中定义设置数据源的类来设定配置键值对, 该类只需要继承接口 ISettingDefinitionProvider 或者 SettingDefinitionProvider 实现类
    ABP 会自动寻找被注册,最后会将配置键值对都汇总到 SettingProvider 类中。如果是存储在数据库中的,则需要重写 ISettingStore
    当然建议依赖 Volo.Abp.SettingManagement.Domain 这个模块,如果数据表是用自定义的,则建议重写 ISettingRepository 接口即可。

  2. ConfigureServices() 方法中注册添加 ISettingValueProvider,比如:值是 json 格式的,就可以定义一个设置值 Provider 来解析。

  3. ISettingValueProvider 可以有多个,并且按倒序进行执行,只要能获取到值就返回,不再继续往下执行。一般自定义的 ISettingValueProvider 放在后面。

  4. 如果将敏感数据保存到设置管理,则建议采用加密的方式,只需要重写 ISettingEncryptionService 即可。 参数定义:IsEncrypted = true

  5. Volo.Abp.SettingManagement.Domain 是采用数据库加缓存的方式来读写设置的,
    通过 SettingCacheItemInvalidator 来注册 Setting 实体的 EntityChanged 事件,从而达到缓存能跟实体同步更新。

  6. 为啥 ABP 还需要设置管理,而不用 .NET Core 自带的配置(Configuration)?
    因为 ABP 设置管理可以做到三个层级,用户,租户和全局(系统级),同时 ABP 的设置管理只是做了一层封装,
    具体的数据源可以是 .NET Core 自带的配置(Configuration),也可以是分布式配置。只不过需要我们自己去写扩展。

  7. 另外建议大家对参数进行打包,比如邮件相关的参数可以封装在一个 EmailConfig 类中,邮件 Host,用户名和密码都是该类的属性,而具体取值同时通过 ISettingValueProvider 来获取的。建议加入分布式缓存。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_45534118/article/details/106169370

智能推荐

稀疏编码的数学基础与理论分析-程序员宅基地

文章浏览阅读290次,点赞8次,收藏10次。1.背景介绍稀疏编码是一种用于处理稀疏数据的编码技术,其主要应用于信息传输、存储和处理等领域。稀疏数据是指数据中大部分元素为零或近似于零的数据,例如文本、图像、音频、视频等。稀疏编码的核心思想是将稀疏数据表示为非零元素和它们对应的位置信息,从而减少存储空间和计算复杂度。稀疏编码的研究起源于1990年代,随着大数据时代的到来,稀疏编码技术的应用范围和影响力不断扩大。目前,稀疏编码已经成为计算...

EasyGBS国标流媒体服务器GB28181国标方案安装使用文档-程序员宅基地

文章浏览阅读217次。EasyGBS - GB28181 国标方案安装使用文档下载安装包下载,正式使用需商业授权, 功能一致在线演示在线API架构图EasySIPCMSSIP 中心信令服务, 单节点, 自带一个 Redis Server, 随 EasySIPCMS 自启动, 不需要手动运行EasySIPSMSSIP 流媒体服务, 根..._easygbs-windows-2.6.0-23042316使用文档

【Web】记录巅峰极客2023 BabyURL题目复现——Jackson原生链_原生jackson 反序列化链子-程序员宅基地

文章浏览阅读1.2k次,点赞27次,收藏7次。2023巅峰极客 BabyURL之前AliyunCTF Bypassit I这题考查了这样一条链子:其实就是Jackson的原生反序列化利用今天复现的这题也是大同小异,一起来整一下。_原生jackson 反序列化链子

一文搞懂SpringCloud,详解干货,做好笔记_spring cloud-程序员宅基地

文章浏览阅读734次,点赞9次,收藏7次。微服务架构简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。这么多小服务,如何管理他们?(服务治理 注册中心[服务注册 发现 剔除])这么多小服务,他们之间如何通讯?这么多小服务,客户端怎么访问他们?(网关)这么多小服务,一旦出现问题了,应该如何自处理?(容错)这么多小服务,一旦出现问题了,应该如何排错?(链路追踪)对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。_spring cloud

Js实现图片点击切换与轮播-程序员宅基地

文章浏览阅读5.9k次,点赞6次,收藏20次。Js实现图片点击切换与轮播图片点击切换<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title></title> <script type="text/ja..._点击图片进行轮播图切换

tensorflow-gpu版本安装教程(过程详细)_tensorflow gpu版本安装-程序员宅基地

文章浏览阅读10w+次,点赞245次,收藏1.5k次。在开始安装前,如果你的电脑装过tensorflow,请先把他们卸载干净,包括依赖的包(tensorflow-estimator、tensorboard、tensorflow、keras-applications、keras-preprocessing),不然后续安装了tensorflow-gpu可能会出现找不到cuda的问题。cuda、cudnn。..._tensorflow gpu版本安装

随便推点

物联网时代 权限滥用漏洞的攻击及防御-程序员宅基地

文章浏览阅读243次。0x00 简介权限滥用漏洞一般归类于逻辑问题,是指服务端功能开放过多或权限限制不严格,导致攻击者可以通过直接或间接调用的方式达到攻击效果。随着物联网时代的到来,这种漏洞已经屡见不鲜,各种漏洞组合利用也是千奇百怪、五花八门,这里总结漏洞是为了更好地应对和预防,如有不妥之处还请业内人士多多指教。0x01 背景2014年4月,在比特币飞涨的时代某网站曾经..._使用物联网漏洞的使用者

Visual Odometry and Depth Calculation--Epipolar Geometry--Direct Method--PnP_normalized plane coordinates-程序员宅基地

文章浏览阅读786次。A. Epipolar geometry and triangulationThe epipolar geometry mainly adopts the feature point method, such as SIFT, SURF and ORB, etc. to obtain the feature points corresponding to two frames of images. As shown in Figure 1, let the first image be ​ and th_normalized plane coordinates

开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先抽取关系)_语义角色增强的关系抽取-程序员宅基地

文章浏览阅读708次,点赞2次,收藏3次。开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先关系再实体)一.第二代开放信息抽取系统背景​ 第一代开放信息抽取系统(Open Information Extraction, OIE, learning-based, 自学习, 先抽取实体)通常抽取大量冗余信息,为了消除这些冗余信息,诞生了第二代开放信息抽取系统。二.第二代开放信息抽取系统历史第二代开放信息抽取系统着眼于解决第一代系统的三大问题: 大量非信息性提取(即省略关键信息的提取)、_语义角色增强的关系抽取

10个顶尖响应式HTML5网页_html欢迎页面-程序员宅基地

文章浏览阅读1.1w次,点赞6次,收藏51次。快速完成网页设计,10个顶尖响应式HTML5网页模板助你一臂之力为了寻找一个优质的网页模板,网页设计师和开发者往往可能会花上大半天的时间。不过幸运的是,现在的网页设计师和开发人员已经开始共享HTML5,Bootstrap和CSS3中的免费网页模板资源。鉴于网站模板的灵活性和强大的功能,现在广大设计师和开发者对html5网站的实际需求日益增长。为了造福大众,Mockplus的小伙伴整理了2018年最..._html欢迎页面

计算机二级 考试科目,2018全国计算机等级考试调整,一、二级都增加了考试科目...-程序员宅基地

文章浏览阅读282次。原标题:2018全国计算机等级考试调整,一、二级都增加了考试科目全国计算机等级考试将于9月15-17日举行。在备考的最后冲刺阶段,小编为大家整理了今年新公布的全国计算机等级考试调整方案,希望对备考的小伙伴有所帮助,快随小编往下看吧!从2018年3月开始,全国计算机等级考试实施2018版考试大纲,并按新体系开考各个考试级别。具体调整内容如下:一、考试级别及科目1.一级新增“网络安全素质教育”科目(代..._计算机二级增报科目什么意思

conan简单使用_apt install conan-程序员宅基地

文章浏览阅读240次。conan简单使用。_apt install conan