.NET配置文件解析过程详解
在我看来,WEB project的开发与WINFORM的开发最大的区别在于web的运行是在Framework上更高一层框架上运行,即ASP。NET框架,程序员在web下的开发可以说是黑盒开发,不是让你去定义程序入口和执行顺序,而是asp.net来调用你的各个方法,程序员做的一切都是一种受控的舞蹈。就像我们调用nunit之类的工具来测试一个dll一样,nunit是容器,是框架,执行哪个方法是由nunt来决定的。因此,也就有了页面执行周期各状态等令刚入门的程序员困惑不已的事,其实,究其根源,在于不了解容器而去使用容器。对于asp.net框架的学习,我们不妨从配置文件开始。
对于程序开发者而言,写配置文件是经常性的工作,如果你写了一个xx.config文件,如果没有详尽的注释,别人恐怕很难读懂,没有良好的配置架构,程序也失去了活力。在我看来,.net配置文件的特点在于反射定义和继承性。
我们访问配置文件时经常觉得配置文件的结构不太符合我们的需要,我们需要从里面更方便地获得自己定义的对象,而不仅仅是key和value,对于自定义配置文件的著述已有很多,在此不再描述,有兴趣的朋友可以访问http://ly4cn.cnblogs.com/archive/2005/09/06/231245.html。
自定义配置节其实还是在.net配置文件架构的应用而已,我们先来搞懂配置文件的结构,弄清楚.net配置文件的运行方式。下面是machine.config的一部分内容:
<configSections>
<section name="runtime" type="System.Configuration.IgnoreSectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowLocation="false" />
<sectionGroup name="system.net">
<section name="authenticationModules" type="System.Net.Configuration.NetAuthenticationModuleHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</ sectionGroup>
</configSections>
SDK中<section>的定义为:
<section
name="section name"
type="configuration section handler class, assembly"
allowDefinition="Everywhere|MachineOnly|MachineToApplication"
allowLocation="true|false" />
<sectionGroup>的定义为:
<sectionGroup
name="section group name"/>
</sectionGroup>
我们来看看.net框架内是如何利用这种结构的。反编译System.dll找到GetConfig方法,在里面我们发现实际获取config的工作默认是由实现了IConfigurationSystem的DefaultConfiguationSystem类来实现的。
public static object GetConfig(string sectionName)
{
if (!ConfigurationSettings._configurationInitialized)
{
lock (typeof(ConfigurationSettings))
{
if ((ConfigurationSettings._configSystem == null) && !ConfigurationSettings.SetConfigurationSystemInProgress)
{
ConfigurationSettings.SetConfigurationSystem(new DefaultConfigurationSystem());
}
}
}
if (ConfigurationSettings._initError != null)
{
throw ConfigurationSettings._initError;
}
return ConfigurationSettings._configSystem.GetConfig(sectionName);
}
我们再来看DefaultConfigurationSystem,这个类主要包含了machine.config的名称路径的基本信息和一些uri操作,而实际的GetConfig的操作交给了ConfiguationRecord来处理,这个类没有实现任何接口,可见他和DefaultConfiguration是绑定在一起的。
1 internal class DefaultConfigurationSystem : IConfigurationSystem
2 {
3 // Methods
4 internal DefaultConfigurationSystem();
5 object IConfigurationSystem.GetConfig(string configKey);
6 void IConfigurationSystem.Init();
7
8 // Properties
9 internal static Uri AppConfigPath { get; }
10 internal static string MachineConfigurationFilePath { get; }
11 internal static string MsCorLibDirectory { get; }
12
13 // Fields
14 private ConfigurationRecord _application;
15 private const string ConfigExtension = "config";
16 private const string MachineConfigFilename = "machine.config";
17 private const string MachineConfigSubdirectory = "Config";
18 private const int MaxPathSize = 0x400;
19 }
20
事实上所有的配置文件的分析和获取都是在ConfiguationRecord里实现的,作为配置文件分析的第一步,正如我们经常做的一样->加载一个配置文件,这个方法公开为 Load(filename)。DefaultConfiguationSystem的Init()方法中用machine.config创建了一个ConfiguationRecord对象,并将其作为父对象传递给当前程序的ConfiguationRecord对象,当然前提是当前程序有配置文件,比如myapp.config,然后再加载当前程序的配置文件,从而实现配置文件的继承。
void IConfigurationSystem.Init()
{
lock (this)
{
if (this._application == null)
{
ConfigurationRecord record1 = null;
string text1 = DefaultConfigurationSystem.MachineConfigurationFilePath;
Uri uri1 = DefaultConfigurationSystem.AppConfigPath;
this._application = record1 = new ConfigurationRecord();
bool flag1 = record1.Load(text1);
if (!flag1 || (uri1 == null))
{
return;
}
this._application = new ConfigurationRecord(record1);
this._application.Load(uri1.ToString());
}
}
}
现在我们可以专注于ConfiguationRecord的具体实现了,Load方法中得到一个XmlTextWriter,并执行.ScanFactoriesRecursive和ScanSectionsRecursive方法。
reader1 = ConfigurationRecord.OpenXmlTextReader(filename);
if (reader1 != null)
{
this.ScanFactoriesRecursive(reader1);
if (reader1.Depth == 1)
{
this.ScanSectionsRecursive(reader1, null);
}
return true;
}
ScanFactoriesRecursive方法会调用他的一个重载方法来解析<configSections>中的<sectionGroup>,<section>,<remove>,<clear>,我们写配置文件时大小写可不能写错哦,.NET没有做toslower之类的转换,直接就是 “== “。在这个方法里程序会将解析得到的sectiongroup以key=tagkey,value= ConfigurationRecord.GroupSingleton的方式存到EnsureFactories里,将section以key=tagkey,value=typestring的方式储存,值得注意的是,这里并没有创建实现IConfigurationSectionHandler的实例对象,而是将其类型名(比如:字符串”system.Configuration.NameValueSectionHandler”)作为值到EnsureFactories,等到后面GetConfig的时候再来反射创建。<remove>则存为ConfigurationRecord.RemovedFactorySingleton。<clear>就清空EnsureFactories。这里的tagkey是各级name的组合,比如”mygroup/mysection”这样以分隔符”/”组合的形式。应该客观地说这部分代码用了很多goto语句,可读性不是太好,但这样写也确实没有什么问题。
this.CheckRequiredAttribute(text3, "name", reader);
this.CheckRequiredAttribute(text4, "type", reader);
this.VerifySectionName(text3, reader);
string text5 = ConfigurationRecord.TagKey(configKey, text3);
if (this.HaveFactory(text5) != ConfigurationRecord.HaveFactoryEnum.NotFound)
{
objArray1 = new object[] { text3 } ;
throw this.BuildConfigError(SR.GetString("Tag_name_already_defined", objArray1), reader);
}
this.EnsureFactories[text5] = text4;
goto Label_02B6;
ScanSectionsRecursive方法会解析配置文件里的section实例,并将其tagkey储存到Hashtable _unevaluatedSections中,表示尚未evaluated的configkey的集合,可见section实例对象的创建和handler一样,都是fetch when need。在后面的操作Getconfig中会使用他。
if (this._unevaluatedSections == null)
{
this._unevaluatedSections = new Hashtable();
}
this._unevaluatedSections[text2] = null;
现在我们就可以看Getconfig方法是怎么来执行得到我们想要的对象的。
public object GetConfig(string configKey)
{
if (this._error != null)
{
throw this._error;
}
if (this._results.Contains(configKey))
{
return this._results[configKey];
}
object obj1 = this.ResolveConfig(configKey);
lock (this._results.SyncRoot)
{
this._results[configKey] = obj1;
}
return obj1;
}
如果_result中有对应configkey的section实例,就返回,没有则需要对configkey进行ResolveConfig,将解析到的对象保存到_result中并返回给用户。在ResolveConfig方法中,可以看到如果当前的配置文件中没有要求的configkey就会返回父级的section实例,比如machine.config里的内容。
public object ResolveConfig(string configKey)
{
Hashtable hashtable1 = this._unevaluatedSections;
if ((hashtable1 != null) && hashtable1.Contains(configKey))
{
return this.Evaluate(configKey);
}
if (this._parent != null)
{
return this._parent.GetConfig(configKey);
}
return null;
}
然后就是Evaluate及其后续操作了,主要就是将configkey分解成字符串数组,一层层地去找对应的xmlelement,找到后传给configkey对应的handler,如果该handler没有创建则反射创建,然后由该handler创建section的实例对象,返回给用户,该部分关键代码如下:
ConfigXmlDocument document1 = new ConfigXmlDocument();
document1.LoadSingleElement(this._filename, reader);
config = factory.Create(config, null, document1.DocumentElement);
现在我们就明白了当我们用System..Configurtion.ConfiguationSetting.GetConfig的时候发生过什么了。
在下一篇文章中,我会对系统内置的具体section的handler的实现做详细的阐述。
.NET配置文件解析过程详解(二)
在上一篇文章的基础上,我们继续我们的解析
以machine.config文件内部定义的部分section handler为例,来分析各个配置节点的工厂是如何产生具体程序对象的。
<configuration>下的section handlers
IgnoreSectionHandler
在machine.config的<configSections>中有以下section是不在sectiongroup里的,换句话说这些section对应的xml节点位于配置文件根节点configuration的次级。
<section name="runtime" type="System.Configuration.IgnoreSectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowLocation="false" />
<section name="mscorlib" type="System.Configuration.IgnoreSectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowLocation="false" />
<section name="startup" type="System.Configuration.IgnoreSectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowLocation="false" />
<section name="system.runtime.remoting" type="System.Configuration.IgnoreSectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowLocation="false" />
<section name="appSettings" type="System.Configuration.NameValueFileSectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
在这里面,runtime, remoting等section对应的handler类是IgnoreSectionHandler,难道他们用同一种工厂就可以创造出各自需要的对象?当然不是,看看SDK里对IgnoreSectionHandler的定义:
配置系统会彻底分析配置文件以收集配置信息,并且当遇到在 <configSections> 元素节中无相应项的配置节时将引发异常。IgnoreSectionHandler 为其他系统所读取的节提供节处理程序,以防止配置文件分析异常。
反编译IgnoreSectionHandler类的Create方法的内容为:
public virtual object Create(object parent, object configContext, XmlNode section)
{
return null;
}
因此,我们发现IgnoreSectionHandler真的是什么也不做,只是为了取得对应配置节在配置文件中的合法地位,是不是有点像办个户口,占个位置啊?SDK里所说的其他系统是什么意思呢?既然IgnoreSectionHandler返回的是个null,那其系统是怎么来通过Getconfig来获得需要的配置信息呢?答案是抛弃默认的GetConfig方法,重头再来。我们以remoting为例,看看他怎么来获得配置文件里的信息的。 在命名空间下System.Runtime.Remoting下有个RemotingConfiguration类,以公共静态方法Configure接收一个配置文件名,并将解析过程交给一个handler类 RemotingConfigHandler,这个handler并没有实现IConfigurationSectionHandler接口。RemotingConfigHandler则将实际的事务交给类RemotingXmlConfigFileData,而RemotingXmlConfigFileData将XML文件的解析过程交给RemotingXmlConfigFileParser来处理。
string text2;
RemotingXmlConfigFileData data1 = new RemotingXmlConfigFileData();
ConfigTreeParser parser1 = new ConfigTreeParser();
ConfigNode node1 = parser1.Parse(filename, "/configuration/system.runtime.remoting");
由此可见,remoting部分是自己重新打造了配置文件的解析过程。为什么会这样呢?为什么结构严谨的.net架构下会允许这样一种不统一的操作呢?这是不是一种bad的设计呢?这就要涉及到system.dll和mscorlib.dll两个组件之间的关系了。System.dll引用了mscorlib.dll,也就是说System.dll依赖于组件mscorlib.dll,通用配置框架的外观类ConfigurationSettings处于system.dll的System.Configuration命名空间下,而RemotingConfigHandler位于mscorlib,这样的结构就不能让RemotingConfigHandler实现System.dll里的IConfigurationSectionHandler接口了,既然某些配置程序的处理在ConfigurationSettings之前,那么我们对于“前辈们”的位置,我们必须认为它是合法的,然后在其位置上放一个牌子”occupied” -- IgnoreSectionHandler ,以通过验证。这种设计模式我认为确实值得商榷,以我目前的了解,从模式角度讲,觉得不是很合理。
NameValueSectionHandler
返回健值对集合对象,ConfigurationSettings.AppSettings就是由NameValueSectionHandler返回的ReadOnlyNameValueCollection。为什么是readonly?原因当然不只是配置文件是只读的那么简单,因为在上一篇文章中,我有谈到解析后的对象会存在hashtable中,一旦程序请求同一个tagkey就返回同一对象,也就是解析一次,如果有删除操作,除非重启程序,否则是无法读到完整的原始配置信息的,这就涉及到我们在写自定义配置节及其hadler时要注意的一个地方,如果返回一个hashtable时,如果有删除和修改操作将会在应用程序生命周期内有效。我曾经写过一个项目,从配置文件返回一个hashtable储存的对象集合,为了提高效率,我会将表中某些对象remove掉,结果导致第一次运行一个页面和后面几次运行一个页面效果不一样的情况,害我费了很多精力找出原因。在上一篇文章中讲过配置文件的继承,但究竟在细节上是怎么来实现的呢?我们从section的继承上寻找答案。实现配置信息继承的代码为:
ReadOnlyNameValueCollection collection1;
if (parent == null)
{
collection1 = new ReadOnlyNameValueCollection(new CaseInsensitiveHashCodeProvider(CultureInfo.InvariantCulture), new CaseInsensitiveComparer(CultureInfo.InvariantCulture));
}
else
{
ReadOnlyNameValueCollection collection2 = (ReadOnlyNameValueCollection) parent;
collection1 = new ReadOnlyNameValueCollection(collection2);
}
NameValueSectionHandler对节点的具体分析代码为:
if (node1.Name == "add")
{
string text1 = HandlerBase.RemoveRequiredAttribute(node1, keyAttriuteName);
string text2 = HandlerBase.RemoveRequiredAttribute(node1, valueAttributeName, true);
HandlerBase.CheckForUnrecognizedAttributes(node1);
collection1[text1] = text2;
continue;
}
if (node1.Name == "remove")
{
string text3 = HandlerBase.RemoveRequiredAttribute(node1, keyAttriuteName);
HandlerBase.CheckForUnrecognizedAttributes(node1);
collection1.Remove(text3);
continue;
}
if (node1.Name.Equals("clear"))
{
HandlerBase.CheckForUnrecognizedAttributes(node1);
collection1.Clear();
continue;
}
NameValueFileSectionHandler
NameValueFileSectionHandler在SDK 中并没有相应的文档。我发现有个特点,就是很多人把这个类当成NameValueSectionHandler用,这样用似乎也没出什么问题,难道ms写了两个一样的类?非也。其实,NameValueFileSectionHandler 可以说是NameValueSectionHandler的一个升级版,升就升在 “File”上!
我们经常抱怨配置文件越来越大了,即使是级联格式,找起东西来还是眼花,其实.net配置文件体系给我们提供了一种内置的操作来实现多个配置文件,那就是NameValueFileSectionHandler。NameValueFileSectionHandler也是返回ReadOnlyNameValueCollection。我们来看看具体的实现代码:
object obj1 = parent;
XmlNode node1 = section.Attributes.RemoveNamedItem("file");
obj1 = NameValueSectionHandler.CreateStatic(obj1, section);
if ((node1 == null) || (node1.Value.Length == 0))
{
return obj1;
}
如果该section没有file这个属性,那么其处理方式就跟NameValueSectionHandler一样,NameValueFileSectionHandler调用NameValueSectionHandler的解析过程来取得对象。如果有file这个属性NameValueFileSectionHandler就去当前目录下或该文件的绝对路径去找到这个文件:
string text3 = Path.GetDirectoryName(node2.Filename);
string text4 = Path.Combine(text3, text1);
对找到的文件,当然是xml解析了,取根节点作为section分析节点。
ConfigXmlDocument document1 = new ConfigXmlDocument();
try
{
document1.Load(text4);
}
然后将新去到的xml节点当成section节点一样用NameValueSectionHandler解析。这样就把要写在配置文件中的name/value集合放到另外一个xml文件里去了,这样就实现了配置文件的分割,这在配置文件有很多键值对的系统中应该是比较有用的。
return NameValueSectionHandler.CreateStatic(obj1, document1.DocumentElement);
DictionarySectionHandler
NameValueSectionHandler和DictionarySectionHandler在定义配置文件的内容形式上是一样的,都是用<key><value>来设置内容的。不同的是DictionarySectionHandler比较封闭,不具有弹性,不像NameValueSectionHandler还提供给fieldhandler调用,”key”,”value”这些字符也是在属性里写死的,而且返回到C#中的类不太一样,DictionarySectionHandler返回的是一个hashtable,但还是字符串的键值对。
SingleTagSectionHandler
最简单的就是他了,就是把属性的键值对作为hashtable的键值对返回。
foreach (XmlAttribute attribute1 in section.Attributes)
{
hashtable1[attribute1.Name] = attribute1.Value;
}
return hashtable1;
在后面的文章中,我将深入<sectionGroup name="system.web">进行研究和分析。