zoukankan      html  css  js  c++  java
  • .Net 自定义应用程序配置

    .Net 自定义应用程序配置

    引言

    几乎所有的应用程序都离不开配置,有时候我们会将配置信息存在数据库中(例如大家可能常会见到名为Config这样的表);更多时候,我们会将配置写在Web.config或者App.Config中。通过将参数写在配置文件(表)中,我们的程序将变得更加灵活,只要对参数进行修改,再由程序中的某段代码去读取相应的值就可以了。而如果直接将配置值写在程序中,当配置需要改变时,则只能通过修改代码来完成,此时往往需要重新编译程序集。

    本文不是讲述.Net Framework中诸多的内置结点如何设置,比如httpHandler、httpModule、membership、roleManager 等。而是讲述.Net中配置的实现方式,以及如何定义、使用我们自定义的结点。

    .Net 中的程序配置介绍

    我们首先了解下.Net 中的配置文件是如何工作的。我们看下这段代码:

    // Web.Config
    <appSettings>
        <add key="SiteName" value="TraceFact.Net"/>
    </appSettings>

    // Default.aspx.cs
    protected void Page_Load(object sender, EventArgs e) {
        Literal siteName = new Literal();
        siteName.Text = ConfigurationManager.AppSettings["SiteName"];
        form1.Controls.Add(siteName);
    }

    上面这段代码大家应该再熟悉不过了,我们在appSettings结点中添加了一个add子结点,给key和value属性赋了值,然后在程序中读取了值。但是为什么可以这么做?如果我们想自定义一个配置系统,我们该怎么做呢?

    我们先抛开.Net的机制不谈,来看看如果自己实现一个应用程序的配置方法该如何做,我想可以是这样的:

    1. 首先建立一个XML文件,在这个文件中创建我们需要的结点(或者结点树),在结点的属性或者文本(innerText)中存储配置值。
    2. 创建一个类,这个类的字段和属性映射XML中的某个结点下的属性和文本,以提供强类型的访问。
    3. 创建一个配置文件Xml的访问类,在下面添加一个方法,比如叫GetSection(string nodeName),参数nodeName是结点(或者结点树的根节点)的名称。在方法内部,先创建第二步的类型实例,然后使用System.Xml命名空间下的方法对结点进行处理,对实例的属性进行赋值,最后返回这个实例。
    4. 在程序中通过这个实例来访问配置的结点值。

    上面的思路应该是很清晰的,可是存在一个问题:我们的XML文件中可能会包含多个结点,而每个结点的结构可能都不相同。比如说:每个结点下的子结点可能不相同,每个结点的属性可能不相同。这样的话,我们的GetSection()方法实际上只能是针对某个特定的结点进行。那么该怎么办呢?我们只有为不同的结点指定不同的GetSection()方法了。而如何进行指定呢?我们可以写一大串的GetSectionA()、GetSectionB、GetSectionC()让它们分别去对应SectionA、SectionB、SectionC。但是我们还有更好的方法,我们可以将调用GetSection()时的结点处理逻辑委托给其他的类型去处理,而在哪里指定某个结点由某个委托程序去处理呢?自然最好还是写在配置文件中。比如,我们的XML文件是这样的:

    <?xml version="1.0"?>
    <configuration>
        <forum name="TraceFact.Net Community">
           <root url="http://forum.tracefact.net" />
           <replyCount>20</replyCount>
           <pageSize>30</pageSize>
           <offlineTime>20</offlineTime>
        </forum>

        <blog name="Jimmy Zhang's Personal Space">
           <root url="http://blog.tracefact.net" />
           <urlMappings>
               <rewriteRule>
                  <request>~/(\d{4})/Default\.aspx</request>
                  <sendTo>~/BlogDetail.aspx?year=$1</sendTo>
               </rewriteRule>
           </urlMappings>
        </blog>
    </configuration>

    那么,我们要定义对于 forum结点和 blog结点使用不同的GetSection()方法,我们就可以这么写:

    <?xml version="1.0"?>
    <configuration>
        <configSections>     
           <section name="forum" type="forumSectionHandler" />
           <section name="blog" type="blogSectionHandler" />
        </configSections>
       
        <!--以下为 forum 和 blog 结点,省略-->

    </configuration>

    其中,configSections下的两个section结点分别用于定义对于forum和blog结点使用哪种处理方式。section结点的name属性说明是对于哪个结点,type属性说明对于该结点用什么程序来处理(当调用GetSection()方法时,会交给type所指定的类型去处理)。

    看到这里你应该已经明白了,上面讲述的其实正是.Net中的配置处理方法:在.Net中,配置文件实际分为了两部分,一部分是配置的实际内容,比如appSettings以及上例中的blog和forum结点;另一部分指定结点的处理程序,这些结点位于 configSections 结点下面。

    当你打开站点下的web.config文件,你可能看不到太多的configSections下的结点,这是因为诸如AppSettings这样的结点属于内置结点,对于它们的设置全部位于C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG 下的Machine.Config文件中,以提供全局服务。(操作系统以及.Net Framework版本不同此目录的地址也不尽相同)。

    .Net 应用程序配置方法

    使用 .Net内置结点 和 .Net内置处理程序

    下面我们来一步步地实现.Net中的应用程序配置,首先看下对于.Net中内置的结点如何进行配置以及在程序中进行读取。

    创建文件夹GeneralConfig,在文件夹下新建一个站点WebSite,修改Web.Config,删除原有内容,添加如下代码(为了美观,我添加了Theme,进行了简单的样式设定,可以从文章所附的代码中进行下载):

    <?xml version="1.0"?>
    <configuration>

        <!-- Basic.aspx,使用.Net内置的结点和处理程序 -->
        <appSettings>
           <add key="SiteName" value="TraceFact.Net"/>
           <add key="Version" value="v1.0.08040301" />
           <add key="GreetingLanguage" value="Chinese" />
        </appSettings>

        <connectionStrings>
           <remove name="LocalSqlServer"/>
           <add name="LocalServer" connectionString="User ID=sa;Password=password;Initial Catalog=pubs;Data Source=."  providerName="System.Data.SqlClient"/>
           <add name="PassportCenter" connectionString="User ID=sa;Password=password;Initial Catalog=pubs;Data Source=www.remotesite.com" providerName="System.Data.SqlClient" />
        </connectionStrings>
       
        <!-- 以下配置应用于本范例程序,但不是文章所讨论的范围 -->
        <system.web>
           <compilation debug="true"/>
           <pages theme="Default" />
        </system.web>

    </configuration>

    本节我们演示如何读取appSettings以及ConnectionStrings下的配置数据。注意到Web.Config中没有configSection结点的设置,也就是并没有定义appSettings结点该如何处理。如上节所说,这是因为它们的结点处理程序定义在了machine.config中,打开machine.config,我们可以看到这样的设置:

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
        <configSections>
           <section name="appSettings" type="System.Configuration.AppSettingsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false"/>
           <section name="connectionStrings" type="System.Configuration.ConnectionStringsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" requirePermission="false"/>
        </configSections>
        <!-- 其余略 -->
    </configuration>

    可以看到,对于appSettings实际上是由System.Configuration.AppSettingsSection处理,而对于connectionStrings 实际上是由System.Configuration.ConnectionStringsSection 处理。另外再观察一下machine.config就会发现,处理程序分成了两种类型:一种是以Section结尾的,比如上面的这两个;还有一种是以Handler结尾的,比如:

    <section name="system.data.dataset" type="System.Configuration.NameValueFileSectionHandler, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" restartOnExternalChanges="false"/>

    可以看到type 属性System.Configuration.NameValueFileSectionHandler是以Handler结尾的。

    之所以会有这样的区别,是因为.Net中对于结点有两种处理方式,一种是定义一个继承自System.Configuration.ConfigurationSection 的类,这也就是以Section结尾的类型;一种是实现System.Configuration.IConfigurationSectionHandler 接口,也就是以Handler结尾的类型。.Net Framework 2.0以后版本推荐采用继承ConfigurationSection类的方式,在本文,范例大多数使用实现IConfigurationSectionHandler接口的方式,下面也会提供一个继承ConfigurationSection类的方式作为对比。

    NOTE:使用私有程序集时 type通常由两部分组成,由逗号“,”分隔,前半部分是类型名称,后半部分是程序集名称。如果是公有程序集(GAC),则需要提供publicKey。

    好了,现在我们看一下如何在程序中读取它们。添加一个文件Basic.aspx,修改它的代码如下(只含主要代码,下同):

    // Basic.aspx
    <h2>Website Information:</h2>
    <hr />
    <b>Website Name</b> :<asp:Literal ID="ltrSiteName" runat="server"></asp:Literal><br />
    <b>Version</b>:<asp:Literal ID="ltrVersion" runat="server"></asp:Literal>
    <br /><br />
    <b>LocalServer Connection</b>:
    <asp:Literal ID="ltrLocalConnection" runat="server"></asp:Literal>

    // Basic.aspx.cs
    protected void Page_Load(object sender, EventArgs e)
    {
        string siteName = ConfigurationManager.AppSettings["SiteName"];
        string version = ConfigurationManager.AppSettings["Version"];
        string localConnection = ConfigurationManager.ConnectionStrings["LocalServer"].ConnectionString;

        ltrSiteName.Text = siteName;
        ltrVersion.Text = version;
        ltrLocalConnection.Text = localConnection;
    }

    在浏览器中浏览,应该可以看到这样的界面:

    使用 自定义结点 和 .Net内置处理程序

    在上一节,我们使用了.Net内置的结点 appSettings 和 connectionStrings结点,并使用了 .Net 内置的处理程序。.Net 内置的处理程序定义于 machine.config中,提供全局服务,所以我们无需进行任何额外工作就可以直接使用。但是使用内置结点在很多情况下不一定方便,比如说,我们希望保存站点使用的邮件服务器的地址、用户名和密码,那么按照上面的做法,我想应该是这样的:

    <add key="MailServer" value="mail.tracefact.net" />
    <add key="MailUser" value="jimmyzhang" />
    <add key="MailPassword" value="123456"/>

    这样的话配置及使用并不方便:首先,很明显这三个add是一组数据,但是除了凭自己的经验判断,再没有任何办法进行区分;其次,如果我们有多组服务器或者很多配置,我们需要写很长的add结点。如果我们可以自定一个结点,情况就会好很多,比如我们在Web.Config中添加一个结点:

    <?xml version="1.0"?>
    <configuration>
        <!-- 其余略 -->
        <mailServer address="mail.tracefact.net" userName="jimmyzhang" password="123456" />
    </configuration>

    这样看起来就好了很多,mailServer表示这是一个关于邮件服务器配置的结点,它的属性/值 分别代表存储的相应的值。以后我们在程序中进行发送邮件时可以根据这里的值来对发送邮件的对象进行参数设置。本节我们就来看下如何在web.Config中使用我们自定义的结点,但使用.Net内置的处理程序。

    NOTE:在web.config中,结点以及属性的命名遵循Camel命名方式,也就是首字母小写,其后的每个单词首字母大些的方式。

    接着在站点中添加一个 Simple.aspx 文件,打开它。此时编译器会报错,提示:“分析器错误信息: 无法识别的配置节 mailServer”。 .Net已经提供了很多内置的处理程序,为了避免发生这个错误,我们必须在configSection中指定对mailServer结点的处理程序。有时候我们希望绕过.Net的机制,直接使用System.Xml命名空间下的类来对配置文件(web.config也是标准的Xml文件)进行操作,但是因为这里会报错,所以有的人干脆就另建一个xml文件了事,然后对新建的xml文件进行操作。实际上,可以通过指定IgnoreSectionHandler 或者 IgnoreSection 处理程序的方式来进行处理,如同它们的名称所暗示的,这两个处理程序什么都不做,仅仅是让.Net 忽略我们的自定义结点。修改 web.Config ,在根节点configuration下创建configSections结点,然后再添加一个section结点,指定它的name属性值为mailServer,意为指定mailServer结点的处理程序,然后指定type为System.Configuration.IgnoreSection

    <?xml version="1.0"?>
    <configuration>
        <configSections>
           <!-- 使用IgnoreSection,可以将指定的XMl结点忽视掉 -->
           <section name="mailServer" type="System.Configuration.IgnoreSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" allowLocation="false" restartOnExternalChanges="true" />
        </configSections>
       
        <!-- 自定义结点 mailServer -->
        <mailServer address="mail.tracefact.net" userName="jimmyzhang" password="123456" />
       
    </configuration>

    此时再次打开 Simple.aspx,编译器不再报错,由于我们什么内容也没有添加,此时会显示一个空白页面。现在,你可以采用“老办法”,编写程序去处理这个结点了,但是本文要讲述的,是一种更优雅、更.Net的方式。

    在本节,我们暂且不自定义处理程序,看看.Net中除了这个IgnoreSectionHandler还有什么可以利用的处理程序。在.Net中,还有一个较为常用的处理程序,就是System.Configuration.SingleTagSectionHandler,它会以Hashtable的形式返回结点的所有属性。

    现在我们将上面定义的configSections结点下name属性为mailServer的section结点的type属性改为System.Configuration.SingleTagSectionHandler:

    <?xml version="1.0"?>
    <configuration>
        <configSections>
           <!-- 对自定义结点 mailServer 定义处理程序,使用.Net 内置的处理程序 -->
           <section name="mailServer" type="System.Configuration.SingleTagSectionHandler, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"  />
        </configSections>

        <mailServer address="mail.tracefact.net" userName="jimmyzhang" password="123456" />
    </configuration>

    然后,修改simple.aspx文件,代码如下所示:

    // Simple.aspx
    <div>
        <h1>使用 自定义结点 和 .Net内置处理程序</h1>
        <h2>MailServer Information:</h2><hr />
        <b>Address</b>: <asp:Literal ID="ltrAddress" runat="server"></asp:Literal><br />
        <b>UserName</b>: <asp:Literal ID="ltrUserName" runat="server"></asp:Literal><br />
        <b>Password</b>: <asp:Literal ID="ltrPassword" runat="server"></asp:Literal>
    </div>

    // Simple.aspx.cs
    protected void Page_Load(object sender, EventArgs e) {
        // 返回一个Hashtable
        Hashtable mailServer = (Hashtable)ConfigurationManager.GetSection("mailServer");

        ltrAddress.Text = mailServer["address"].ToString();
        ltrUserName.Text = mailServer["userName"].ToString();
        ltrPassword.Text = mailServer["password"].ToString();
    }

    我们打开页面,应该会看到诸如下面这样的界面:

    可以看到,当我们使用System.Configuration.SingleTagSectionHandler时,调用GetSection()方法会返回一个Hashtable(当调用GetSection()方法时,会潜在地执行configSection中相应结点所指定的type类型的方法),Hashtable的key为属性的名称,Hashtable的value为属性的值。

    使用 自定义结点 和 自定义处理程序

    上面的方法虽然可行,但还存在着问题:

    • 采用Hashtable的方法,由于key是字符串类型,除非你将结点的属性全部背过了,不然我们不得不去查看web.config文件,找到属性,然后才能书写代码去获取(比如 mailServer["address"])。而不能使用Vs提供的自动提示功能,也就是强类型访问的能力,这样的话使用起来很不方便。
    • 假如我们的站点大了一些,只使用一个邮件服务器可能压力太大,我们需要设置多个邮件服务器,对于子域名 forum.tracefact.net 使用一个邮件服务器;对于 blog.tracefact.net使用另一个邮件服务器,这时我们要如何设置Web.Config呢?

    此时,我们可能会需要下面这样结构的配置:

    <?xml version="1.0"?>
    <configuration>  
        <!-- SimpleCustom.aspx, 使用自定义结点和自定义处理程序 -->
        <mailServerGroup provider="www.edong.com">
           <mailServer client="forum.tracefact.net">
               <address>mail1.tracefact.net</address>
               <userName>jimmyzhang</userName>
               <password>123456</password>
           </mailServer>
           <mailServer client="blog.tracefact.com">
               <address>mail2.tracefact.net</address>
               <userName>webmaster</userName>
               <password>456789</password>
           </mailServer>
        </mailServerGroup>
    </configuration>

    mailServerGroup 结点包含了所有关于邮件服务器的信息。它的provider属性说明邮件服务器是由哪个ISP(Internet Service Provider 互联网服务供应商)提供的,这里是中国易动网(www.edong.com)。其下的结点mailServer是指具体的邮件服务器,client说明此邮件服务器为哪个域名提供服务,address说明邮件服务器的地址,userName和password分别为用户名和密码。

    此时,如果我们使用上一节的办法,将无法实现,因为它只能对单个结点进行操作,结点下不能包含子结点(文本节点也不行)。这个时候,我们最好自定义一个结点处理程序来完成。如同上面所说,此时有两种方法,一种是实现IConfigurationSectionHandler 接口,一种是继承ConfigurationSection类。我们先看如何通过实现IConfigurationSectionHandler接口的方式完成。

    自定义结点处理程序 – 实现IConfigurationSectionHandler接口

    IConfigurationSectionHandler 接口的定义如下:

    namespace System.Configuration {
        public interface IConfigurationSectionHandler {
           object Create(object parent, object configContext, XmlNode section);
        }
    }

    它只要求实现一个方法:Create(),当我们在ConfigurationManager对象上调用GetSection("sectionName")方法的时候,实际上会委托给这个Create()方法进行处理。这个方法最重要的一个参数是类型为XmlNode的section,它代表着名为“sectionName”的结点。它返回一个object类型的对象,这个对象通常是我们自定义的一个关于这个结点的配置对象,对象的字段和属性映射结点的属性和文本值,来提供强类型的访问(你也可以返回一个Hashtable,这样就无需自定义类型)。

    NOTE:结点在在传递时有一个转换,调用GetSection()时,传递的是String类型的结点名称;而在Create()方法中,传递的是该名称的XmlNode类型的结点。

    我们在解决方案下面新添加一个类库项目,CustomConfig。然后在下面添加一个类文件,MailServerConfigHandler.cs。现在我们来编写代码,首先创建一个类,MailServer,这个类用于映射Web.Config中mailServerGroup结点下的mailServer子结点:

    public class MailServer {

        // 存储mailServer的子结点(Address,UserName,Password)的innerText值
        // 以及属性 Client 的值
        private Hashtable serverNode;

        public MailServer() {
           serverNode = new Hashtable();
        }

        public Hashtable ServerNode {
           get { return serverNode; }
        }

        public string Client {
           get {
               return serverNode["client"] as string;
           }
        }

        public string Address {
           get {
               return serverNode["address"] as string;
           }
        }

        public string UserName {
           get {
               return serverNode["userName"] as string;
           }
        }

        public string Password {
           get {
               return serverNode["password"] as string;
           }
        }
    }

    注意,在MailServer类的内部,我们维护了一个Hashtable,这是因为在处理mailServerGroup结点时,我们对于它的子结点mailServer的所有属性值以及innerText都会存储在一个Hashtable中,在后面将会看到。

    然后我们再建一个类 MailServerConfig,让这个类继承自List<MailServer>,它用于映射mailServerGroup结点,我们对于mailServerGroup结点的属性以及子结点的访问,实际上均通过这个类来进行:

    public class MailServerConfig : List<MailServer> {
        private string provider;     // 对应 mailServerGroup 结点的provider 属性
        public string Provider {
           get { return provider; }
           set { provider = value; }
        }
    }

    最后,到了我们对于结点的实际处理工作,创建类 MailServerConfigurationHandler,让它实现IConfigurationSectionHandler 接口:

    // 自定义配置结点 mailServerGroup 的处理程序
    public class MailServerConfigurationHandler : IConfigurationSectionHandler {

        // section 为 MailServerGroup 结点
        public object Create(object parent, object configContext, XmlNode section) {

           // 设置方法返回的配置对象,可以是任何类型
           MailServerConfig config = new MailServerConfig();

           // 获取结点的属性信息       
           config.Provider =
               section.Attributes["provider"] == null ? "" : section.Attributes["provider"].Value;

           // 获取 MailServer 结点
           foreach (XmlNode child in section.ChildNodes) {

               MailServer server = new MailServer();

               // 添加Client属性
               if (child.Attributes["client"] != null)
                  server.ServerNode.Add("client", child.Attributes["client"].Value);

               // 获取MailServer下的 Name,UserName,Password 结点
               foreach (XmlNode grandChild in child.ChildNodes) {

                  // 添加文本
                  server.ServerNode.Add(grandChild.Name, grandChild.InnerText);
               }

               // 将server加入 MailServerConfig
               config.Add(server);
           }
           return config;
        }
    }

    我在这段代码中添加了详细的注释,这里就不再多说明了,需要注意的是它返回了一个MailServerConfig对象,在程序中,我们将通过MailServerConfig来访问结点信息。

    现在我们为站点添加CustomConfig的项目引用,修改Web.Config文件,添加下面代码来说明对于mailServerGroup结点的处理程序:

    <?xml version="1.0"?>
    <configuration>
        <configSections>     
           <section name="mailServerGroup" type="CustomConfig.MailServerConfigurationHandler, CustomConfig"  />
        </configSections>

        <!-- mailServerGroup 结点,此处略 -->
    </configuration>

    接下来,我们再在站点下添加一个SimpleCustom.aspx文件,使用它来测试我们的配置处理程序:

    // SimpleCustom.aspx
    <h1>使用 自定义结点 和 自定义处理程序</h1>
    <b>MailServerGroup Provider</b>: <asp:Literal ID="ltrServerProvider" runat="server"></asp:Literal>

    <h2>Mail Server1(Client:<asp:Literal ID="ltrClient1" runat="server"></asp:Literal>) Information:</h2>
    <hr />
        <b>Address</b>: <asp:Literal ID="ltrAddress1" runat="server"></asp:Literal> <br />
        <b>UserName</b>: <asp:Literal ID="ltrUserName1" runat="server"></asp:Literal><br />
        <b>Password</b>: <asp:Literal ID="ltrPassword1" runat="server"></asp:Literal>

    <h2>Mail Server2(Client:<asp:Literal ID="ltrClient2" runat="server"></asp:Literal>) Information:</h2>
    <hr />
        <b>Address</b>: <asp:Literal ID="ltrAddress2" runat="server"></asp:Literal> <br />
        <b>UserName</b>: <asp:Literal ID="ltrUserName2" runat="server"></asp:Literal><br />
        <b>Password</b>: <asp:Literal ID="ltrPassword2" runat="server"></asp:Literal>
    <br /><br />

    // SimpleCustom.aspx.cs
    protected void Page_Load(object sender, EventArgs e) {
        // 获取 mailServerConfig 对象
        MailServerConfig mailConfig = (MailServerConfig)ConfigurationManager.GetSection("mailServerGroup");

        // 获取 MailServerGroup 结点的 Provider 属性
        ltrServerProvider.Text = mailConfig.Provider;

        // 获取第一租 MailServer 数据
        ltrClient1.Text = mailConfig[0].Client;
        ltrAddress1.Text = mailConfig[0].Address;
        ltrUserName1.Text = mailConfig[0].UserName;
        ltrPassword1.Text = mailConfig[0].Password;

        // 获取第二租 MailServer 数据
        ltrClient2.Text = mailConfig[1].Client;
        ltrAddress2.Text = mailConfig[1].Address;
        ltrUserName2.Text = mailConfig[1].UserName;
        ltrPassword2.Text = mailConfig[1].Password;
    }

    现在打开页面,应该可以看到下面的页面:

    自定义结点处理程序 – 继承ConfigurationSection基类

    除了实现IConfigurationSectionHandler接口来自定义结点处理程序,还可以通过继承ConfigurationSection基类的方式来完成,我们还以上面的例子来做说明。一般来说我们想要存储的数据可以用两种方式来存储:一种是存储到结点的属性中,一种是存储在结点的文本(InnerText)中。比如:

    <node>这里是要存储的值</node>
    <!-- 或者是下面这样,两种的效果是一样的  -->
    <node text="这里是要存储的值" />

    因为一个结点可以有很多的属性,但只有一个InnerText,而在程序又要将这两种形式区别处理显然太麻烦了,所以Microsoft干脆就只使用属性存储而不使用InnerText,大家可以看一下machine.config中的配置,可曾见到过一个以InnerText来存储配置信息的?在ConfigurationSection中,也没有提供对InnerText的处理,所以对于上面的例子,我们首先要进行重新格式化,仅使用属性来存储我们的配置值。

    除此以外,我们还要将结点分组,对于入口结点,也就是mailServerGroup而言,这个结点组相当于它的一个属性(就好像provider一样),因为结点组包含多个结点,所以映射到面向对象的代码中,自然就成了一个集合类。所以我们需要按照这些概念将web.config中的mailServerGroup改写成下面这样:

    <mailServerGroup2 provider="www.edong.com">
        <mailServers>
           <mailServer
               client="forum.tracefact.net"
               address="mail1.tracefact.net"
               userName="jimmyzhang"
               password="123456" />
           <mailServer
               client="blog.tracefact.net"
               address="mail2.tracefact.net"
               userName="webmaster"
               password="456789" />
        </mailServers>
    </mailServerGroup2>

    注意,我们将mailServerGroup改成了mailServerGroup2,以免和上一节的冲突。现在我们在CustomConfig项目中添加一个文件 MailServerSection.cs,然后添加如下代码:

    // MailServerSection 为入口
    public class MailServerSection : ConfigurationSection {

        [ConfigurationProperty("provider", IsKey = true)]
        public string Provider {
           get { return this["provider"] as string; }
        }

        [ConfigurationProperty("mailServers", IsDefaultCollection = false)]
        public MailServerCollection MailServers {
           get {
               return (MailServerCollection)this["mailServers"];
           }
           set {
               this["mailServers"] = value;
           }
        }
    }

    // MailServer 结点
    public sealed class MailServerElement : ConfigurationElement {

        [ConfigurationProperty("client", IsKey = true, IsRequired = true)]
        public string Client {
           get { return this["client"] as string; }
           set { this["client"] = value; }
        }

        [ConfigurationProperty("address")]
        public string Address {
           get { return this["address"] as string; }
           set { this["client"] = value; }
        }

        [ConfigurationProperty("userName")]
        public string UserName {
           get { return this["userName"] as string; }
           set { this["client"] = value; }
        }

        [ConfigurationProperty("password")]
        public string Password {
           get { return this["password"] as string; }
           set { this["client"] = value; }
        }
    }

    // MailServer 集合类
    public sealed class MailServerCollection : ConfigurationElementCollection {

        public override ConfigurationElementCollectionType CollectionType {
           get { return ConfigurationElementCollectionType.BasicMap; }
        }

        protected override ConfigurationElement CreateNewElement() {
           return new MailServerElement();
        }

        protected override Object GetElementKey(ConfigurationElement element) {
           return ((MailServerElement)element).Client;
        }

        protected override string ElementName {
           get { return "mailServer"; }
        }

        public new int Count {
           get { return base.Count; }
        }


        public MailServerElement this[int index] {
           get {
               return (MailServerElement)BaseGet(index);
           }
           set {
               if (BaseGet(index) != null) {
                  BaseRemoveAt(index);
               }
               BaseAdd(index, value);
           }
        }

        new public MailServerElement this[string Name] {
           get {
               return (MailServerElement)BaseGet(Name);
           }
        }

        public int IndexOf(MailServerElement element) {
           return BaseIndexOf(element);
        }

        public void Add(MailServerElement element) {
           BaseAdd(element);
        }

        public void Remove(MailServerElement element) {
           if (BaseIndexOf(element) >= 0)
               BaseRemove(element.Client);
        }

        public void RemoveAt(int index) {
           BaseRemoveAt(index);
        }

        public void Remove(string client) {
           BaseRemove(client);
        }

        public void Clear() {
           BaseClear();
        }
    }

    这段代码由三部分组成,一部分是MailServerSection类,很明显它是配置结点的入口,它包含两个属性,一个是String类型的provider属性,它映射mailServerGroup结点的provider属性;一个是MailServerCollection类型的MailServers属性,这个属性映射我们新添加的mailServers结点,mailServers结点下还包含了若干个mailServer结点。从它的名称也可以看出来,它是一个集合类。

    MailServerElement类用于映射mailServer结点的属性,这里是我们实际存储数据的地方。MailServerCollection类用于映射mailServers结点,可以看出它是一个集合类,另外还包含了很多对于结点进行操作的方法,大部分的能力都继承自ConfigurationElementCollection基类。

    值得注意的是,之所以可以使用这种方式实现,使用了大量的特性标记。当你看到特性标记的时候,你应该就想到必须有地方使用反射来读取特性的值,不然特性毫无意义。只是这部分的内容属于.Net Framework的底层,无需我们操心。

    现在在Web.Config的configSections结点下添加我们的结点处理程序的配置:

    <section name="mailServerGroup2" type="CustomConfig.MailServerSection, CustomConfig"  />

    然后复制SimpleCustom.aspx文件,并将文件名改为SimpleCustom2.aspx,然后修改SimpleCustom2.aspx,几乎不需要做太多修改就可以了:

    // 获取 MailServerSection 对象
    MailServerSection mailSection = (MailServerSection)ConfigurationManager.GetSection("mailServerGroup2");

    // 获取 MailServerGroup 结点的 Provider 属性
    ltrServerProvider.Text = mailSection.Provider;

    // 获取第一租 MailServer 数据
    ltrClient1.Text = mailSection.MailServers[0].Client;
    ltrAddress1.Text = mailSection.MailServers[0].Address;
    ltrUserName1.Text = mailSection.MailServers[0].UserName;
    ltrPassword1.Text = mailSection.MailServers[0].Password;

    // 获取第二租 MailServer 数据
    ltrClient2.Text = mailSection.MailServers[1].Client;
    ltrAddress2.Text = mailSection.MailServers[1].Address;
    ltrUserName2.Text = mailSection.MailServers[1].UserName;
    ltrPassword2.Text = mailSection.MailServers[1].Password;

    打开后可以看到和上一节完全一样的界面。

    “存储”类型实例

    有时候,我们可能不仅希望能在Web.Config中存储字符串类型的值,而希望存储一个对象。比如说我们想在程序中应用Strategy模式,我们可能希望在配置中定义Strategy模式采用哪个算法。现在我们先实现一个简单的Strategy模式,然后再看如何进行配置。新添加一个类库项目ClassLib,然后添加IGreetingStrategy.cs文件,修改代码如下:

    namespace ClassLib
    {
        // 定义接口
        public interface IGreetingStrategy {
           string GreetingType { get; }
           void SetGreetingWords(ITextControl textControl);
        }

        // 英文版问候程序
        public class EnglishGreeting : IGreetingStrategy {
           public string GreetingType {
               get { return "English Greeting"; }
           }

           public void SetGreetingWords(ITextControl textControl) {
               textControl.Text = "Hello, my reader !";
           }
        }

        // 中文版问候程序
        public class ChineseGreeting : IGreetingStrategy {

           private string greetingType;

           public ChineseGreeting(string greetingType) {
               this.greetingType = greetingType;
           }

           public ChineseGreeting() : this("中文问候") { }

           public string GreetingType {
               get { return greetingType; }
           }

           public void SetGreetingWords(ITextControl textControl) {
               textControl.Text = "你好, 我的读者 !";
           }
        }


        public class GeneralClass {
           // 这个类可能还有很多的 字段、属性、方法,这里省略
           private IGreetingStrategy gs;

           public GeneralClass(IGreetingStrategy gs)  {
               this.gs = gs;
           }

           public string GeneralProperty {
               get {
                  // 做一些额外的工作,这里省略
                  return "<span style='color:#008000'>" + gs.GreetingType + "</span>";
               }
           }

           public void GeneralMethod(ITextControl textControl) {
               // 做一些额外的工作,这里省略...
               gs.SetGreetingWords(textControl);
               textControl.Text = "<span style='color:#008000'>" + textControl.Text + "</span>";
               // 省略...
           }
        }
    }

    GeneralClass是一个普通类型(你可以把它想象成实际应用中的任何类型),它的内部维护一个IGreetingStrategy类型,在GeneralClass的方法和属性中,调用了IGreetingStrategy所提供的方法,实际上会根据IGreetingStrategy的不同实现调用了不同对象的方法(英文问候或者中文问候)。

    NOTE:这里不再对Strategy模式进行讨论了,可以参见我的《奇幻RPG(角色技能 与 Strategy模式)》一文。

    现在让站点引用新建的ClassLib项目,新添一个文件ObjectStore.aspx,修改代码如下:

    // ObjectStore.aspx
    <h1>在Config中存储类型信息(模拟存储对象)</h1>
    <hr />
    <b>Greeting Type</b>: <asp:Literal ID="ltrGreetingType" runat="server" ></asp:Literal><br />
    <b>Greeting Words</b>: <asp:Literal ID="ltrGreetingWrods" runat="server" ></asp:Literal>

    // ObjectStore.aspx.cs
    protected void Page_Load(object sender, EventArgs e) {
        // Hard Coding,直接写入到程序里,不使用配置
        IGreetingStrategy greetingStrategy = new ChineseGreeting();
        GeneralClass generalObj = new GeneralClass(greetingStrategy);

        // 以下为通用代码
        if (generalObj != null){
           ltrGreetingType.Text = generalObj.GeneralProperty;
           generalObj.GeneralMethod(ltrGreetingWrods);
        } else{
           ltrGreetingType.Text = "IGreetingStrategy Load Error.";
           ltrGreetingWrods.Text = "IGreetingStrategy Load Error.";
        }
    }

    在这里,我们根本没有进行任何程序配置,直接HardCoding到了代码中,目的只是先测试下代码是否运行正常。此时在浏览器中打开页面,应该可以看到如下的画面:

    好了,现在我们有了新的需求,我希望使用英文版的问候方法,也就是使用EnglishGreeting,该如何做呢?似乎很简单,我们只要修改一行代码就可以了:

    // 将 ChineseGreeting 改为 EnglishGreeting
    IGreetingStrategy greetingStrategy = new EnglishGreeting();

    问题是:这是采用HardCoding直接修改代码的方式,如果我们希望可以通过配置文件来完成这样的转化,该如何做呢?我想有不少人大概会这样,先在Web.Config中的appSettings下面添加一个结点,然后在程序中对这个结点的值进行判断,根据判断结果来决定使用ChineseGreeting还是EnglishGreeting:

    修改Web.Config结点,在appSettings下添加这样的代码:

    <add key="GreetingLanguage" value="English" />

    然后修改 Object.aspx.cs文件,将代码改成如下这样:

    // 使用AppSettings 以及 if else(switch)语句来进行配置
    string strategy = ConfigurationManager.AppSettings["GreetingLanguage"];
    IGreetingStrategy greetingStrategy = null;
    GeneralClass generalObj = null;

    if (strategy == "Chinese")
        greetingStrategy = new ChineseGreeting();
    else if (strategy == "English")
        greetingStrategy = new EnglishGreeting();

    if (greetingStrategy != null)
        generalObj = new GeneralClass(greetingStrategy);

    // 以下代码相同,略…

    这样显然可以实现我们的要求,但是它还有不足:1、对于语言类型的判断我们依然是Hardcoding到代码中去的,这样如果我们日后添加了韩文Korean或者日文Japanese,我们依然需要修改代码;2、appSettings中以及程序中都是字符串的形式存储,如果我们不小心输错一个字母,那么if-else语句就不会通过。

    那么我们该如何存储这个IGreetingStrategy类型的对象呢?使用Xml串行化么?不!我们应该想想有什么办法可以通过一个字符串(Xml文件中的配置结点存储的值为字符串类型),来获得一个对象呢?答案是使用反射!我们可以借鉴.Net的方式,将类型信息存储到Web.Config的结点中,然后在程序中获取结点的值,最后再利用反射来动态地创建类型。

    NOTE:下面的部分代码要求你对反射有所了解,可以参看 《.Net 中的反射(动态创建类型实例)–Part.4

    有了思路,接下来我们就来一步步地实现,我们首先在Web.Config中创建一个自定义结点greetingStrategy:

    <!-- ObjectStore.aspx, 配置类型信息(存储对象)  -->
    <greetingStrategy type="ClassLib.ChineseGreeting, ClassLib" />

    它只有一个属性type,当我们指定程序使用哪个IGreetingStrategy时,只要在这里进行设置就可以了。type由“,”分隔为了两部分,前部分是类型名称,后半部分是类型所在的程序集,如果是GAC,那么还需要添加publicKey等信息。当前的这个结点设置为ChineseGreeting。

    接下来,我们要为这个结点创建处理程序。我们在CustomConfig项目下再添加一个文件GreetingConfigHandler.cs,添加如下代码:

    public class GreetingConfigurationHandler : IConfigurationSectionHandler {
        // 这里的section结点为 Web.Config中的greetingStrategy结点
        public object Create(object parent, object configContext, XmlNode section) {

           // 获取结点type属性的值
           Type t = Type.GetType(section.Attributes["type"].Value);
           // 将要创建的类型实例
           object obj = null;

           try {
               obj = Activator.CreateInstance(t);
           } catch (Exception ex) {
               return null;
           }
           // obj为结点 type 属性中定义的对象,在这里是 ClassLib.ChineseGreeting
           return obj;  
        }
    }

    上面这段代码根据greetingStrategy结点的type属性创建并返回了一个IGreetingStrategy类型实例。接下来我们在Web.Config中的configSections结点下添加对greetingStrategy结点的处理程序:

    <section name="greetingStrategy" type="CustomConfig.GreetingConfigurationHandler, CustomConfig"/>

    OK!现在我们再次修改ObjectStore.aspx.cs文件,使用新的方式来获取IGreetingStrategy对象:

    IGreetingStrategy greetingStrategy =
        (IGreetingStrategy)ConfigurationManager.GetSection("greetingStrategy");

    GeneralClass generalObj = null;
    if (greetingStrategy != null)
        generalObj = new GeneralClass(greetingStrategy);

    打开页面,应该会看到和上面一样的效果,区别只是我们采用了更加灵活的方式。

    使用有参数的构造函数创建类型实例

    注意,在这里我们创建类型时使用的是无参数的构造函数,在Activator的CreateInstance()方法中没有提供构造函数需要的参数。但是回头看下我们的ChineseGreeting类型的定义,它还有一个包含一个参数的构造函数:

    public class ChineseGreeting : IGreetingStrategy {

        private string greetingType;

        public ChineseGreeting(string greetingType) {
           this.greetingType = greetingType;
        }
        // 其余略
    }

    那么如果我们需要通过有参数的构造函数创建一个类型实例,又该如何做呢?首先,我们应该修改Web.Config中的greetingStrategy结点,让它包含参数信息:

    <greetingStrategy type="ClassLib.ChineseGreeting, ClassLib" >
        <params greetingType="**中文问候**" />
    </greetingStrategy>

    接下来,我们要修改GreetingConfigurationHandler类型,让它在创建类型时根据结点获取值,然后传递参数greetingType进去。

    此时有两种策略:

    • 在GreetingConfigurationHandler中对greetingStrategy结点进行处理,取得params结点greetingType属性的值,然后传递给ChineseGreeting类型的构造函数。
    • 在ChineseGreeting中新添一个构造函数,接受一个XmlNode结点,将greetingStrategy结点再次进行传递,然后在这个构造函数中进行处理。

    我们来分别尝试,先使用第一种,修改GreetingConfigurationHandler如下所示(注意为了使代码简单,我没有做诸如param结点是否存在这样的判断,以下同):

    public class GreetingConfigurationHandler : IConfigurationSectionHandler {
        // 这里的section结点为 Web.Config中的greetingStrategy结点
        public object Create(object parent, object configContext, XmlNode section) {

           // 获取结点type属性的值
           Type t = Type.GetType(section.Attributes["type"].Value);
           
           // 获取param结点的属性greetingType
           XmlAttribute attr = section.SelectSingleNode("param").Attributes["greetingType"];
           object[] parameters = { attr.Value };

           // 将要创建的类型实例
           object obj = null;

           try {
               obj = Activator.CreateInstance(t, parameters); // 使用有参数的构造函数
           } catch (Exception ex) {
               return null;
           }
           // obj为结点的 type属性中定义的对象,在这里是 ClassLib.ChineseGreeting
           return obj;
        }
    }

    然后打开页面,可以看到如下图所示,可见此次使用了有参数的构造函数,并读取了Web.Config中的值。

    还有一种方法,是直接将section进行传递,也就是将XmlNode类型的greetingStrategy结点进行传递,在新的构造函数中对这个结点进行处理。此时的GreetingConfigurationHandler除了根据type来创建对象以外,对这个结点不做任何的额外处理。

    我们再次修改 GreetingConfigurationHandler类型,只修改一行代码就可以了:

    // 直接将section结点进行传递
    object[] parameters = { section };

    然后我们为ChineseGreeting再添加一个构造函数:

    public ChineseGreeting(XmlNode section) {
        XmlAttribute attr = section.SelectSingleNode("param").Attributes["greetingType"]; // 获取属性
        greetingType = attr.Value;      // 为字段赋值
    }

    编译ClassLib和CustomConfig项目,然后再次打开ObjectStore.aspx文件,应该看到和上面相同的输出结果。

    统一结点配置管理

    上面一节絮絮叨叨说了这么多,那么我究竟想告诉大家什么呢?可以想一想,我们的应用程序可能会有非常多可以设置的地方,比如我们还可以设置 URL 地址映射、设置每页显示的回帖数、设置分页大小等等,这样我们将会创建非常多的自定义结点,而为了使用每个自定义结点,我们又会创建非常多的Handler处理程序,这样的话光web.config中的configSections结点就需要写一长串,有没有办法对这些配置进行统一管理呢?当然可以!其实上一节的第二种方法就已经实现了这种效果,你可以将params结点想象成一个结点树,那么greetingStrategy就相当于一个入口结点。

    我知道上面这样说你可能不好理解,那么现在我们来看下如何实现:

    首先,我们需要一个Handler处理程序,我们采用上一节的第二种方式,由于这种方式仅仅是将section结点进行传递,然后根据type创建对象,所以我们完全不需要对它进行任何更改。但是为了避免使这两节的内容产生混淆,我们在CustomConfig下在新创建一个GeneralConfigHandler.cs文件,将上面的代码几乎是原封不动地复制下来(改了类型名):

    public class GeneralConfigurationHandler : IConfigurationSectionHandler {
        // 这里的section结点为 Web.Config中的 配置的根结点
        public object Create(object parent, object configContext, XmlNode section) {

           // 获取结点type属性的值
           Type t = Type.GetType(section.Attributes["type"].Value);

           // 直接将section进行传递
           object[] parameters = { section };

           // 将要创建的类型实例
           object obj = null;

           try {
               obj = Activator.CreateInstance(t, parameters); // 使用有参数的构造函数
           } catch (Exception ex) {
               return null;
           }
           // obj为结点的 type属性中定义的对象,在这里是 ClassLib.ChineseGreeting
           return obj;
        }
    }

    然后,我们需要在Web.Config中定义一个程序配置的根结点,对于应用程序的所有配置,我们都将通过这个根节点进行访问,而我们上面创建的GeneralConfigHandler则用于创建对这个根结点进行映射的类型的实例(我们稍后会讲述这个类型)。现在我们看一下这个Web.Config中该如何设置:

    <configSections>
       <section name="traceFact" type="CustomConfig.GeneralConfigurationHandler, CustomConfig"/>
    </configSections>

    <!-- General.aspx, 通用配置存储方法 -->
    <traceFact type="CustomConfig.ConfigManager, CustomConfig">
        <forum name="TraceFact.Net Community">
           <root url="http://forum.tracefact.net" />
           <replyCount>20</replyCount>
           <pageSize>30</pageSize>
           <offlineTime>20</offlineTime>
        </forum>

        <blog name="JimmyZhang's Space">
           <root url="http://blog.tracefact.net" />
           <urlMappings>
               <rewriteRule>
                  <request>~/(\d{4})/Default\.aspx</request>
                  <sendTo>~/BlogDetail.aspx?year=$1</sendTo>
               </rewriteRule>
           </urlMappings>
        </blog>

        <!-- 将上面的配置也包含进来 -->
        <mailServer address="mail.tracefact.net" userName="jimmyzhang" password="123456" />
        <greetingStrategy type="ClassLib.ChineseGreeting, ClassLib" />
    </traceFact>

    注意上面的配置,我们将所有的配置都写在了traceFact根结点下,对于其所有的子结点,我们都通过这个traceFact作为入口来访问。另外注意traceFact结点的type属性,它是一个ConfigManager类型,这个类型的实例是由GeneralConfigurationHandler通过反射动态创建的。现在我们来看一下ConfigManager类型,它实际上只是作为一个容器,包含其下具体的配置结点的引用:

    public class ConfigManager {

        private XmlNode section;
        private ForumConfiguration forumConfig;

        // private BlogConfiguration blogConfig;
        // private MailServerConfiguration mailServerConfig;
        // private IGreetingStrategy greetingStrategy;
        // 以下类似,省略 ...

        public ForumConfiguration ForumConfig {
           get { return forumConfig; }
        }

        //public BlogConfiguration BlogConfig {
        //  get { return blogConfig; }
        //} 以下类似,省略 ...

        public ConfigManager(XmlNode section) {
           this.section = section;

           forumConfig = new ForumConfiguration(section.SelectSingleNode("forum"));

           // blogConfig = new BlogConfiguration(section.SelectSingleNode("blog"));
           // mailServerConfig = new MailServerConfiguration(section.SelectSingleNode("mailServer"));
           // 以下类似,省略 ...
        }
    }

    可以看到,这个ConfigManager仅仅是作为一个容器,包含对其下具体结点配置的引用,并通过traceFact根节点,获取traceFact其下子结点,然后再创建用于映射具体的子结点的类型实例。

    所以对于每一个子结点,我们都需要再创建一个自定义的类,这里我仅创建一个 ForumConfiguration来做说明:

    // 具体的子结点配置 forum 结点
    public class ForumConfiguration {
        private XmlNode forumNode;

        // 将 forum 结点传递进来
        public ForumConfiguration(XmlNode section){
           this.forumNode = section;
        }

        public string Name{
           get{ return forumNode.Attributes["name"].Value; }
        }

        public string RootUrl{
           get { return forumNode.SelectSingleNode("root").Attributes["url"].Value; }
        }

        public int PageSize{
           get { return int.Parse(forumNode.SelectSingleNode("pageSize").InnerText); }
        }

        public int ReplyCount{
           get{ return int.Parse(forumNode.SelectSingleNode("replyCount").InnerText); }
        }

        public int OfflineTime{
           get { return int.Parse(forumNode.SelectSingleNode("offlineTime").InnerText); }
        }
    }

    接着创建一个General.aspx 文件,添加如下代码:

    // General.aspx
    <h1>统一结点配置管理</h1>
    <hr />
    <b>Name:</b><asp:Literal ID="ltrName" runat="server"></asp:Literal><br />
    <b>Root Url:</b><asp:Literal ID="ltrRootUrl" runat="server"></asp:Literal><br />
    <b>Reply Count:</b><asp:Literal ID="ltrReplyCount" runat="server"></asp:Literal><br />
    <b>Page Size:</b><asp:Literal ID="ltrPageSize" runat="server"></asp:Literal><br />
    <b>Offline Time:</b><asp:Literal ID="ltrOfflineTime" runat="server"></asp:Literal>

    // General.aspx.cs
    // 获取 ConfigManager 类型实例
    ConfigManager config = (ConfigManager)ConfigurationManager.GetSection("traceFact");

    ltrName.Text = config.ForumConfig.Name;
    ltrOfflineTime.Text = config.ForumConfig.OfflineTime.ToString();
    ltrPageSize.Text = config.ForumConfig.PageSize.ToString();
    ltrReplyCount.Text = config.ForumConfig.ReplyCount.ToString();
    ltrRootUrl.Text = config.ForumConfig.RootUrl.ToString();

    然后打开页面,应该可以看到如下的画面:

    总结

    在这篇文章中,我向大家简单地介绍了如何通过实现System.Configuration.IConfigurationSectionHandler 接口或者继承System.Configuration.ConfigurationSection 基类来实现自定义结点。

    我们先后学习了如何 使用内置.Net结点以及内置结点处理程序、使用自定义结点配合.Net内置处理程序、自定义结点及处理程序、通过在配置中保存类型信息然后使用反射动态创建对象来模拟存储类型实例,最后我们结合反射以及配置信息创建了自己的ConfigManager来实现对自定义配置结点的管理。

    感谢阅读,希望这片文章能对你有所帮助!

    原文出自:http://www.tracefact.net/CLR-and-Framework/Custom-Application-Configuration.aspx

  • 相关阅读:
    mybatis源码解读(二)——构建Configuration对象
    mybatis源码解读(一)——初始化环境
    JDK1.8源码(七)——java.util.HashMap 类
    JDK1.8源码(六)——java.util.LinkedList 类
    JDK1.8源码(五)——java.util.ArrayList 类
    JDK1.8源码(四)——java.util.Arrays 类
    JDK1.8源码(三)——java.lang.String 类
    JDK1.8源码(二)——java.lang.Integer 类
    JDK1.8源码(一)——java.lang.Object类
    Java的深拷贝和浅拷贝
  • 原文地址:https://www.cnblogs.com/wzh206/p/3076495.html
Copyright © 2011-2022 走看看