一、简介

ABP vNext 提供了全套的本地化字符串支持,具体用法可以参考官方使用文档。vNext 本身是对 Microsoft 提供的本地化组件进行了实现,通过 JSON 文件提供本地化源,这一点与老 ABP 不太一样,老 ABP 框架是全套自己手撸。vNext 针对服务端和客户端都提供了文字本地化的工具类,这样开发人员可以很快速地开发支持多语言的网站程序。

二、源码分析

本地化涉及的主要模块有 Volo.Abp.Localization.AbstractionsVolo.Abp.Localization,可以看到 Volo 针对包的结构也逐渐向 Microsoft 的构建方式靠拢。有直接依赖的模块是 Volo.Abp.VirtualFileSystem,之所以会引用到这个模块,是因为默认的本地化数据源是通过内嵌 JSON 文件实现的,所以会用到虚拟文件系统读取数据。

2.1 本地化的抽象接口

首先打开 Volo.Abp.Localization.Abstractions 项目,它的基本结构如下图所示,需要注意的核心类型就是 ILocalizableString 接口和它的两个具体实现 FixedLocalizableStringLocalizableString

这里的 IAbpStringLocalizerFactoryWithDefaultResourceSupport 接口是为 AbpStringLocalizerFactoryExtensions 服务的,后面会详细解释,主要作用是根据默认资源类型快速创建一个 IStringLocalizer 实例。

2.1.1 本地化字符串对象的封装

可以看到在该项目内部定义了一个 ILocalizableString 的接口,在 ABP vNext 内部需要用到多语言表示的字符串属性,都是定义的 ILocalizableString 类型。本质上它是针对 Microsoft 提供的 LocalizedString 进行了一层包装,这个接口只提供了一个方法 Localize(),具体的签名见下面的代码。

public interface ILocalizableString
{
LocalizedString Localize(IStringLocalizerFactory stringLocalizerFactory);
}

在 ABP vNext 框架当中,拥有两个实现,分别是 LocalizableStringFixedLocalizableString,后者用于创建固定字串的显示。例如 ABP vNext 自带的权限系统,针对权限名称和描述必须传递 ILocalizableString 类型的值,但是开发人员暂时没有提供对应的本地化翻译,这个时候就可以使用 FixedLocalizableString 传递固定字符串。

实现也是很简单,在调用了 Localize() 方法之后,会根据构造函数的 Value 创建一个新的 LocalizedString 对象。

public class FixedLocalizableString : ILocalizableString
{
public string Value { get; } public FixedLocalizableString(string value)
{
Value = value;
} public LocalizedString Localize(IStringLocalizerFactory stringLocalizerFactory)
{
return new LocalizedString(Value, Value);
}
}

用法举例:

public class DataDictionaryDefinitionPermissionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var dataDictionaryGroup = context.AddGroup(DataDictionaryPermissions.GroupName, L("Permission:DataDictionary")); var dataDictionary = dataDictionaryGroup.AddPermission(DataDictionaryPermissions.DataDictionary.Default, L("Permission:DataDictionary"));
dataDictionary.AddChild(DataDictionaryPermissions.DataDictionary.Create, L("Permission:Create"));
dataDictionary.AddChild(DataDictionaryPermissions.DataDictionary.Update, L("Permission:Edit"));
dataDictionary.AddChild(DataDictionaryPermissions.DataDictionary.Delete, L("Permission:Delete"));
// 这里使用了 FixedLocalizableString 提供本地化字符串。
dataDictionary.AddChild(DataDictionaryPermissions.DataDictionary.Management, new FixedLocalizableString("字典管理"));
} private static LocalizableString L(string name)
{
return LocalizableString.Create<DataDictionaryResource>(name);
}
}

另一个 LocalizableString 就是正常通过 IStringLocalizerFactory 获取的本地化字符串对象。

