深入了解三种针对文件(JSON、XML与INI)的配置源

物理文件是我们最常用到的原始配置的载体,最佳的配置文件格式主要由三种,它们分别是JSON、XML和INI,对应的配置源类型分别是JsonConfigurationSource、XmlConfigurationSource和IniConfigurationSource。 [ 本文已经同步到《ASP.NET Core框架揭秘》之中]

目录
一、FileConfigurationSource  & FileConfigurationProvider 
二、JsonConfigurationSource &JsonConfigurationProvider
三、XmlConfiguationSource & XmlConfiguationProvider
四、IniConfigurationSource & IniConfigurationSource

一、FileConfigurationSource  & FileConfigurationProvider

上述这三个具体的ConfigurationSource类型具有如下一个相同的基类FileConfigurationSource。

   1: public abstract class FileConfigurationSource : IConfigurationSource
   2: {
   3:     public IFileProvider     FileProvider { get; set; }
   4:     public bool              Optional { get; set; }
   5:     public string            Path { get; set; }
   6:     public bool              ReloadOnChange { get; set; }
   7:  
   8:     public abstract IConfigurationProvider Build(IConfigurationBuilder builder);
   9: }

如上面的代码片段所示,FileConfigurationSource是一个抽象类,它利用一个FileProvider对象来提供原始的配置文件,而Path属性自然代表配置文件的路径。Optional属性表示明当前的FileConfigurationSource是否是可选的配置源,其默认值为False。当某个FileConfigurationSource的Optional属性为True的时候,如果指定的配置文件路径不存在,将不会有任何异常被抛出来。由于FileProvider具有监控文件变化的能力,它的ReloadOnChange属性表示如果被监控的配置文件发生改变后是否需要重新加载配置。

由于FileConfigurationSource总是利用FileProvider来读取配置文件的内容,所以当我们创建一个具体的FileConfigurationSource对象的时候都需要采用显式或者隐式的方式指定一个FileProvider对象(关于FileProvider,可以参阅我的“文件系统”博文系列)。我们可以调用扩展方法SetFileProvider将一个默认的FileProvider注册到ConfigurationBuilder对象上,从相面的代码片段可以看出注册的FileProvider被保存到Properties属性表示的字典对象上,对应的Key为“FileProvider”。

   1: public static class FileConfigurationExtensions
   2: {
   3:     private static string FileProviderKey = "FileProvider";
   4:  
   5:     public static IFileProvider GetFileProvider(this IConfigurationBuilder builder)
   6:     {
   7:         object obj2;       
   8:         if (builder.Properties.TryGetValue(FileProviderKey, out obj2))
   9:         {
  10:             return (builder.Properties[FileProviderKey] as IFileProvider);
  11:         }
  12:         return new PhysicalFileProvider(AppContext.BaseDirectory ?? string.Empty);
  13:     }
  14:  
  15:     public static IConfigurationBuilder SetBasePath(this IConfigurationBuilder builder, string basePath)
  16:     {       
  17:         return builder.SetFileProvider(new PhysicalFileProvider(basePath));
  18:     }
  19:  
  20:     public static IConfigurationBuilder SetFileProvider(this IConfigurationBuilder builder, IFileProvider fileProvider)
  21:     {        
  22:         builder.Properties[FileProviderKey] = fileProvider;
  23:         return builder;
  24:     }
  25: }

通过隐式方式提供的FileProvider通过调用ConfigurationBuilder的另一个扩展方法GetFileProvider方法获取。从上面给出的代码片段我们可以看到,它会优先返回我们注册的FileProvider。如果这样的FileProvdier尚未注册,该方法会返回指向当前应用执行目录的PhysicalFileProvider对象。除了上述这两个方法,ConfigurationBuilder还具有另一个名为SetBasePath的方法,该方法采用指定的路径创建一个PhysicalFileProvider对象并对它进行注册。

