[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

智能推荐

“码农”一词是怎么来的?为什么中国程序员会被码农?程序员和农民有什么关联?-程序员宅基地

文章浏览阅读1.7w次。原创: 思齐大神 来源:蚁开源社区很多同学会问,IT行业在中国并不是特别差的行业,而程序员的工资也并不低,但为什么中国的程序员总被称作码农或者说是苦逼的程序员?中国的程序员生活和欧美的有什么不一样?​先说两个小段子街边,一对情侣在吵架。女孩对男孩说,“我们分手吧!”男孩沉默半天,开口问道,“我能再说最后一句话吗?”“说吧,婆婆妈妈的。”“我会编程……”“会编程有个屁用啊,现在到处都是会编程的人!”男孩涨红了脸,接着说道,“我会编程……我会变成…童话里,你爱的那个天使……”【程序._码农

Java初学者也能看的懂的AQS_aqs对接-程序员宅基地

文章浏览阅读259次。Java初学者也能看的懂的AQS学习AQS之前,你需要了解以下内容,如果不是很清楚,那么理解本文会有点吃力。(Java初学者得会一下内容)synchronizedCASLock前言synchronized首先我们知道synchronized是Java关键字,上锁释放锁等一切操作都是由JVM控制的。我们只能通过虚拟机的C++才能去研究其底层实现。我们除了判断synchronized是作为方法的修饰符,还是当做同步代码块使用以外,没什么需要我们程序员操作的。cas一种自旋的原子操作,也是J_aqs对接

【JZOJ5262】【GDOI2018模拟8.12】树(DP,性质题)_gdoi2018省选模拟树-程序员宅基地

文章浏览阅读460次。DescriptionSolution首先我们可以知道两个性质:1、路径u-v和路径v-w可以合并为路径u-w;2、路径u1-v1加路径u2-v2和路径u1-v2加路径u2-v1是等价的(就是起始点和终点可以互换) 那么知道这些性质之后就很好做了。我们只用知道每个点多少次做起点和多少次做终点。 我们设f[i]表示满足i子树的需求i上的值要是多少。 那么枚举i的所有儿子,判断a[i]-f[i],_gdoi2018省选模拟树

[PTA]7-65 字符串替换 (15 分)含思路_字符串替换pta-程序员宅基地

文章浏览阅读2.8k次,点赞4次,收藏28次。我们进行简单的运算即可实现倒序。_字符串替换pta

linux网络设置_linux如何开启网络连接-程序员宅基地

文章浏览阅读4k次,点赞5次,收藏22次。traceroute 180.101.50.188————————测试到180.101.50.188有多少个网关。vim /etc/sysconfig/static-routes——————————修改。netstat -antp | grep 22———————查看端口号22的相关信息。systemctl restart network————————————重启。systemctl restart network————————重新启动。_linux如何开启网络连接

pr中,视频导入后,视频画面大小显示不完整应该如何解决?_avi视频到pr里会放大-程序员宅基地

文章浏览阅读4w次,点赞23次,收藏6次。本人pr小白,今天编辑视频时候遇到了问题,也解决了,所以分享记录一下。问题一视频下面原来有字幕的,可是导入的视频变大了,现在看不到了怎么办?还有就是,频导入之后画质好像变糊了又是为什么?解决:将箭头放到要编辑的视频那里,右击,然后点击设为帧大小这样完整的视频就出来了。问题二如果视频模糊,就是序列设置的不对 要先新建序列一般的都是1920×1080本人博客:https://blog.csdn.net/weixin_46654114本人b站求关注:https://space.bi_avi视频到pr里会放大

随便推点

SeetaFace2 Android 平台编译_seetafacerecognizer2.0.ats-程序员宅基地

文章浏览阅读4.1k次。SeetaFace2 Android 平台编译项目地址:https://github.com/seetafaceengine/SeetaFace2SeetaFace2 人脸识别引擎包括了搭建一套全自动人脸识别系统所需的三个核心模块,即:人脸检测模块 FaceDetector、面部关键点定位模块 FaceLandmarker 以及人脸特征提取与比对模块 FaceRecognizer。面部关键点定位支持 5 点 和 81 点定位,两个辅助模块 FaceTracker 和 QualityAssessor 用_seetafacerecognizer2.0.ats

Oracle删除约束和主键的语句_oracle删除主键的sql语句-程序员宅基地

文章浏览阅读3.2w次,点赞4次,收藏34次。1.删除约束语句:alter table 表名 drop constraint 约束名;alter table mz_sf4 drop constraint pk_id1;2.删除主键语句:alter table 表名 drop primary key;alter table mz_sf3 drop primary key;如果出错:ORA-02273:此唯一主键已_oracle删除主键的sql语句

MySQL~InnoDB的备份和主从复制-程序员宅基地

文章浏览阅读989次,点赞9次,收藏13次。这份面试题几乎包含了他在一年内遇到的所有面试题以及答案,甚至包括面试中的细节对话以及语录,可谓是细节到极致,甚至简历优化和怎么投简历更容易得到面试机会也包括在内!也包括教你怎么去获得一些大厂,比如阿里,腾讯的内推名额!某位名人说过成功是靠99%的汗水和1%的机遇得到的,而你想获得那1%的机遇你首先就得付出99%的汗水!你只有朝着你的目标一步一步坚持不懈的走下去你才能有机会获得成功!成功只会留给那些有准备的人!《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取。

大数据平台核心技术 学堂在线 雨课堂 第八讲作业答案 人文交流月_vertectorization-程序员宅基地

文章浏览阅读2k次。关于Vertectorization哪些是正确的( )相对于其他编程模型,sql在大数据领域有哪些好处( )哪些部分适合做codegen( )关于内存计算描述不正确的有( )_vertectorization

java汉字拼音简码_java生成首字母拼音简码的总结-程序员宅基地

文章浏览阅读306次。百度找到了某论坛高人写的java(具体论坛记不清了),直接用来调用,再次非常感谢,基本上实现了我的需求package MD5;import java.util.Scanner;public class ChineseToPinYin {/*** 汉字转拼音缩写** @param str* 要转换的汉字字符串* @return String 拼音缩写*/public Strin..._java生成拼音码

C++ 数据结构——堆排序_数据结构堆排序c++-程序员宅基地

文章浏览阅读93次。/* 堆排序 */#include <iostream>using namespace std;int *data;void Sift(int k,int last){ int i,j,temp; i=k;j=2*i+1; while (j<=last) { if(j<last&&data[j]<data[j+1]) j++; if(data[i]>data[j]) _数据结构堆排序c++