public class LocalizableString : ILocalizableString
{
[CanBeNull]
public Type ResourceType { get; } [NotNull]
public string Name { get; } public LocalizableString(Type resourceType, [NotNull] string name)
{
Name = Check.NotNullOrEmpty(name, nameof(name));
ResourceType = resourceType;
} public LocalizedString Localize(IStringLocalizerFactory stringLocalizerFactory)
{
return stringLocalizerFactory.Create(ResourceType)[Name];
} public static LocalizableString Create<TResource>([NotNull] string name)
{
return new LocalizableString(typeof(TResource), name);
}
}

在类型里面定义了一个静态方法,用于目标类型与 KEY 的 LocalizableString 对象,常见于权限定义的地方,在上面的示例代码中有用到过。

2.1.2 本地化资源类型的别名

TODO

2.2 本地化的基础设施

下文指代的基础设施是指 ABP vNext 为了优雅地实现 Microsoft 本地化接口所构建的一系列组件,ABP vNext 的大部分组件都是基于 Microsoft 提供的抽象体系,也是为了更好地兼容。

2.2.1 模块的启动

具体的实现模块逻辑也不复杂,首先替换了默认的本地化资源容器工厂。接着往 ABP 的虚拟文件系统添加了当前模块,以便后续访问对应的 JSON 文件,最后往本地化的相关配置项添加了两个本地化资源。

public class AbpLocalizationModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
AbpStringLocalizerFactory.Replace(context.Services); Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<AbpLocalizationModule>("Volo.Abp", "Volo/Abp");
}); Configure<AbpLocalizationOptions>(options =>
{
options
.Resources
.Add<DefaultResource>("en"); options
.Resources
.Add<AbpLocalizationResource>("en")
.AddVirtualJson("/Localization/Resources/AbpLocalization");
});
}
}

2.2.2 本地化的配置

AbpLocalizationOptions 内部定义了本地化系统的相关参数,主要由资源集合(Resources)、默认资源(DefaultResourceType)、全局的本地化数据源提供者(GlobalContributors)、支持的语言(Languages)。

注意:

当进行本地化操作时,没有指定资源类型的时候会使用默认资源类型。

public class AbpLocalizationOptions
{
public LocalizationResourceDictionary Resources { get; } public Type DefaultResourceType { get; set; } public ITypeList<ILocalizationResourceContributor> GlobalContributors { get; } public List<LanguageInfo> Languages { get; } public Dictionary<string, List<NameValue>> LanguagesMap { get; } public Dictionary<string, List<NameValue>> LanguageFilesMap { get; } public AbpLocalizationOptions()
{
Resources = new LocalizationResourceDictionary();
GlobalContributors = new TypeList<ILocalizationResourceContributor>();
Languages = new List<LanguageInfo>();
LanguagesMap = new Dictionary<string, List<NameValue>>();
LanguageFilesMap = new Dictionary<string, List<NameValue>>();
}
}

从上述代码我们可以知道,要让本地化系统正常工作,我们会接触到下面这几个类型 LocalizationResourceDictionaryLocalizationResourceILocalizationResourceContributorLanguageInfo

2.2.3 本地化资源的定义

在使用本地化系统的时候,ABP vNext 文档首先会让我们定义一个类型,并在模块的 ConfigureService() 周期,通过配置项添加到本地化系统当中,就像这样。

Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Add<DataDictionaryResource>("en")
.AddVirtualJson("/Localization/Resources");
});

这里可以看到,ABP vNext 实现了一套流畅方法(Fluent Method),通过这一系列的操作,我们会生成一个 LocalizationResource 实例,添加到配置系统当中,以便后续进行使用。

这里的 Add() 方法是由 LocalizationResourceDictionary 提供的,它本质上就是一个字典,只不过由 ABP vNext 封装了一些自定义的方法,方便添加字典项数据。可以看到它的实现也很简单,首先判断字典是否存在对应的 Key,如果不存在就使用资源类型和区域文化信息构造一个新的 LocalizationResource 对象,并将其添加到字典当中。