虽然JsonConfigurationSource、XmlConfigurationSource和IniConfigurationSource这些针对通过文件类型的ConfigurationSource会提供不同类型的ConfigurationProvider来读取对应的配置文件并将读取的内容转换成一个配置字典,但是这些ConfigurationProvider都派生与如下一个FileConfigurationProvider类型。FileConfigurationSource和FileConfigurationProvider都定义在“Microsoft.Extensions.Configuration
.FileExtensions”这个NuGet包中。

   1: public abstract class FileConfigurationProvider : ConfigurationProvider
   2: {
   3:  
   4:     public FileConfigurationSource Source { get; private set; }
   5:  
   6:     public FileConfigurationProvider(FileConfigurationSource source)
   7:     {
   8:         this.Source = source;
   9:         if (this.Source.ReloadOnChange)
  10:         {
  11:             ChangeToken.OnChange(
  12:                 () => this.Source.FileProvider.Watch(this.Source.Path), 
  13:                 this.Load);
  14:         }
  15:     }
  16:  
  17:     public override void Load()
  18:     {
  19:         IFileInfo fileInfo = this.Source.FileProvider.GetFileInfo(this.Source.Path);
  20:  
  21:         if ((fileInfo == null) || !fileInfo.Exists)
  22:         {
  23:             if (!this.Source.Optional)
  24:             {
  25:                 throw new FileNotFoundException();
  26:             }
  27:             base.Data = new Dictionary<string, string>(
  28:                 StringComparer.OrdinalIgnoreCase);
  29:         }
  30:         else
  31:         {
  32:             using (Stream stream = fileInfo.CreateReadStream())
  33:             {
  34:                 this.Load(stream);
  35:             }
  36:         }
  37:         base.OnReload();
  38:     }
  39:  
  40:     public abstract void Load(Stream stream);
  41: }

我们通过如上所示的代码片段以简化的形式模拟了FileConfigurationProvider的实现逻辑。它定义了一个抽象方法Load来完成针对配置文件的读取和配置字典的生成,该参数代表读取文件的输出流。在重写的Load方法中,它直接利用FileProvider得到描述配置文件的FileInfo对象,并调用此FileInfo对象的CreateReadStream方法得到这个Stream对象。

上面的这个代码片段还体现了额外一些细节。首先,如果我们将FileConfigurationSource的ReloadOnChange属性设置为True,意味着我们希望当配置文件发生该表的时候重新加载该文件。FileConfigurationProvider直接利用FileProvider的Watch方法监视配置文件的变换,并将Load方法注册为回调从而到达配置数据同步的目的。其次,如果指定的配置文件不存在,并且FileConfigurationSource的Optional属性被设置为True,FileConfigurationProvider是不能抛出FileNotFoundException异常的。

二、JsonConfigurationSource&JsonConfigurationProvider

JsonConfigurationSource代表针对通过JSON文件定义的配置源,该类型定义在NuGet包“Microsoft.Extensions.Configuration.Json”中。如下面的代码片段所示,在重写的Build方法中,如果FileProvider属性没有被显式赋值,它会调用ConfigurationBuilder的扩展方法GetFileProvider得到一个FileProvdier并对该属性赋值。Build方法最终创建并返回的是一个根据自己创建的JsonConfigurationProvider对象。作为FileConfigurationProvider的继承者,JsonConfigurationProvider利用重写的Load方法读取配置文件的内容并将其转换成配置字典。

   1: public class JsonConfigurationSource : FileConfigurationSource
   2: {
   3:     public override IConfigurationProvider Build(IConfigurationBuilder builder)
   4:     {
   5:         base.FileProvider = base.FileProvider ?? builder.GetFileProvider();
   6:         return new JsonConfigurationProvider(this);
   7:     }
   8: }
   9:  
  10: public class JsonConfigurationProvider : FileConfigurationProvider
  11: {
  12:     public JsonConfigurationProvider(JsonConfigurationSource source);
  13:     public override void Load(Stream stream);
  14: }

“Microsoft.Extensions.Configuration.Json”这个NuGet包为我们定义了如下所示的一系列针对IConfigurationBuilder接口的扩展方法AddJsonFile来完成针对JsonConfigurationSource的注册。如果调用第一个AddJsonFile方法重载,我们可以利用指定的Action<JsonConfigurationSource>对象对创建的JsonConfigurationSource进行初始化。至于后续的AddJsonFile方法重载,实际上就是通过相应的参数初始化JsonConfigurationSource的Path、Optional和ReloadOnChange属性罢了。

   1: public static class JsonConfigurationExtensions
   2: {
   3:     public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, Action<JsonConfigurationSource> configureSource);
   4:     public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string path);
   5:     public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string path, bool optional);
   6:     public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string path, bool optional,bool reloadOnChange);
   7: }

当使用JSON文件来定义配置的时候,我们会发现不论对于何种数据结构(复杂对象、集合、数组和字典),我们都能通过JSON格式以一种简单而自然的方式来定义它们。同样以前面定义的Profile类型为例,我们可以利用如下所示的三个JSON文件分别定义一个完整的Profile对象、一个Profile对象的集合以及一个Key和Value类型分别为字符串和Profile的字典。

