在應用程序運行的時,需要根據不同的配置執行不同的內容。有很多根據配置而初始化的功能往往是在應用程序啟動的時候需要執行。對於很多類型的應用程序,特別是客戶端的應用程序,啟動的性能特別重要。也因此,在啟動過程中需要依賴配置文件的不同配置而啟動不同的功能時,就對配置文件的讀寫和解析性能提出了很高的要求
本文來和大家簡單介紹我團隊開源的 dotnetCampus.Configurations 高性能配置文件讀寫庫。這個庫不僅包含了配置文件的讀取解析,還包括了自定義配置文件格式,也就是 COIN 硬幣格式的配置文件。提供了多線程和多進程的讀寫安全的功能和毫秒級的配置文件讀取解析性能,以及最低支持到 .NET Framework 4.5 框架
背景
我有很多個客戶端 .NET 應用程序,我需要在客戶端啟動的過程中,讀取一些配置文件,包括機器級配置和用戶級配置。原本一開始我的應用程序都是采用先啟動通用邏輯,將通用界面顯示出來,接着慢慢去讀取配置文件,根據配置文件展開不同的功能
后面產品變心了,加了一些有趣的功能。如換膚等功能,此時就需要在第一個界面顯示出來之前就需要讀取配置了。我寫了另一篇博客 C# 配置文件存儲 各種序列化算法性能比較 告訴大家各個配置文件的讀取性能和序列化解析性能
但是現在通用的 XML 或 JSON 或 INI 等格式的性能,盡管看起來足夠快了,但放在啟動過程這個業務里面,依然顯得性能不夠。在啟動的流程,每一個毫秒都是非常重要的。於是我所在的 CBB 公共組件團隊就對配置文件的讀取和解析有了性能上的要求,在基准測試機器人,能夠在 10 毫秒內完全讀取完成一份基准的配置文件。然而對於通用如上幾個格式的文件來說,幾乎沒有一個能在小於 90 毫秒內完成。這就使得我需要去尋找一個更快的配置文件讀寫方式
在后續的產品迭代中,有幾個產品的應用是允許用戶多開的,開啟多個進程的時候,也需要進行讀寫相同的一個配置文件。此時就出現另一個問題,如何保證配置的讀寫是進程級安全的
綜合考慮了之后,在太子的帶領下,開發和開源了 dotnetCampus.Configurations 硬幣格式的高性能配置文件讀寫庫
為什么叫硬幣 COIN 呢,原因是取自 COIN = Configuration\n
即“配置+換行符”,因默認使用“\n”作為換行符而得名
開源
這是基於最友好的 MIT 協議的在 GitHub 完全開源的倉庫,請看 https://github.com/dotnet-campus/dotnetCampus.Configurations
此配置文件庫完全百分百使用 C# 編寫,支持如下 .NET 框架
- netstandard2.0
- net45
- netcoreapp3.0
等等 .NET 5 和 .NET 6 呢?在 .NET 5 或更高版本將會自動使用 .NET Core 3.0 的庫,放心,這是完全 IL 級兼容的。為什么要有 .NET Standard 2.0 的? 因為還要給 Xamarin 做兼容哦。對於 .NET Framework 系列的,最低要求是 .NET Framework 4.5 版本,對於更高的 .NET Framework 版本,也將會自動引用 .NET Framework 4.5 版本,放心,這也是完全 IL 級兼容的
本庫已在超過 500 萬台設備上穩定運行超過一年時間,還請放心使用
使用方法
介紹了那么多,是時候來看看此配置文件庫的使用方法
按照慣例,在使用 .NET 庫只需要兩步,第一是通過 NuGet 安裝,第二是開始使用。本文的硬幣格式的高性能配置庫也是通過 NuGet 分發的,包含了兩個分支版本,分別是傳統的 DLL 版本的 NuGet 和源代碼兩個版本。為了方便起見,咱先來介紹傳統的 DLL 版本的使用方法
右擊項目管理 NuGet 程序包,在瀏覽里面搜尋 dotnetCampus.Configurations 進行安裝
在命令行使用如下代碼即可給項目安裝上硬幣格式的高性能配置文件讀寫庫
dotnet add package dotnetCampus.Configurations
除了使用命令行安裝之外,對於 SDK 風格的新 csproj 項目格式的項目,可以編輯 csproj 文件,在 csproj 文件上添加如下代碼進行安裝
<PackageReference Include="dotnetCampus.Configurations" Version="1.6.8" />
使用硬幣格式的高性能配置文件讀寫庫時,需要傳入配置文件所在的路徑,如以下代碼
// 使用一個文件路徑創建默認配置的實例。文件可以存在也可以不存在,甚至其所在的文件夾也可以不需要提前存在。
// 這里的配置文件后綴名 coin 是 Configuration\n,即 “配置+換行符” 的簡稱。你也可以使用其他擴展名,因為它實際上只是 UTF-8 編碼的純文本而已。
var configs = DefaultConfiguration.FromFile(@"C:\Users\lvyi\Desktop\walterlv.coin");
在獲取到 configs 變量之后,即可對此變量進行讀寫,如下面代碼
獲取值:
// 獲取配置 Foo 的字符串值。
// 這里的 value 一定不會為 null,如果文件不存在或者沒有對應的配置項,那么為空字符串。
string value0 = configs["Foo"];
// 獲取字符串值的時候,如果文件不存在或者沒有對應的配置項,那么會使用默認值(空傳遞運算符 ?? 可以用來指定默認值)。
string value1 = configs["Foo"] ?? "anonymous";
設置值:
// 設置配置 Foo 的字符串值。
configs["Foo"] = "lvyi";
// 可以設置為 null,但你下次再次獲取值的時候卻依然保證不會返回 null 字符串。
configs["Foo"] = null;
// 可以設置為空字符串,效果與設置為 null 是等同的。
configs["Foo"] = "";
在大型項目中使用
實際應用中,應該將 configs 緩存起來,而不是每次使用的時候,都通過 DefaultConfiguration.FromFile 去創建新的對象
初始化:
// 這里是大型項目配置初始化處的代碼。
// 此類型中包含底層的配置讀寫方法,而且所有讀寫全部是異步的,防止影響啟動性能。
var configFileName = @"C:\Users\lvyi\Desktop\walterlv.coin";
var config = ConfigurationFactory.FromFile(configFileName);
// 如果你需要對整個應用程序公開配置,那么可以公開 CreateAppConfigurator 方法返回的新實例。
// 這個實例的所有配置讀寫全部是同步方法,這是為了方便其他模塊使用。
// 以下是 Container 即是容器,放入到容器中相當於全局單例
Container.Set<IAppConfigurator>(config.CreateAppConfigurator());
在業務模塊中定義類型安全的配置類:
internal class StateConfiguration : Configuration
{
/// <summary>
/// 獲取或設置整型。
/// </summary>
internal int? Count
{
get => GetInt32();
set => SetValue(value);
}
/// <summary>
/// 獲取或設置帶默認值的整型。
/// </summary>
internal int Length
{
get => GetInt32() ?? 2;
set => SetValue(Equals(value, 2) ? null : value);
}
/// <summary>
/// 獲取或設置布爾值。
/// </summary>
internal bool? State
{
get => GetBoolean();
set => SetValue(value);
}
/// <summary>
/// 獲取或設置字符串。
/// </summary>
internal string Value
{
get => GetString();
set => SetValue(value);
}
/// <summary>
/// 獲取或設置帶默認值的字符串。
/// </summary>
internal string Host
{
get => GetString() ?? "https://localhost:17134";
set => SetValue(Equals(value, "https://localhost:17134") ? null : value);
}
/// <summary>
/// 獲取或設置非基元值類型。
/// </summary>
internal Rect? Screen
{
get => this.GetValue<Rect>();
set => this.SetValue<Rect>(value);
}
}
在業務模塊中使用:
private readonly IAppConfiguration _config = Container.Get<IAppConfigurator>(); // 從 Container 容器獲取,相當於從單例獲取對象
// 讀取配置。
private void Restore()
{
var config = _config.Of<StateConfiguration>();
var bounds = config.Screen;
if (bounds != null)
{
// 恢復窗口位置和尺寸。
}
}
// 寫入配置。
public void Update()
{
var config = _config.Of<StateConfiguration>();
config.Screen = new Rect(0, 0, 3840, 2160);
}
兼容 Microsoft.Extensions.Configuration 配置
默認在 ASP.NET Core 里面將使用 Microsoft.Extensions.Configuration 配置,硬幣格式的高性能配置文件讀寫庫於此也是兼容的。但需要額外再安裝一個兼容層的 NuGet 庫,通過右擊項目管理 NuGet 程序包,在瀏覽里面搜尋 dotnetCampus.Configurations.MicrosoftExtensionsConfiguration 進行安裝
安裝完成之后,可以通過擴展方法 ToAppConfigurator 從 IConfigurationBuilder 或者 IConfiguration 創建 IAppConfigurator 對象,通過 IAppConfigurator 即可重新接入到硬幣格式的高性能配置文件讀寫庫
public void Foo(IConfigurationBuilder builder)
{
IAppConfigurator appConfigurator = builder.ToAppConfigurator();
// 完成接入
}
public void Foo(IConfiguration configuration)
{
IAppConfigurator appConfigurator = configuration.ToAppConfigurator();
// 完成接入
}
如在拿到 appConfigurator 變量之后,即可和上文一樣的代碼訪問配置
private void Foo(IConfiguration configuration)
{
IAppConfigurator appConfigurator = configuration.ToAppConfigurator();
var config = appConfigurator.Of<StateConfiguration>();
// 讀取配置。
var bounds = config.Screen;
if (bounds != null)
{
// 恢復窗口位置和尺寸。
}
// 寫入配置。
config.Screen = new Rect(0, 0, 3840, 2160);
}
配置文件格式
配置文件讀寫庫的性能,除了代碼層面的影響,更重要的是配置文件格式的影響。為了做到盡可能的高性能,於是重新設置了一套配置文件格式,這就是 COIN 硬幣配置文件格式
配置格式如下
配置文件以行為單位,將行首是 >
字符的行作為注釋,在 >
后面的內容將會被忽略。在第一個非 >
字符開頭的行作為 Key
值,在此行以下直到文件結束或下一個 >
字符開始的行之間內容作為 Value
值
> 配置文件
> 版本 1.0
State.BuildLogFile
xxxxx
> 注釋內容
Foo
這是第一行
這是第二行
>
> 配置文件結束
此配置文件格式不支持樹型結構,而是 Key-Value 方式。作為配置文件是足夠的,但是作為存儲文件格式卻是不適合的,這就是和 XML 和 JSON 最大的差別
特性
- 高性能讀寫
- 在初始化階段使用全異步處理,避免阻塞主流程。
- 使用特別為高性能讀寫而設計的配置文件格式。
- 多線程和多進程安全高性能讀寫
- 無異常設計
- 所有配置項的讀寫均為“無異常設計”,你完全不需要在業務代碼中處理任何異常。
- 為防止業務代碼中出現意料之外的
NullReferenceException
,所有配置項的返回值均不為實際意義的null
。- 值類型會返回其對應的
Nullable<T>
類型,這是一個結構體,雖然有null
值,但不會產生空引用。 - 引用類型僅提供字符串,返回
Nullable<ConfigurationString>
類型,這也是一個結構體,你可以判斷null
,但實際上不可能為null
。
- 值類型會返回其對應的
- 全應用程序統一的 API
- 在大型應用中開放 API 時記得使用
CreateAppConfigurator()
來開放,這會讓整個應用程序使用統一的一套配置讀寫 API,且完全的 IO 無感知。
- 在大型應用中開放 API 時記得使用
本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名林德熙(包含鏈接:http://blog.csdn.net/lindexi_gd ),不得用於商業目的,基於本文修改后的作品務必以相同的許可發布。如有任何疑問,請與我聯系。