public class LocalizationResourceDictionary : Dictionary<Type, LocalizationResource>
{
public LocalizationResource Add<TResouce>([CanBeNull] string defaultCultureName = null)
{
return Add(typeof(TResouce), defaultCultureName);
} public LocalizationResource Add(Type resourceType, [CanBeNull] string defaultCultureName = null)
{
if (ContainsKey(resourceType))
{
throw new AbpException("This resource is already added before: " + resourceType.AssemblyQualifiedName);
} return this[resourceType] = new LocalizationResource(resourceType, defaultCultureName);
} // ... 其他代码。
}

转到 LocalizationResouce 的定义,内部存储了具体的资源类型、资源名称、当前资源默认的区域文化信息、本地化数据源提供者(与全局的不同,这里仅作用于某个具体本地化资源)、继承的基类资源类型集合。

资源类型和资源名称用于区分不同的本地化资源。默认区域文化信息代表当前资源,当获取指定语言的本地化字符串失败时,会读取默认的区域文化信息对应的本地化字符串。

这里的 BaseResouceTypes 是为了复用其他资源的本地化字符串,例如你定义了一个 AppleResouceType,但是你想要获取 FruitResouceType 对应的字符串,那么就需要往这个集合添加需要服用的资源类型。ABP vNext 为 LocalizationResource 提供了一个扩展方法 AddBaseTypes() 便于在模块配置时添加需要复用的类型。除此之外 ABP vNext 也提供了特性支持,跟模块定义一样,在类定义上面添加 InheritResourceAttribute 特性,传入需要复用的类型定义即可。

[InheritResource(
typeof(LocalizationTestValidationResource),
typeof(LocalizationTestCountryNamesResource)
)]
public sealed class LocalizationTestResource
{ }

可以看到在下述代码当中,ABP vNext 会扫描当前 ResouceType 的特性,并将其定义的复用类型添加到基类集合当中。

public class LocalizationResource
{
[NotNull]
public Type ResourceType { get; } [NotNull]
public string ResourceName => LocalizationResourceNameAttribute.GetName(ResourceType); [CanBeNull]
public string DefaultCultureName { get; set; } [NotNull]
public LocalizationResourceContributorList Contributors { get; } [NotNull]
public List<Type> BaseResourceTypes { get; } public LocalizationResource(
[NotNull] Type resourceType,
[CanBeNull] string defaultCultureName = null,
[CanBeNull] ILocalizationResourceContributor initialContributor = null)
{
ResourceType = Check.NotNull(resourceType, nameof(resourceType));
DefaultCultureName = defaultCultureName; BaseResourceTypes = new List<Type>();
Contributors = new LocalizationResourceContributorList(); if (initialContributor != null)
{
Contributors.Add(initialContributor);
} AddBaseResourceTypes();
} protected virtual void AddBaseResourceTypes()
{
var descriptors = ResourceType
.GetCustomAttributes(true)
.OfType<IInheritedResourceTypesProvider>(); foreach (var descriptor in descriptors)
{
foreach (var baseResourceType in descriptor.GetInheritedResourceTypes())
{
BaseResourceTypes.AddIfNotContains(baseResourceType);
}
}
}
}

当资源类型(Resource Type) 定义好之后,通过上面的一番操作,就能够得到一个 LocalizationResource 实例,并将其添加到了 AbpLocalizationOptions 内的 LocalizationResourceDictionary 对象。开发人员定义了多少个本地化资源类型,就会往这个字典添加多少个 LocaliztaionResource 实例。

2.2.4 本地化的数据源

不论是配置项还是某个本地化资源定义类,都会存储一组 Contributor 。转到对应的接口定义,这个接口定义了三个公开方法,分别用于初始化(Initialize())、获取某个具体的本地化字符串(LocalizedString())、为指定的字典填充本地化资源数据(Fill())。

public interface ILocalizationResourceContributor
{
void Initialize(LocalizationResourceInitializationContext context); LocalizedString GetOrNull(string cultureName, string name); void Fill(string cultureName, Dictionary<string, LocalizedString> dictionary);
}