Profile类型

   1: public class Profile
   2: {
   3:     public Gender          Gender { get; set; }
   4:     public int             Age { get; set; }
   5:     public ContactInfo     ContactInfo { get; set; }
   6: }
   7:  
   8: public class ContactInfo
   9: {
  10:     public string EmailAddress { get; set; }
  11:     public string PhoneNo { get; set; }
  12: }
  13:  
  14: public enum Gender
  15: {
  16:     Male,
  17:     Female
  18: }

Profile对象:

   1: {
   2:   "profile": {
   3:     "gender"      : "Male",
   4:     "age"         : "18",
   5:     "contactInfo": {
   6:       "email"            : "foobar@outlook.com",
   7:       "phoneNo"          : "123456789"
   8:     }
   9:   }
  10: }

Profile集合或者数组

   1: {
   2:   "profiles": [
   3:     {
   4:       "gender"     : "Male",
   5:       "age"        : "18",
   6:       "contactInfo": {
   7:         "email"       : "foo@outlook.com",
   8:         "phoneNo"     : "123"
   9:       }
  10:     },
  11:     {
  12:       "gender"    : "Male",
  13:       "age"       : "25",
  14:       "contactInfo": {
  15:         "email"     : "bar@outlook.com",
  16:         "phoneNo"      : "456"
  17:       }
  18:     },
  19:     {
  20:       "gender"    : "Female",
  21:       "age"       : "40",
  22:       "contactInfo": {
  23:         "email"        : "baz@outlook.com",
  24:         "phoneNo"      : "789"
  25:       }
  26:     }
  27:   ]
  28: }

Profile字典

   1: {
   2:   "profiles": {
   3:     "foo": {
   4:       "gender"     : "Male",
   5:       "age"        : "18",
   6:       "contactInfo": {
   7:         "email"     : "foo@outlook.com",
   8:         "phoneNo"   : "123"
   9:       }
  10:     },
  11:     "bar": {
  12:       "gender"     : "Male",
  13:       "age"        : "25",
  14:       "contactInfo": {
  15:         "email"        : "bar@outlook.com",
  16:         "phoneNo"      : "456"
  17:       }
  18:     },
  19:     "baz": {
  20:       "gender": "Female",
  21:       "age"   : "40",
  22:       "contactInfo": {
  23:         "email"      : "baz@outlook.com",
  24:         "phoneNo"    : "789"
  25:       }
  26:     }
  27:   }
  28: }

三、XmlConfiguationSource & XmlConfiguationProvider

XML也是一种常用的配置定义形式,它对数据的表达能力甚至强于JSON,基于所有类型的数据结构都可以通过XML表示出来。当我们通过一个XML元素表示一个复杂对象的时候,对象的数据成员定义成当前XML元素的子元素。如果数据成员是一个简单数据类型,我们还可以选择将其定义成当前XML元素的属性(Attribute)。针对一个Profile对象,我们可以采用如下两种不同的形式来定义。

   1: <Profile>
   2:   <Gender>Male</Gender>
   3:   <Age>18</Age>
   4:   <ContactInfo>
   5:     <EmailAddress >foobar@outlook.com</Email>
   6:     <PhoneNo>123456789</PhoneNo>
   7:   </ContactInfo>
   8: </Profile>

或者

   1: <Profile Gender="Male" Age="18">
   2:   <ContactInfo EmailAddress ="foobar@outlook.com" PhoneNo="123456789"/>
   3: </Profile>

虽然XML对数据结构的表达能力总体要强于JSON,但是作为配置模型的数据来源却有自己的局限性,比如它们对集合的表现形式有点不尽如人意。举个简单的例子,对于一个元素类型为Profile的集合,我们可以采用具有如下结构的XML来表现。

   1: <Profiles>
   2:   <Profile Gender="Male" Age="18">
   3:     <ContactInfo EmailAddress ="foobar@outlook.com" PhoneNo="123"/>
   4:   </Profile>
   5:   <Profile Gender="Male" Age="25">
   6:     <ContactInfo EmailAddress ="bar@outlook.com" PhoneNo="456"/>
   7:   </Profile>
   8:   <Profile Gender="Male" Age="40">
   9:     <ContactInfo EmailAddress ="baz@outlook.com" PhoneNo="789"/>
  10:   </Profile>
  11: </Profiles>

