.NET6?ConfigurationManager的實(shí)現(xiàn)及使用方式
前言
友情提示:建議閱讀本文之前先了解下.Net Core配置體系相關(guān),也可以參考本人之前的文章《.Net Core Configuration源碼探究 》然后對.Net Core的Configuration體系有一定的了解,使得理解起來更清晰。
在.Net6中關(guān)于配置相關(guān)多出一個(gè)關(guān)于配置相關(guān)的類ConfigurationManager,如果大概了解過Minimal API中的WebApplicationBuilder類相信你肯定發(fā)現(xiàn)了,在Minimal API中的配置相關(guān)屬性Configuration正是ConfigurationManager的對象。ConfigurationManager本身并沒有引入新的技術(shù),也不是一個(gè)體系,只是在原來的基礎(chǔ)上進(jìn)行了進(jìn)一步的封裝,使得配置體系有了一個(gè)新的外觀操作,暫且可以理解為新瓶裝舊酒。本文我們就來了解下ConfigurationManager類,來看下微軟為何在.Net6中會(huì)引入這么一個(gè)新的操作。
使用方式
關(guān)于.Net6中ConfigurationManager的使用方式,我們先通過簡單的示例演示一下
ConfigurationManager configurationManager = new();
configurationManager.AddJsonFile("appsettings.json",true,reloadOnChange:true);
string serviceName = configurationManager["ServiceName"];
Console.WriteLine(serviceName);
當(dāng)然,關(guān)于獲取值得其他方式。比如GetSection、GetChildren相關(guān)方法還是可以繼續(xù)使用的,或者使用Binder擴(kuò)展包相關(guān)的Get<string>()、GetValue<NacosOptions>("nacos")類似的方法也照樣可以使用。那它和之前的.Net Core上的配置使用起來有什么不一樣呢,我們看一下之前配置相關(guān)的使用方式,如下所示
IConfigurationBuilder configurationBuilder = new ConfigurationBuilder().AddJsonFile("appsettings.json");
IConfiguration configuration = configurationBuilder.Build();
string serviceName = configuration["ServiceName"];
Console.WriteLine(serviceName);
這里需要注意的是,如果你是使用ConfigurationManager或者是IConfiguration封裝的Helper類相關(guān),并沒有通過框架體系默認(rèn)注入的時(shí)候,一定要注意將其設(shè)置為單例模式。其實(shí)這個(gè)很好理解,先不說每次用的時(shí)候都去實(shí)例化帶來的內(nèi)存CPU啥的三高問題。讀取配置文件本質(zhì)不就是把數(shù)據(jù)讀到內(nèi)存中嗎?內(nèi)存中有一份緩存這就好了,每次都去重新實(shí)例去讀本身就是一種不規(guī)范的方式。許多時(shí)候如果你實(shí)在不知道該定義成什么樣的生命周期,可以參考微軟的實(shí)現(xiàn)方式,以ConfigurationManager為例,我們可以參考WebApplicationBuilder類中對ConfigurationManager注冊的生命周期[點(diǎn)擊查看源碼]
public ConfigurationManager Configuration { get; } = new();
//這里注冊為了單例模式
Services.AddSingleton<IConfiguration>(_ => Configuration);
通過上面我們演示的示例可以看出在ConfigurationManager的時(shí)候注冊配置和讀取配置相關(guān)都只是使用了這一個(gè)類。而在之前的配置體系中,注冊配置需要使用IConfigurationBuilder,然后通過Build方法得到IConfiguration實(shí)例,然后讀取是通過IConfiguration實(shí)例進(jìn)行的。本身操作配置的時(shí)候IConfigurationBuilder和IConfiguration是滿足單一職責(zé)原則沒問題,像讀取配置這種基礎(chǔ)操作,應(yīng)該是越簡單越好,所以微軟才進(jìn)一步封裝了ConfigurationManager來簡化配置相關(guān)的操作。
在.Net6中微軟并沒有放棄IConfigurationBuilder和IConfiguration,因?yàn)檫@是操作配置文件的基礎(chǔ)類,微軟只是借助了它們兩個(gè)在上面做了進(jìn)一層封裝而已,這個(gè)是需要我們了解的。
源碼探究
上面我們了解了新的ConfigurationManager的使用方式,這里其實(shí)我們有疑問了,為什么ConfigurationManager可以進(jìn)行注冊和讀取操作。上面我提到過ConfigurationManager本身就是新瓶裝舊酒,而且它只是針對原有的配置體系做了一個(gè)新的外觀,接下來哦我們就從源碼入手,看一下它的實(shí)現(xiàn)方式。
定義入手
首先來看一下ConfigurationManager的的定義,如下所示[點(diǎn)擊查看源碼]
public sealed class ConfigurationManager : IConfigurationBuilder, IConfigurationRoot, IDisposable
{
}
其實(shí)只看它的定義就可以解答我們心中的大部分疑惑了,之所以ConfigurationManager能夠滿足IConfigurationBuilder和IConfigurationRoot這兩個(gè)操作的功能是因?yàn)樗旧砭褪菍?shí)現(xiàn)了這兩個(gè)接口,集它們的功能于一身了,IConfigurationRoot接口本身就集成自IConfiguration接口。因此如果給ConfigurationManager換個(gè)馬甲的話你就會(huì)發(fā)現(xiàn)還是原來的配方還是原來的味道
ConfigurationManager configurationManager = new();
IConfigurationBuilder configurationBuilder = configurationManager.AddJsonFile("appsettings.json", true, reloadOnChange: true);
//盡管放心的調(diào)用Build完全不影響啥
IConfiguration configuration = configurationBuilder.Build();
string serviceName = configuration["ServiceName"];
Console.WriteLine(serviceName);
這種寫法只是為了更好的看清它的本質(zhì),如果真實(shí)操作這么寫,確實(shí)有點(diǎn)畫蛇添足了,因?yàn)镃onfigurationManager本身就是為了簡化我們的操作。
認(rèn)識IConfigurationBuilder和IConfiguration
通過上面我們了解到ConfigurationManager可以直接注冊過配置文件就可以直接去操作配置文件里的內(nèi)容,這一步是肯定通過轉(zhuǎn)換得到的,畢竟之前的方式我們是通過IConfigurationBuilder的Build操作得到的IConfiguration的實(shí)例,那么我們就先來看下原始的方式是如何實(shí)現(xiàn)的。這里需要從IConfigurationBuilder的默認(rèn)實(shí)現(xiàn)類ConfigurationBuilder說起,它的實(shí)現(xiàn)很簡單[點(diǎn)擊查看源碼]
public class ConfigurationBuilder : IConfigurationBuilder
{
/// <summary>
/// 添加的數(shù)據(jù)源被存放到了這里
/// </summary>
public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();
public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();
/// <summary>
/// 添加IConfigurationSource數(shù)據(jù)源
/// </summary>
/// <returns></returns>
public IConfigurationBuilder Add(IConfigurationSource source)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
Sources.Add(source);
return this;
}
public IConfigurationRoot Build()
{
//獲取所有添加的IConfigurationSource里的IConfigurationProvider
var providers = new List<IConfigurationProvider>();
foreach (var source in Sources)
{
var provider = source.Build(this);
providers.Add(provider);
}
//用providers去實(shí)例化ConfigurationRoot
return new ConfigurationRoot(providers);
}
}
這里我們來解釋一下,其實(shí)我們注冊配置相關(guān)的時(shí)候比如AddJsonFile()、AddEnvironmentVariables()、AddInMemoryCollection()等等它們其實(shí)都是擴(kuò)展方法,本質(zhì)就是添加IConfigurationSource實(shí)例,而IConfigurationBuilder的Build本質(zhì)操作其實(shí)就是在IConfigurationSource集合中得到IConfigurationProvider集合,因真正從配置讀取到的數(shù)據(jù)都是包含在IConfigurationProvider實(shí)例中的,ConfigurationRoot通過一系列的封裝,讓我們可以更便捷的得到配置里相關(guān)的信息。這就是ConfigurationBuilder的工作方式,也是配置體系的核心原理。
我們既然知道了添加配置的本質(zhì)其實(shí)就是IConfigurationBuilder.Add(IConfigurationSource source)那么我就來看一下ConfigurationManager是如何實(shí)現(xiàn)這一步的。我們知道ConfigurationManager實(shí)現(xiàn)了IConfigurationBuilder接口,所以必然重寫了IConfigurationBuilder的Add方法,找到源碼位置[點(diǎn)擊查看源碼]
private readonly ConfigurationSources _sources = new ConfigurationSources(this); ;
IConfigurationBuilder IConfigurationBuilder.Add(IConfigurationSource source)
{
_sources.Add(source ?? throw new ArgumentNullException(nameof(source)));
return this;
}
這里返回了this也就是當(dāng)前ConfigurationManager實(shí)例是為了可以進(jìn)行鏈?zhǔn)骄幊?,ConfigurationSources這個(gè)類是個(gè)新物種,原來的類叫ConfigurationSource,這里多了個(gè)s表明了這是一個(gè)集合類,我們就來看看它是個(gè)啥操作,找到源碼位置[點(diǎn)擊查看源碼]
/// <summary>
/// 本身是一個(gè)IConfigurationSource集合
/// </summary>
private class ConfigurationSources : IList<IConfigurationSource>
{
private readonly List<IConfigurationSource> _sources = new();
private readonly ConfigurationManager _config;
/// <summary>
/// 因?yàn)槭荂onfigurationManager的內(nèi)部類所以傳遞了當(dāng)前ConfigurationManager實(shí)例
/// </summary>
/// <param name="config"></param>
public ConfigurationSources(ConfigurationManager config)
{
_config = config;
}
/// <summary>
/// 根據(jù)索引獲取其中一個(gè)IConfigurationSource實(shí)例
/// </summary>
/// <returns></returns>
public IConfigurationSource this[int index]
{
get => _sources[index];
set
{
_sources[index] = value;
_config.ReloadSources();
}
}
public int Count => _sources.Count;
public bool IsReadOnly => false;
/// <summary>
/// 這是重點(diǎn)添加配置源
/// </summary>
/// <param name="source"></param>
public void Add(IConfigurationSource source)
{
//給自己的IConfigurationSource集合添加
_sources.Add(source);
//調(diào)用了ConfigurationManager的AddSource方法
_config.AddSource(source);
}
/// <summary>
/// 實(shí)現(xiàn)IList清除操作
/// </summary>
public void Clear()
{
_sources.Clear();
//這里可以看到ConfigurationManager的ReloadSources方法很重要
//通過名字可以看出是刷新配置數(shù)據(jù)用的
_config.ReloadSources();
}
public void Insert(int index, IConfigurationSource source)
{
_sources.Insert(index, source);
_config.ReloadSources();
}
public bool Remove(IConfigurationSource source)
{
var removed = _sources.Remove(source);
_config.ReloadSources();
return removed;
}
public void RemoveAt(int index)
{
_sources.RemoveAt(index);
_config.ReloadSources();
}
//這里省略了實(shí)現(xiàn)了實(shí)現(xiàn)IList接口的其他操作
//ConfigurationSources本身就是IList<IConfigurationSource>
}
正如我們看到的那樣ConfigurationSources本身就是一個(gè)IConfigurationSource的集合,在新的.Net體系中微軟喜歡把集合相關(guān)的操作封裝一個(gè)Collection類,這樣的好處就是讓大家能更清晰的了解它是功能實(shí)現(xiàn)類,而不在用一個(gè)數(shù)據(jù)結(jié)構(gòu)的眼光去看待。通過源碼我們還看到了Add方法里還調(diào)用了ConfigurationManager的AddSource方法,這究竟是一個(gè)什么操作我們來看下[點(diǎn)擊查看源碼]
private readonly object _providerLock = new();
private readonly List<IConfigurationProvider> _providers = new();
private readonly List<IDisposable> _changeTokenRegistrations = new();
private void AddSource(IConfigurationSource source)
{
lock (_providerLock)
{
//在IConfigurationSource中得到IConfigurationProvider實(shí)例
var provider = source.Build(this);
//添加到_providers集合中
//我們提到過從配置源得到的配置都是通過IConfigurationProvider得到的
_providers.Add(provider);
//IConfigurationProvider的Load方法是從配置源中得到配置數(shù)據(jù)加載到程序內(nèi)存中
provider.Load();
//注冊更改令牌操作,使得配置可以進(jìn)行動(dòng)態(tài)刷新加載
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => provider.GetReloadToken(), () => RaiseChanged()));
}
//添加新的配置源要刷新令牌操作
RaiseChanged();
}
private ConfigurationReloadToken _changeToken = new();
private void RaiseChanged()
{
//每次對配置源進(jìn)行更改操作需要得到新的更改令牌實(shí)例,用于可重復(fù)通知配置變更相關(guān)
var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
previousToken.OnReload();
}
從上面的ConfigurationSources方法里我們可以看到動(dòng)態(tài)的針對ConfigurationSources里的ConfigurationSource進(jìn)行更改會(huì)每次都調(diào)用ReloadSources方法,我們來看一下它的實(shí)現(xiàn)[點(diǎn)擊查看源碼]
private readonly object _providerLock = new();
private void ReloadSources()
{
lock (_providerLock)
{
//釋放原有操作
DisposeRegistrationsAndProvidersUnsynchronized();
//清除更改令牌
_changeTokenRegistrations.Clear();
//清除_providers
_providers.Clear();
//重新加載_providers
foreach (var source in _sources)
{
_providers.Add(source.Build(this));
}
//重新加載數(shù)據(jù)添加通知令牌
foreach (var p in _providers)
{
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
}
}
RaiseChanged();
}
這個(gè)方法幾乎是重新清除了原來的操作,然后完全的重新加載一遍數(shù)據(jù),理論上來說是一個(gè)低性能的操作,不建議頻繁使用。還有因?yàn)镃onfigurationManager實(shí)現(xiàn)了IConfigurationBuilder接口所以也必然實(shí)現(xiàn)了它的Build方法少不了,看一下它的實(shí)現(xiàn)[點(diǎn)擊查看源碼]
IConfigurationRoot IConfigurationBuilder.Build() => this;
這波操作真的很真的很騷氣,我即是IConfigurationRoot我也是IConfigurationBuilder,反正操作都是我自己,所以這里你可勁的Build也不影響啥,反正得到的也都是一個(gè)ConfigurationManager實(shí)例。到了這里結(jié)合我們之前了解到的傳統(tǒng)的IConfigurationBuilder和IConfiguration關(guān)系,以及我們上面展示的展示的ConfigurationSources類的實(shí)現(xiàn)和ConfigurationManager的AddSource方法。其實(shí)我們可以發(fā)現(xiàn)我們上面展示的ConfigurationManager類的相關(guān)操作其實(shí)就是實(shí)現(xiàn)了之前ConfigurationBuilder類里的操作。其實(shí)這里微軟可以不用實(shí)現(xiàn)ConfigurationSources類完全基于ConfigurationBuilder也能實(shí)現(xiàn)一套,但是顯然微軟沒這么做,具體想法咱們不得而知,估計(jì)是只想以來抽象,而并不像以來原來的實(shí)現(xiàn)方式吧。
我們上面展示的這一部分的ConfigurationManager代碼,其實(shí)就是替代了原來的ConfigurationBuilder類的功能。
讀取操作
上面我們看到了在ConfigurationManager中關(guān)于以前ConfigurationManager類的實(shí)現(xiàn)。接下來我們看一下讀取相關(guān)的操作,即在這里ConfigurationManager成為了IConfiguration實(shí)例,所以我們先來看下IConfiguration接口的定義[點(diǎn)擊查看源碼]
public interface IConfiguration
{
/// <summary>
/// 通過配置名稱獲取值
/// </summary>
/// <returns></returns>
string this[string key] { get; set; }
/// <summary>
/// 獲取一個(gè)配置節(jié)點(diǎn)
/// </summary>
/// <returns></returns>
IConfigurationSection GetSection(string key);
/// <summary>
/// 獲取所有子節(jié)點(diǎn)
/// </summary>
/// <returns></returns>
IEnumerable<IConfigurationSection> GetChildren();
/// <summary>
/// 刷新數(shù)據(jù)通知
/// </summary>
/// <returns></returns>
IChangeToken GetReloadToken();
}
通過代碼我們看到了IConfiguration的定義,也就是在ConfigurationManager類中必然也實(shí)現(xiàn)也這幾個(gè)操作,首先便是通過索引器直接根據(jù)配置的名稱獲取值得操作[點(diǎn)擊查看源碼]
private readonly object _providerLock = new();
private readonly List<IConfigurationProvider> _providers = new();
/// <summary>
/// 可讀可寫的操作
/// </summary>
/// <returns></returns>
public string this[string key]
{
get
{
lock (_providerLock)
{
//通過在IConfigurationProvider集合中獲取配置值
return ConfigurationRoot.GetConfiguration(_providers, key);
}
}
set
{
lock (_providerLock)
{
//也可以把值放到IConfigurationProvider集合中
ConfigurationRoot.SetConfiguration(_providers, key, value);
}
}
}
其中_providers中的值是我們在AddSource方法中添加進(jìn)來的,這里的本質(zhì)其實(shí)還是針對ConfigurationRoot做了封裝。ConfigurationRoot實(shí)現(xiàn)了IConfigurationRoot接口,IConfigurationRoot實(shí)現(xiàn)了IConfiguration接口。而ConfigurationRoot的GetConfiguration方法和SetConfiguration是最直觀體現(xiàn)ConfigurationRoot本質(zhì)就是IConfigurationProvider包裝的證據(jù)。我們來看一下ConfigurationRoot這兩個(gè)方法的實(shí)現(xiàn)[點(diǎn)擊查看源碼]
internal static string GetConfiguration(IList<IConfigurationProvider> providers, string key)
{
//倒序遍歷providers,因?yàn)镃onfiguration采用的后來者居上的方式,即后注冊的Key會(huì)覆蓋先前注冊的Key
for (int i = providers.Count - 1; i >= 0; i--)
{
IConfigurationProvider provider = providers[i];
//如果找到Key的值就直接返回
if (provider.TryGet(key, out string value))
{
return value;
}
}
return null;
}
internal static void SetConfiguration(IList<IConfigurationProvider> providers, string key, string value)
{
if (providers.Count == 0)
{
throw new InvalidOperationException("");
}
//給每個(gè)provider都Set這個(gè)鍵值,雖然浪費(fèi)了一部分內(nèi)存,但是可以最快的獲取
foreach (IConfigurationProvider provider in providers)
{
provider.Set(key, value);
}
}
關(guān)于GetSection的方法實(shí)現(xiàn),本質(zhì)上是返回ConfigurationSection實(shí)例,ConfigurationSection本身也是實(shí)現(xiàn)了IConfiguration接口,所有關(guān)于配置獲取的操作出口都是面向IConfiguration的。
public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key);
GetChildren方法是獲取配置的所有子節(jié)點(diǎn)的操作,本質(zhì)是返回IConfigurationSection的集合,實(shí)現(xiàn)方式如如下
private readonly object _providerLock = new();
public IEnumerable<IConfigurationSection> GetChildren()
{
lock (_providerLock)
{
//調(diào)用了GetChildrenImplementation方法
return this.GetChildrenImplementation(null).ToList();
}
}
這里調(diào)用了GetChildrenImplementation方法,而GetChildrenImplementation是一個(gè)擴(kuò)展方法,我們來看一下它的實(shí)現(xiàn)[點(diǎn)擊查看源碼]
internal static IEnumerable<IConfigurationSection> GetChildrenImplementation(this IConfigurationRoot root, string path)
{
//在當(dāng)前ConfigurationManager實(shí)例中獲取到所有的IConfigurationProvider實(shí)例
//然后包裝成IConfigurationSection集合
return root.Providers
.Aggregate(Enumerable.Empty<string>(),
(seed, source) => source.GetChildKeys(seed, path))
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(key => root.GetSection(path == null ? key : ConfigurationPath.Combine(path, key)));
}
通過這段代碼再次應(yīng)驗(yàn)了那句話所有獲取配置數(shù)據(jù)都是面向IConfiguration接口的,數(shù)據(jù)本質(zhì)都是來自于IConfigurationProvider讀取配置源中的數(shù)據(jù)。
ConfigurationBuilderProperties
在ConfigurationManager中還包含了一個(gè)Properties屬性,這個(gè)屬性本質(zhì)來源于IConfigurationBuilder。在IConfigurationBuilder中它和IConfigurationSource是平行關(guān)系,IConfigurationSource用于在配置源中獲取數(shù)據(jù),而Properties是在內(nèi)存中獲取數(shù)據(jù),本質(zhì)是一個(gè)字典
private readonly ConfigurationBuilderProperties _properties = new ConfigurationBuilderProperties(this); IDictionary<string, object> IConfigurationBuilder.Properties => _properties;
這里咱們就不細(xì)說這個(gè)具體實(shí)現(xiàn)了,我們知道它本質(zhì)是字典,然后操作都是純內(nèi)存的操作即可,來看一下它的定義[點(diǎn)擊查看源碼]
private class ConfigurationBuilderProperties : IDictionary<string, object>
{
}
基本上許多緩存機(jī)制即內(nèi)存操作都是基于字典做的一部分實(shí)現(xiàn),所以大家對這個(gè)實(shí)現(xiàn)的方式有一定的認(rèn)識即可,即使在配置體系的核心操作ConfigurationProvider中讀取的配置數(shù)據(jù)也是存放在字典中的。這個(gè)可以去ConfigurationProvider類中自行了解一下[點(diǎn)擊查看源碼]
protected IDictionary<string, string> Data { get; set; }
protected ConfigurationProvider()
{
Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
總結(jié)
通過本文我們了解到了.Net6配置體系中的新成員ConfigurationManager,它是一個(gè)新內(nèi)容但不是一個(gè)新技術(shù),因?yàn)樗窃谠械呐渲皿w系中封裝了一個(gè)新的外觀,以簡化原來對配置相關(guān)的操作。原來對配置的操作需要涉及IConfigurationBuilder和IConfiguration兩個(gè)抽象操作,而新的ConfigurationManager只需要一個(gè)類,其本質(zhì)是因?yàn)镃onfigurationManage同時(shí)實(shí)現(xiàn)了IConfigurationBuilder和IConfiguration接口,擁有了他們兩個(gè)體系的能力。整體來說重寫了IConfigurationBuilder的實(shí)現(xiàn)為主,而讀取操作主要還是借助原來的ConfigurationRoot對節(jié)點(diǎn)數(shù)據(jù)的讀取操作。
到此這篇關(guān)于.NET6?ConfigurationManager的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān).NET6?ConfigurationManager實(shí)現(xiàn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Asp.net treeview實(shí)現(xiàn)無限級樹實(shí)現(xiàn)代碼
最近研究了一下treeview,發(fā)現(xiàn)有兩種實(shí)現(xiàn)無限級樹的方法,文字不想多寫,直入主題。2009-09-09
Web.config 和 App.config 的區(qū)別分析
Web.config 和 App.config 的區(qū)別分析,需要的朋友可以參考一下2013-05-05
TrieTree服務(wù)-組件構(gòu)成及其作用介紹
本文將一步步教你配置和使用TrieTree服務(wù),需要的朋友可以參考下2013-01-01
ASP.NET技巧:做個(gè)DataList可分頁的數(shù)據(jù)源
ASP.NET技巧:做個(gè)DataList可分頁的數(shù)據(jù)源...2006-09-09
asp.net實(shí)現(xiàn)的DES加密解密操作示例
這篇文章主要介紹了asp.net實(shí)現(xiàn)的DES加密解密操作,結(jié)合具體實(shí)例形式分析了asp.net實(shí)現(xiàn)DES加密與解密算法的實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-07-07
asp.net 在處理向該請求提供服務(wù)所需的配置文件時(shí)出錯(cuò)
遭遇:“說明: 在處理向該請求提供服務(wù)所需的配置文件時(shí)出錯(cuò)。請檢查下面的特定錯(cuò)誤詳細(xì)信息并適當(dāng)?shù)匦薷呐渲梦募??!卞e(cuò)誤2010-03-03