所有的数据源都是由各个 Contributor 提供的,这里以 VirtualFileLocalizationResourceContributorBase 为例,在内部通过虚拟文件系统获取到了文件数据,通过文件数据构造一系列的字典。这里的 字典有两层,第一层的 Key 是区域文化信息,Value 是对应区域文化信息的本地化字符串字典。第二层的 Key 是本地化字符串的标识,Value 就是具体的 LocalizedString 实例。

public abstract class VirtualFileLocalizationResourceContributorBase : ILocalizationResourceContributor
{
// ... 其他代码
public void Initialize(LocalizationResourceInitializationContext context)
{
_virtualFileProvider = context.ServiceProvider.GetRequiredService<IVirtualFileProvider>();
} public LocalizedString GetOrNull(string cultureName, string name)
{
return GetDictionaries().GetOrDefault(cultureName)?.GetOrNull(name);
} public void Fill(string cultureName, Dictionary<string, LocalizedString> dictionary)
{
GetDictionaries().GetOrDefault(cultureName)?.Fill(dictionary);
} private Dictionary<string, ILocalizationDictionary> GetDictionaries()
{
// ... 获取本地化资源的字典,这里的字典按区域文化进行分组。
} private Dictionary<string, ILocalizationDictionary> CreateDictionaries()
{
var dictionaries = new Dictionary<string, ILocalizationDictionary>(); foreach (var file in _virtualFileProvider.GetDirectoryContents(_virtualPath))
{
// ... 其他代码。
// 根据文件创建某个区域文化的具体的数据源字典。
var dictionary = CreateDictionaryFromFile(file);
if (dictionaries.ContainsKey(dictionary.CultureName))
{
throw new AbpException($"{file.GetVirtualOrPhysicalPathOrNull()} dictionary has a culture name '{dictionary.CultureName}' which is already defined!");
} dictionaries[dictionary.CultureName] = dictionary;
} return dictionaries;
} protected abstract bool CanParseFile(IFileInfo file); protected virtual ILocalizationDictionary CreateDictionaryFromFile(IFileInfo file)
{
using (var stream = file.CreateReadStream())
{
return CreateDictionaryFromFileContent(Utf8Helper.ReadStringFromStream(stream));
}
} protected abstract ILocalizationDictionary CreateDictionaryFromFileContent(string fileContent);
}

2.2.5 本地化资源字典

具体存储本地化字符串标识和展示文本的对象是 ILocalizationDictionary,它的具体实现是 StaticLocalizationDictionary,在其内部有一个 Dictionary<string, LocalizedString> 字典,这个字典就对应的 JSON 文件当中的本地化数据了。

{
"culture": "zh-Hans",
"texts": {
"DisplayName:Abp.Localization.DefaultLanguage": "默认语言",
"Description:Abp.Localization.DefaultLanguage": "应用程序的默认语言."
}
}

对应的字典形式就是内部字典的 KEY:Value

public class StaticLocalizationDictionary : ILocalizationDictionary
{
public string CultureName { get; } protected Dictionary<string, LocalizedString> Dictionary { get; } public StaticLocalizationDictionary(string cultureName, Dictionary<string, LocalizedString> dictionary)
{
CultureName = cultureName;
Dictionary = dictionary;
} public virtual LocalizedString GetOrNull(string name)
{
return Dictionary.GetOrDefault(name);
} public void Fill(Dictionary<string, LocalizedString> dictionary)
{
foreach (var item in Dictionary)
{
dictionary[item.Key] = item.Value;
}
}
}

当最外层的 Contributor 获取文本的时候,实际是结合区域文化信息(Culture Name) 定义到对应的本地化资源字典,再通过 Name 获取资源字典内部对应的 LocalizedString 对象。

2.3 同 Microsoft 本地化集成

2.2 节讲完了 ABP vNext 实现的基础设施,结合某个资源类型附带的 Contributor 组就能够获取到具体的本地化字符串数据,在本节主要讲解 ABP vNext 同 Microsoft 的集成。