但是这段XML却不能正确地转换成配置字典,原因很简单,因为字典的Key必须是唯一的,这必然要求最终构成配置树的每个节点必须具有不同的路径。上面这段XML很明显不满足这个基本的要求,因为表示一个Profile对象的三个XML元素(<Profile>...</Profile>)是“同质”的,对于由它们表示的三个Profile对象来说,分别表示性别、年龄、电子邮箱地址和电话号码的四个叶子节点的路径是完全一样的,所以根据无法作为配置字典的Key。通过前面针对配置绑定的介绍我们知道,如果需要通过配置字典来表示一个Profile对象的集合,我们需要按照如下的方式为每个集合元素加上相应的索引(“foo”、“bar”和“baz”)。

   1: foo:Gender
   2: foo:Age
   3: foo:ContactInfo:EmailAddress
   4: foo:ContactInfo:PhoneNo
   5:  
   6: bar:Gender
   7: bar:Age
   8: bar:ContactInfo:EmailAddress
   9: bar:ContactInfo:PhoneNo
  10:  
  11: baz:Gender
  12: baz:Age
  13: baz:ContactInfo:EmailAddress
  14: baz:ContactInfo:PhoneNo

按照这样的结构,如果我们需要以XML的方式来表示一个Profile对象的集合,就不得不采用如下的结构,即采用索引来命名集合元素对应的XML元素。当时这样的定义方式从语义的角度来讲是不合理的,因为同一个集合的所有元素应该是“同质”的,同质的XML元素采用不同的名称有点说不过去。根据配置绑定的规则,这样的结构同样可以表示一个由三个元素组成的Dictionary<string, Profile>对象,Key分别是“Foo”、“Bar”和“Baz”。如果用这样的XML来表示一个字典对象,语义上就完全没有问题了。

   1: <Profiles>
   2:   <Foo Gender="Male" Age="18">
   3:     <ContactInfo EmailAddress ="foobar@outlook.com" PhoneNo="123"/>
   4:   </Foo>
   5:   <Bar Gender="Male" Age="25">
   6:     <ContactInfo EmailAddress ="foobar@outlook.com" PhoneNo="123"/>
   7:   </Bar>
   8:   <Baz Gender="Male" Age="18">
   9:     <ContactInfo EmailAddress ="baz@outlook.com" PhoneNo="789"/>
  10:   </Baz>
  11: </Profiles>

针对XML文件的配置源类型为XmlConfigurationSource,该类型定义在“Microsoft.Extensions.Configuration.Xml”这个NuGet包中。如下面的代码片段所示,XmlConfigurationSource通过重写的Build方法创建了一个XmlConfigurationProvider对象。作为抽象类型FileConfigurationProvider的继承者,XmlConfigurationProvider通过重写的Load方法完成了针对XML文件的读取和配置字典的初始化。

   1: public class XmlConfigurationSource : FileConfigurationSource
   2: {
   3:     public override IConfigurationProvider Build(IConfigurationBuilder builder)
   4:     {
   5:         base.FileProvider = base.FileProvider ?? builder.GetFileProvider();
   6:         return new XmlConfigurationProvider(this);
   7:     }
   8: }
   9:  
  10: public class XmlConfigurationProvider : FileConfigurationProvider
  11: {   
  12:     public XmlConfigurationProvider(XmlConfigurationSource source);   
  13:     public override void Load(Stream stream);
  14: }

JsonConfigurationSource的注册可以通过调用针对IConfigurationBuilder的扩展方法AddJsonFile来完成。与之类似,“Microsoft.Extensions.Configuration.Xml”这个NuGet包中同样提供了如下一系列名为AddXmlFile的扩展方法重载来根据指定的XML文件创建相应的XmlConfigurationSource并注册到指定的ConfigurationBuilder对象上。AddXmlFile和AddJsonFile方法具有完全一样的声明。

   1: public static class XmlConfigurationExtensions
   2: {
   3:     public static IConfigurationBuilder AddXmlFile(this IConfigurationBuilder builder, string path);
   4:     public static IConfigurationBuilder AddXmlFile(this IConfigurationBuilder builder, string path, bool optional);
   5:     public static IConfigurationBuilder AddXmlFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange);
   6:     public static IConfigurationBuilder AddXmlFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange);
   7: }

四、IniConfigurationSource & IniConfigurationSource

“INI”是“Initialization”的缩写,INI文件又被称为初始化文件,它是Windows系统普遍使用的配置文件,同时也被一些Linux和Unix系统所支持。INI文件直接以键值对的形式定义配置项,如下所示的代码片段体现了INI文件的基本格式。总的来说,INI文件以单纯的“{Key}={Value}”的形式定义配置项,{Value}可以定义在可选的双引号中(如果值的前后包括空白字符,必须使用双引号,否则会被忽略)。

   1: [Section]
   2: key1=value1
   3: key2 = " value2 "
   4: ; comment
   5: # comment
   6: / comment

除了以“{Key}={Value}”的定义的原子配置项外,我们还可以采用“[{SectionName}]”的形式定义配置节对它们进行分组。中括号(“[]”)同时作为下一个的配置节开始的标志,同时也作为上一个配置结束的标志,所以采用INI文件定义的配置节并不存在层次化的结构,即没有“子配置节”的概念。除此之外,我们可以在INI中定义相应的注释,注释行前置的字符可以采用“;”、“#”或者“/”。