2.3.1 IStringLocalizer 工厂

AbpLocalizationModule 模块中,第一句就是替换了默认的 String Localizer 工厂,并注入了 ResourceManagerStringLocalizerFactory 类型,这个类型主要用于后续的默认行为。

internal static void Replace(IServiceCollection services)
{
services.Replace(ServiceDescriptor.Singleton<IStringLocalizerFactory, AbpStringLocalizerFactory>());
services.AddSingleton<ResourceManagerStringLocalizerFactory>();
}

在 Microsoft 提供的 IStringLocalizerFactory 接口中,只定义了两个创建 IStringLocalizer 的方法。

public interface IStringLocalizerFactory
{
IStringLocalizer Create(Type resourceSource); IStringLocalizer Create(string baseName, string location);
}

第二个方法 ABP 是直接调用的默认工厂(ResouceManagerStringLocalizerFactory) 提供的方法,而且还加了个 TODO 注明不知道什么时候会被调用。

public virtual IStringLocalizer Create(string baseName, string location)
{
//TODO: Investigate when this is called? return InnerFactory.Create(baseName, location);
}

这里我们着重关注第一个方法,ABP 主要实现的也是第一个方法,它会根据传入的 resourceSource 参数从缓存当中获取(不存在则构造)对应的 IStringLocalizer 。如果在 ABP 提供的资源集合当中,没有查找到对应的 Type,则直接调用默认工厂返回 IStringLocalizer。如果存在则会以 Type 作为 Key,StringLocalizerCacheItem(就是 LocalizationResource 的马甲) 作为 Value,从缓存拿,没拿到就构建一个新的并加入到缓存中。

public virtual IStringLocalizer Create(Type resourceType)
{
var resource = AbpLocalizationOptions.Resources.GetOrDefault(resourceType);
if (resource == null)
{
return InnerFactory.Create(resourceType);
} if (LocalizerCache.TryGetValue(resourceType, out var cacheItem))
{
return cacheItem.Localizer;
} lock (LocalizerCache)
{
return LocalizerCache.GetOrAdd(
resourceType,
_ => CreateStringLocalizerCacheItem(resource)
).Localizer;
}
} private StringLocalizerCacheItem CreateStringLocalizerCacheItem(LocalizationResource resource)
{
// 构造时会将全局配置的 Contributor 添加到对应的组。
foreach (var globalContributor in AbpLocalizationOptions.GlobalContributors)
{
resource.Contributors.Add((ILocalizationResourceContributor) Activator.CreateInstance(globalContributor));
} using (var scope = ServiceProvider.CreateScope())
{
var context = new LocalizationResourceInitializationContext(resource, scope.ServiceProvider); // 调用各个 Contributor 的初始化方法,进行初始化操作。
foreach (var contributor in resource.Contributors)
{
contributor.Initialize(context);
}
} return new StringLocalizerCacheItem(
new AbpDictionaryBasedStringLocalizer(
resource,
resource.BaseResourceTypes.Select(Create).ToList()
)
);
}

2.3.2 IStringLocalizer

ABP 针对 IStringLocalizer 的默认实现是 AbpDictionaryBasedStringLocalizerIStringLocalizer 主要包含两个索引器和一个 GetAllStrings() 方法。

索引器本身直接就调用的 GetLocalizedString()GetLocalizedStringFormatted() 方法。后者用于处理格式化的参数,内部就是利用的 string.Format() 方法替换占位符的内容。