由于INI文件自身就体现为一个数据字典,所以我们可以采用“路径化”的Key来定义最终绑定为复杂对象、集合或者字典的配置数据。如果采用INI文件来定义一个Profile对象的基本信息,我们就可以采用如下定义形式。

   1: Gender                         = "Male"
   2: Age                            = "18"
   3: ContactInfo:EmailAddress       = "foobar@outlook.com"
   4: ContactInfo:PhoneNo            = "123456789"

由于Profile的配置信息具有两个层次(Profile>ContactInfo),我们可以按照如下的形式将EmailAddress和PhoneNo定义在配置节“ContactInfo”中,这个INI文件和上面是完全等效的。

   1: Gender  = "Male"
   2: Age     = "18"
   3:  
   4: [ContactInfo]
   5: EmailAddress = "foobar@outlook.com"
   6: PhoneNo      =  "123456789"

针对INI文件类型的配置源类型通过如下所示的IniConfigurationSource来表示,该类型定义在“Microsoft.Extensions.Configuration.Ini”这个NuGet包中。IniConfigurationSource在重写的Build方法中会创建的ConfigurationProvdier类型为IniConfigurationProvider。作为抽象类FileConfigurationProvider的继承者,IniConfigurationProvider利用重写的Load方法完成INI文件内容的读取和配置字典的初始化。

   1: public class IniConfigurationSource : FileConfigurationSource
   2: {
   3:     public override IConfigurationProvider Build(IConfigurationBuilder builder)
   4:     {
   5:         base.FileProvider = base.FileProvider ?? builder.GetFileProvider();
   6:         return new IniConfigurationProvider(this);
   7:     }
   8:  }
   9:  
  10: public class IniConfigurationProvider : FileConfigurationProvider
  11: {
  12:     public IniConfigurationProvider(IniConfigurationSource source);
  13:     public override void Load(Stream stream);
  14: }

既然JsonConfigurationSource和XmlConfigurationSource的注册可以通过调用IConfigurationBuilder接口的扩展方法AddJsonFile和AddXmlFile来完成,“Microsoft.Extensions.Configuration.Ini”这个NuGet包自然会也会为IniConfigurationSource定义如下所示的AddIniFile扩展方法。

   1: public static class IniConfigurationExtensions
   2: {
   3:     public static IConfigurationBuilder AddIniFile(this IConfigurationBuilder builder, string path);
   4:     public static IConfigurationBuilder AddIniFile(this IConfigurationBuilder builder, string path, bool optional);
   5:     public static IConfigurationBuilder AddIniFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange);
   6:     public static IConfigurationBuilder AddIniFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange);
   7: }
作者:蒋金楠 
微信公众账号:大内老A
微博:www.weibo.com/artech

最新文章

  1. codevs 1531 山峰
  2. 双系统如何删除Linux,恢复Windows从MBR引导启动?
  3. css3之currentColor
  4. mongodb(4查询)
  5. 系统监控、诊断工具之top
  6. 使用 windows 计划任务播放音乐文件
  7. read by other session
  8. html中的空格可以用什么代替
  9. Python基础教程学习(四)类的创建与继承
  10. 使用SSIS对Dynamics CRM 系统进行数据迁移
  11. 开发一个基于 Android系统车载智能APP
  12. 使用 Swoole 来加速你的 Laravel 应用
  13. 论JVM爆炸的几种姿势及自救方法
  14. PGSQL-通过SQL语句来计算两个日期相差的天数
  15. How-to: Do Real-Time Log Analytics with Apache Kafka, Cloudera Search, and Hue
  16. javascript中call()、apply()的区别
  17. Docker Swarm 环境搭建
  18. YAML学习
  19. 【翻译】TCP backlog在Linux中的工作原理
  20. cocos2d JS 设置字幕循环滚动(背景图滚动亦可)

热门文章

  1. POJ 2152 Fire(树形DP)
  2. QMessageBox 中的 OK 按钮改为中文“确定”
  3. 开发反模式(GUID) - 伪键洁癖
  4. 设置EntityFramework 在开发时自动更新数据库
  5. 【转】用户空间使用i2c_dev--不错
  6. 【hihocoder 1039 字符串消除】模拟
  7. poj 2342 Anniversary party_经典树状dp
  8. [转]Google2012.9.24校园招聘会笔试题
  9. iOS 推送证书制作 (JAVA/PHP)
  10. 再一次强调,ORACLE外键必须加索引