public class AbpDictionaryBasedStringLocalizer : IStringLocalizer, IStringLocalizerSupportsInheritance
{
// ... 其他代码 public virtual LocalizedString this[string name] => GetLocalizedString(name); public virtual LocalizedString this[string name, params object[] arguments] => GetLocalizedStringFormatted(name, arguments); // ... 其他代码 protected virtual LocalizedString GetLocalizedString(string name)
{
return GetLocalizedString(name, CultureInfo.CurrentUICulture.Name);
} protected virtual LocalizedString GetLocalizedString(string name, string cultureName)
{
var value = GetLocalizedStringOrNull(name, cultureName); // 如果没有从当前容器取得对应的本地化字符串,就从复用的基类中获取。
if (value == null)
{
foreach (var baseLocalizer in BaseLocalizers)
{
using (CultureHelper.Use(CultureInfo.GetCultureInfo(cultureName)))
{
var baseLocalizedString = baseLocalizer[name];
if (baseLocalizedString != null && !baseLocalizedString.ResourceNotFound)
{
return baseLocalizedString;
}
}
} return new LocalizedString(name, name, resourceNotFound: true);
} return value;
}
}

转到 GetLocalizedStringOrNull() 方法内部,可以看到获取本地化字符串的具体逻辑。

  1. 首先会从本地化资源定义的 Contributors 中获取本地化字符串。
  2. 如果没有找到则尝试从类似的区域文化信息字典中获取,例如 zh-Hans(简体中文) 源没有拿到则考虑从 zh-Hant(繁体中文)获取。
  3. 还是没有取得,最后会使用默认的区域文化信息匹配对应的本地化字符串,一般来说该值建议设置为 en
protected virtual LocalizedString GetLocalizedStringOrNull(string name, string cultureName, bool tryDefaults = true)
{
//Try to get from original dictionary (with country code)
var strOriginal = Resource.Contributors.GetOrNull(cultureName, name);
if (strOriginal != null)
{
return strOriginal;
} if (!tryDefaults)
{
return null;
} //Try to get from same language dictionary (without country code)
if (cultureName.Contains("-")) //Example: "tr-TR" (length=5)
{
var strLang = Resource.Contributors.GetOrNull(CultureHelper.GetBaseCultureName(cultureName), name);
if (strLang != null)
{
return strLang;
}
} //Try to get from default language
if (!Resource.DefaultCultureName.IsNullOrEmpty())
{
var strDefault = Resource.Contributors.GetOrNull(Resource.DefaultCultureName, name);
if (strDefault != null)
{
return strDefault;
}
} //Not found
return null;
}

三、总结

[Abp vNext 源码分析] - 21. 界面与文字的本地化的更多相关文章

  1. [Abp vNext 源码分析] - 文章目录

    一.简要介绍 ABP vNext 是 ABP 框架作者所发起的新项目,截止目前 (2019 年 2 月 18 日) 已经拥有 1400 多个 Star,最新版本号为 v 0.16.0 ,但还属于预览版 ...

  2. [Abp vNext 源码分析] - 5. DDD 的领域层支持(仓储、实体、值对象)

    一.简要介绍 ABP vNext 框架本身就是围绕着 DDD 理念进行设计的,所以在 DDD 里面我们能够见到的实体.仓储.值对象.领域服务,ABP vNext 框架都为我们进行了实现,这些基础设施都 ...

  3. [Abp vNext 源码分析] - 11. 用户的自定义参数与配置

    一.简要说明 文章信息: 基于的 ABP vNext 版本:1.0.0 创作日期:2019 年 10 月 23 日晚 更新日期:暂无 ABP vNext 针对用户可编辑的配置,提供了单独的 Volo. ...

  4. [Abp vNext 源码分析] - 3. 依赖注入与拦截器

    一.简要说明 ABP vNext 框架在使用依赖注入服务的时候,是直接使用的微软提供的 Microsoft.Extensions.DependencyInjection 包.这里与原来的 ABP 框架 ...

  5. [Abp vNext 源码分析] - 2. 模块系统的变化

    一.简要说明 本篇文章主要分析 Abp vNext 当中的模块系统,从类型构造层面上来看,Abp vNext 当中不再只是单纯的通过 AbpModuleManager 来管理其他的模块,它现在则是 I ...

  6. [Abp vNext 源码分析] - 1. 框架启动流程分析

    一.简要说明 本篇文章主要剖析与讲解 Abp vNext 在 Web API 项目下的启动流程,让大家了解整个 Abp vNext 框架是如何运作的.总的来说 ,Abp vNext 比起 ABP 框架 ...

  7. [Abp vNext 源码分析] - 4. 工作单元

    一.简要说明 统一工作单元是一个比较重要的基础设施组件,它负责管理整个业务流程当中涉及到的数据库事务,一旦某个环节出现异常自动进行回滚处理. 在 ABP vNext 框架当中,工作单元被独立出来作为一 ...

  8. [Abp vNext 源码分析] - 6. DDD 的应用层支持 (应用服务)

    一.简要介绍 ABP vNext 针对于应用服务层,为我们单独设计了一个模块进行实现,即 Volo.Abp.Ddd.Application 模块. PS:最近博主也是在恶补 DDD 相关的知识,这里推 ...

  9. [Abp vNext 源码分析] - 7. 权限与验证

    一.简要说明 在上篇文章里面,我们在 ApplicationService 当中看到了权限检测代码,通过注入 IAuthorizationService 就可以实现权限检测.不过跳转到源码才发现,这个 ...

随机推荐

  1. Go | Go 语言打包静态文件以及如何与Gin一起使用Go-bindata

    系列文章目录 第一章 Go 语言打包静态文件以及如何与Gin一起使用Go-bindata 目录 系列文章目录 前言 一.go-bindata是什么? 二.使用步骤 1. 安装 2. 使用 3. 读取文 ...

  2. LG P4161 [SCOI2009]游戏/LG P6280 [USACO20OPEN]Exercise G

    Description(P4161) windy学会了一种游戏. 对于1到N这N个数字,都有唯一且不同的1到N的数字与之对应. 最开始windy把数字按顺序1,2,3,……,N写一排在纸上. 然后再在 ...

  3. 【MySQL】面试官问我:MySQL如何实现无数据插入,有数据更新?我是这样回答的!

    写在前面 马上就是金九银十的跳槽黄金期了,很多读者都开始出去面试了.这不,又一名读者出去面试被面试官问了一个MySQL的问题:向MySQL中插入数据,如何实现MySQL中没有当前id标识的数据时插入数 ...

  4. Gradle Wrapper

    Gradle Wrapper 当把本地一个项目放入到远程版本库的时候,如果这个项目是以gradle构建的,那么其他人从远程仓库拉取代码之后如果本地没有安装过gradle会无法编译运行,如果对gradl ...

  5. 从零开始的SpringBoot项目 ( 五 ) 整合 Swagger 实现在线API文档的功能

    综合概述 spring-boot作为当前最为流行的Java web开发脚手架,越来越多的开发者选择用其来构建企业级的RESTFul API接口.这些接口不但会服务于传统的web端(b/s),也会服务于 ...

  6. Javascript常见数据类型API

    1 - 内置对象 1.1 内置对象 ​ JavaScript 中的对象分为3种:自定义对象 .内置对象. 浏览器对象 ​ 前面两种对象是JS 基础 内容,属于 ECMAScript: 第三个浏览器对象 ...

  7. 力扣Leetcode 21. 合并两个有序链表

    合并两个有序链表 将两个升序链表合并为一个新的升序链表并返回.新链表是通过拼接给定的两个链表的所有节点组成的. 示例: 输入:1->2->4, 1->3->4 输出:1-> ...

  8. 深入学习redis 的线程模型

    一.redis 的线程模型 redis 内部使用文件事件处理器 file event handler,它是单线程的,所以redis才叫做单线程模型.它采用IO多路复用机制同时监听多个 socket,将 ...

  9. php高效读取和写入

    /** * 删除非空目录里面所有文件和子目录 * @param string $dir * @return boolean */ function fn_rmdir($dir) { //先删除目录下的 ...

  10. Linux安装doker

    docker安装(centos) 官方文档:https://docs.docker.com/engine/install/centos/ 前提条件 内核系统3.10以上的centos7.可用 unam ...