通過“四大行為”對WCF的擴展[實例篇]
為了讓讀者對如何利用相應的行為對WCF進行擴展有個深刻的認識,在這裏我提供一個簡單的實例演示。本實例模擬的場景是這樣的:我們創建一個支持多語言的資源服務,該服務旨在為調用者提供基於某種語言的文本型資源。但是,我們不希望客戶端在每次調用服務的時候都顯式地製定具體的語言,而是根據客戶端服務調用線程表示語言文化的上下文來自動識別所需的語言。[源代碼從這裏下載]
要讓資源服務具有識別語言文化的能夠,我們必須將客戶端服務調用線程當前的語言文化信息(具體來說就是Thread的兩個屬性:CurrentUICulture和CurrentCulture)自動傳遞到服務端。我們具體的實現原理是這樣的:我們將客戶端服務調用線程的CurrentUICulture和CurrentCulture的語言文化代碼保存在出棧消息的SOAP報頭中,並為它們起一個預定義的名稱和命名空間;在服務操作在服務端執行之前,我們根據這個預定義SOAP報頭名稱和命名空間將這兩個語言文化代碼從入棧消息中獲取出來,創建相應的CultureInfo對象並作為服務操作執行線程的CurrentUICulture和CurrentCulture。那麼服務操作在執行的時候,隻需要根據當前線程的語言文化上下文提供相應資源就可以了。接下來,我們就來一步一步地實現這樣一個簡單的擴展。
目錄
步驟一、創建自定義CallContextInitializer:CultureReceiver
步驟二、自定義ClientMessageInspector:CultureSender
步驟三、創建行為:CulturePropagationBehaviorAttribute
步驟四、為CulturePropagationBehaviorAttribute定義配置元素
步驟五、創建實例應用檢驗語言文化的自動傳播
所謂客戶端當前語言文化信息的傳遞,無外乎是客戶端將當前線程的CurrentUICulture和CurrentCulture放到出棧消息中;而服務端將其從入棧消息中取出,並對當前線程的CurrentUICulture和CurrentCulture進行相應的設置。我們先來實現在服務端用於進行語言文化信息獲取的組件,我將其命名為CultureReceiver。
由於CultureReceiver在從入棧消息中獲取表示客戶端線程的CurrentUICulture和CurrentCulture信息的時,需要預先知道相應報頭的名稱和命名空間(命名空間僅僅用於SOAP報頭),為此我們將這些定義成如下一個名稱為CultureMessageHeaderInfo的類。屬性CurrentCultureName,CurrentUICultureName和Namespace分別表示代碼客戶端線程CurrentCulture和CurrentUICulture的報頭名稱。
1: namespace Artech.WcfExtensions.CulturePropagation
2: {
3: internal class CultureMessageHeaderInfo
4: {
5: public string Namespace{ get; set; }
6: public string CurrentCultureName{ get; set; }
7: public string CurrentUICultureName { get; set; }
8: }
9: }
而我們進行語言文化報頭接收以及對服務操作執行線程當前語言文化的設置的CultureReceiver組件被定義成一個實現了接口ICallContextInitializer的CallContextInitializer對象。在介紹WCF服務端運行時框架的時候,我們已經對CallContextInitializer進行了說明。我們說CallContextInitializer的兩個方法BeforeInvoke和AfterInvoke方法分別在操作方法執行前後被調用。
而我們恰好可以通過實現BeforeInvoke方法將存放在入棧消息報頭的表示客戶端線程CurrentCulture和CurrentUICulture的內容取出,並以此創建相應的CultureInfo作為當前線程的CurrentCulture和CurrentUICulture。由於WCF服務端采用線程池的機製處理客戶端請求,線程會被重用,所以我們有必要在操作方法執行之後將當前線程的語言文化設置恢複到之前的狀態,而這恰好可以實現在AfterInvoke方法中。下麵的代碼片斷表示CultureReceiver的全部定義。
1: using System.Globalization;
2: using System.ServiceModel;
3: using System.ServiceModel.Channels;
4: using System.ServiceModel.Dispatcher;
5: using System.Threading;
6: namespace Artech.WcfExtensions.CulturePropagation
7: {
8: internal class CultureReceiver : ICallContextInitializer
9: {
10: public CultureMessageHeaderInfo messageHeaderInfo;
11: public CultureReceiver(CultureMessageHeaderInfo messageHeaderInfo)
12: {
13: this.messageHeaderInfo = messageHeaderInfo;
14: }
15:
16: public void AfterInvoke(object correlationState)
17: {
18: CultureInfo[] cultureInfos = correlationState as CultureInfo[];
19: if (null != cultureInfos)
20: {
21: Thread.CurrentThread.CurrentCulture = cultureInfos[0];
22: Thread.CurrentThread.CurrentUICulture = cultureInfos[1];
23: }
24: }
25:
26: public object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message)
27: {
28: CultureInfo[] originalCulture = new CultureInfo[] { CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture };
29: CultureInfo currentCulture = null;
30: CultureInfo currentUICulture = null;
31: if (message.Headers.FindHeader(this.messageHeaderInfo.CurrentCultureName, this.messageHeaderInfo.Namespace) > -1)
32: {
33: currentCulture = new CultureInfo(message.Headers.GetHeader<string>(this.messageHeaderInfo.CurrentCultureName,
34: this.messageHeaderInfo.Namespace));
35: Thread.CurrentThread.CurrentCulture = currentCulture;
36: }
37: if (message.Headers.FindHeader(this.messageHeaderInfo.CurrentUICultureName, this.messageHeaderInfo.Namespace) > -1)
38: {
39: currentUICulture = new CultureInfo(message.Headers.GetHeader<string>(this.messageHeaderInfo.CurrentUICultureName,
40: this.messageHeaderInfo.Namespace));
41: Thread.CurrentThread.CurrentUICulture = currentUICulture;
42: }
43: return originalCulture;
44: }
45: }
46: }
對於客戶端,我們通過自定義一個ClientMessageInspector利用“消息檢驗”機製將代表當前線程CurrentCulture和CurrentUICulture的語言文化代碼以SOAP報頭的形式植入請求消息中。我們將改自定義的ClientMessageInspector稱為CultureSender。如下麵的代碼所示,上述的關於客戶端線程當前語言文化信息的發送實現在BeforeSendRequest方法中。
1: using System.Globalization;
2: using System.ServiceModel;
3: using System.ServiceModel.Channels;
4: using System.ServiceModel.Dispatcher;
5: namespace Artech.WcfExtensions.CulturePropagation
6: {
7: internal class CultureSender: IClientMessageInspector
8: {
9: private CultureMessageHeaderInfo messageHeaderInfo;
10:
11: public CultureSender(CultureMessageHeaderInfo messageHeaderInfo)
12: {
13: this.messageHeaderInfo = messageHeaderInfo;
14: }
15:
16: public void AfterReceiveReply(ref Message reply, object correlationState) { }
17: public object BeforeSendRequest(ref Message request, IClientChannel channel)
18: {
19: request.Headers.Add(MessageHeader.CreateHeader(this.messageHeaderInfo.CurrentCultureName, this.messageHeaderInfo.Namespace, CultureInfo.CurrentCulture.Name));
20: request.Headers.Add(MessageHeader.CreateHeader(this.messageHeaderInfo.CurrentUICultureName, this.messageHeaderInfo.Namespace, CultureInfo.CurrentUICulture.Name));
21: return null;
22: }
23: }
24: }
到目前為止,真正實現語言文化信息從客戶端到服務端傳播的自定義CallContextInitialier(CultureReceiver)和ClientMessageInspector(CultureSender)都已經創建好了。我們目前需要做的是通過定義相應的行為將這兩個自定義組件分別應用到WCF的服務端和客戶端運行時框架中去。具體來說,我們需要創建CultureReceiver對象並將其添加到相應DispatchOperation的CallContextInitializer列表之中,創建CultureSender對象並將其添加到ClientRuntime的MessageInspector列表之中。
以下定義的CulturePropagationBehaviorAttribute就是這樣一個行為。從下麵給出的代碼中我們可以看到,CulturePropagationBehaviorAttribute實現了三個行為接口(IServiceBehavior, IEndpointBehavior, IContractBehavior),所以它既是一個服務行為,同時也是一個終結點行為和契約行為。我們自定義的CultureReceiver和CultureSender分別通過ApplyDispatchBehavior和ApplyClientBehavior方法被應用到服務端和客戶端運行時框架。
1: using System;
2: using System.Collections.ObjectModel;
3: using System.ServiceModel;
4: using System.ServiceModel.Channels;
5: using System.ServiceModel.Description;
6: using System.ServiceModel.Dispatcher;
7: namespace Artech.WcfExtensions.CulturePropagation
8: {
9: public class CulturePropagationBehaviorAttribute: Attribute, IServiceBehavior,
10: IEndpointBehavior, IContractBehavior
11: {
12: private CultureMessageHeaderInfo messageHeaderInfo;
13:
14: public const string DefaultNamespace = "https://www.artech.com/culturepropagation";
15: public const string DefaultCurrentCultureName = "CurrentCultureName";
16: public const string DefaultCurrentUICultureName = "CurrentUICultureName";
17:
18: public string Namespace
19: {
20: get{return messageHeaderInfo.Namespace;}
21: set{messageHeaderInfo.Namespace = value;}
22: }
23: public string CurrentCultureName
24: {
25: get { return messageHeaderInfo.CurrentCultureName; }
26: set { messageHeaderInfo.CurrentCultureName = value; }
27: }
28: public string CurrentUICultureName
29: {
30: get { return messageHeaderInfo.CurrentUICultureName; }
31: set { messageHeaderInfo.CurrentUICultureName = value; }
32: }
33:
34: public CulturePropagationBehaviorAttribute()
35: {
36: messageHeaderInfo = new CultureMessageHeaderInfo
37: {
38: Namespace = DefaultNamespace,
39: CurrentCultureName = DefaultCurrentCultureName,
40: CurrentUICultureName = DefaultCurrentUICultureName
41: };
42: }
43:
44: //IServiceBehavior
45: public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase,
46: Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) {}
47: public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
48: {
49: foreach (ChannelDispatcher channelDispatcher in serviceHostBase.ChannelDispatchers)
50: {
51: foreach (EndpointDispatcher endpoint in channelDispatcher.Endpoints)
52: {
53: foreach (DispatchOperation operation in endpoint.DispatchRuntime.Operations)
54: {
55: operation.CallContextInitializers.Add(new CultureReceiver(messageHeaderInfo));
56: }
57: }
58: }
59: }
60: public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { }
61:
62: //IEndpointBehavior
63: public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }
64: public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
65: {
66: clientRuntime.MessageInspectors.Add(new CultureSender(messageHeaderInfo));
67: }
68: public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
69: {
70: foreach (DispatchOperation operation in endpointDispatcher.DispatchRuntime.Operations)
71: {
72: operation.CallContextInitializers.Add(new CultureReceiver(messageHeaderInfo));
73: }
74: }
75: public void Validate(ServiceEndpoint endpoint) { }
76:
77: //IContractBehavior
78: public void AddBindingParameters(ContractDescription contractDescription,
79: ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }
80: public void ApplyClientBehavior(ContractDescription contractDescription,
81: ServiceEndpoint endpoint, ClientRuntime clientRuntime)
82: {
83: clientRuntime.MessageInspectors.Add(new CultureSender(messageHeaderInfo));
84: }
85: public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint,
86: DispatchRuntime dispatchRuntime)
87: {
88: foreach (DispatchOperation operation in dispatchRuntime.Operations)
89: {
90: operation.CallContextInitializers.Add(new CultureReceiver(messageHeaderInfo));
91: }
92: }
93: public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint) { }
94: }
95: }
由於作為服務行為和契約行為的CulturePropagationBehaviorAttribute又是一個自定義特性,所以我們可以直接將其應用到服務契約接口(作為契約行為)或者服務類型(作為服務行為)上。在應用該行為特性時,你可以設置CurrentCultureName、CurrentUICultureName和Namespace屬性改變封裝客戶端線程CurrentCulture和CurrentUICulture的SOAP報頭的名稱和命名空間。如果此三個屬性沒有經過顯式設置,它們具有默認值。
作為契約行為:
1: [ServiceContract]
2: [CulturePropagationBehavior(CurrentCultureName = "culture", CurrentUICultureName = "uiCulture")]
3: public interface IResourceService
4: {
5: [OperationContract]
6: string GetString(string key);
7: }
作為服務行為:
1: [CulturePropagationBehavior(CurrentCultureName = "culture", CurrentUICultureName = "uiCulture")]
2: public class ResourceService : IResourceService
3: {
4: public string GetString(string key)
5: {
6: //省略實現
7: }
8: }
除了通過編程的方式應用行為之外,對於WCF四種類型的行為,契約行為和操作行為隻能通過以自定義特性的方式以聲明的方式分別應用到服務契約接口(或者類)和操作契約方法或者操作實現方法上。終結點行為隻能通過配置的方式應用到對應的終結點。而服務行為,則可以同時采用聲明和配置的方式應用到目標服務上麵。
由於我們定義的CulturePropagationBehaviorAttribute既是服務行為,也是終結點行為,為了實現以配置的方式來使用該行為,我們需要為之創建一個配置元素的類。作為服務行為或者終結點行為的配置元素類均繼承自抽象類BehaviorExtensionElement。
在這裏我們創建了如下一個CulturePropagationBehaviorElement類來定義CulturePropagationBehaviorAttribute的配置元素。在BehaviorExtensionElement中,我們定義了三個可選(IsRequired = false)的配置屬性CurrentCultureName、CurrentUICultureName和Namespace分別代表用於封裝客戶端線程CurrentCulture和CurrentUICulture的SOAP報頭名稱和命名空間。
1: using System;
2: using System.Configuration;
3: using System.ServiceModel.Configuration;
4: namespace Artech.WcfExtensions.CulturePropagation
5: {
6: public class CulturePropagationBehaviorElement: BehaviorExtensionElement
7: {
8: [ConfigurationProperty("namespace",IsRequired =false,
9: DefaultValue = CulturePropagationBehaviorAttribute.DefaultNamespace)]
10: public string Namespace
11: {
12: get { return (string)this["namespace"]; }
13: set { this["namespace"] = value; }
14: }
15: [ConfigurationProperty("currentCultureName", IsRequired = false,
16: DefaultValue = CulturePropagationBehaviorAttribute.DefaultCurrentCultureName)]
17: public string CurrentCultureName
18: {
19: get { return (string)this["currentCultureName"]; }
20: set { this["currentCultureName"] = value; }
21: }
22: [ConfigurationProperty("currentUICultureName", IsRequired = false,
23: DefaultValue = CulturePropagationBehaviorAttribute.DefaultCurrentUICultureName)]
24: public string CurrentUICultureName
25: {
26: get { return (string)this["currentUICultureName"]; }
27: set { this["currentUICultureName"] = value; }
28: }
29: public override Type BehaviorType
30: {
31: get { return typeof(CulturePropagationBehaviorAttribute); }
32: }
33: protected override object CreateBehavior()
34: {
35: return new CulturePropagationBehaviorAttribute
36: {
37: Namespace = this.Namespace,
38: CurrentCultureName = this.CurrentCultureName,
39: CurrentUICultureName = this.CurrentUICultureName
40: };
41: }
42: }
43: }
下麵的XML片斷反映了如何將CulturePropagationBehaviorAttribute作為服務行為以配置的方式應用到目標服務上。首先,我們需要將基於服務行為的配置元素類型以行為或者的形式定義在<extensions>/<behaviorExtensions>結點下,並給它一個名稱(在這裏將我們定義的擴展起名為culturePropagation)。在配置服務行為的時候,我們隻需要在行為配置節點中添加以行為擴展名為元素名的XML結點,並對定義在配置元素類型中的配置屬性進行相應的設置即可。在本例中,我們定義了一個名稱為defaultSvcBehavior的服務行為。該行為結點的字結點<culturePropagation>代表我們自定義的CulturePropagationBehaviorAttribute行為。在<culturePropagation>結點下,我們對namespace、currentCultureName和currentUICultureName作了顯式設置(由於三個配置屬性可選的,如果沒有對它們進行顯式設置,它們將會具有一個默認值)。最終這個名稱為defaultSvcBehavior的服務行為被應用到了ResourceService服務上(behaviorConfiguration="defaultSvcBehavior")。
1: <configuration>
2: <system.serviceModel>
3: <services>
4: <service behaviorConfiguration="defaultSvcBehavior"
5: name="Artech.WcfServices.Servicies.ResourceService">
6: <endpoint address = “http://127.0.0.1:3721/resourceservice”
7: binding="ws2007HttpBinding"
8: contract="Artech.WcfServices.Contracts.IResourceService" />
9: </service>
10: </services>
11: <behaviors>
12: <serviceBehaviors>
13: <behavior name="defaultSvcBehavior">
14: <culturePropagation
15: namespace="https://www.artech.com/"
16: currentCultureName="cultureName"
17: currentUICultureName="uiCultureName"/>
18: </behavior>
19: </serviceBehaviors>
20: </behaviors>
21: <extensions>
22: <behaviorExtensions>
23: <add name="culturePropagation"
24: type="Artech.WcfExtensions.CulturePropagation.CulturePropagationBehaviorElement, Artech.WcfExtensions.Lib" />
25: </behaviorExtensions>
26: </extensions>
27: </system.serviceModel>
28: </configuration>
如果你需要將CulturePropagationBehaviorAttribute以終結點行為的方式應用到服務的某個終結點上,配置方式與此類似。首先,你同樣需要將代表行為配置元素的類型名稱定義成行為擴展。然後將代表該行為配置的XML結點添加到終結點配置節點即可。在下麵這段配置中,ResourceService服務具有唯一的終結點,該終結點應用了一個名稱為defaultEndpointBehavior的終結點行為。而該種節點行為包含了CulturePropagationBehaviorAttribute的相關配置。
1: <configuration>
2: <system.serviceModel>
3: <services>
4: <service name="Artech.WcfServices.Servicies.ResourceService">
5: <endpoint address = "https://127.0.0.1:3721/resourceservice"
6: binding = "ws2007HttpBinding"
7: contract ="Artech.WcfServices.Contracts.IResourceService"
8: behaviorConfiguration="defaultEndpointBehavior"/>
9: </service>
10: </services>
11: <behaviors>
12: <endpointBehaviors>
13: <behavior name="defaultEndpointBehavior">
14: <culturePropagation
15: Namespace ="https://www.artech.com/"
16: currentCultureName ="cultureName"
17: currentUICultureName="uiCultureName"/>
18: </behavior>
19: </endpointBehaviors>
20: </behaviors>
21: <extensions>
22: <behaviorExtensions>
23: <add name="culturePropagation" type="Artech.WcfExtensions.CulturePropagation.CulturePropagationBehaviorElement, Artech.WcfExtensions.Lib" />
24: </behaviorExtensions>
25: </extensions>
26: </system.serviceModel>
27: </configuration>
到目前為止,關於實現語言文化從客戶端自動傳播到服務端的所有擴展實現均已完成。為了檢驗我們自定義的行為CulturePropagationBehaviorAttribute是否真的能夠實現這個目標,我們需要通過建立一個簡單的WCF應用程序來檢驗。
我們采用之前介紹過的文本型資源提供服務,為此我們創建了如下一個簡單的代表服務契約接口IResourceService,該服務契約具有一個唯一的操作契約方法GetString用於獲取基於給定的鍵值得到的對應的文本型資源的內容。需要注意的是,我們定義的CulturePropagationBehaviorAttribute以契約行為的形式應用到IResourceService接口之上。
1: using System.ServiceModel;
2: using Artech.WcfExtensions.CulturePropagation;
3: namespace Artech.WcfServices.Contracts
4: {
5: [ServiceContract(Namespace = "https://www.artech.com/")]
6: [CulturePropagationBehavior]
7: public interface IResourceService
8: {
9: [OperationContract]
10: string GetString(string key);
11: }
12: }
為了提供對於多語言的支持,我們將資源文本的內容定義在資源文件中。為此我們在定義服務類型的項目中添加了如下圖所示的兩個資源文件。其中Resources.resx代表語言文化中性的資源文件,而Resources.zh-CN.resx則代表基於中國(大陸)簡體中文的資源文件。兩個資源文件定義了英文和中文作為內容的兩個文本資源條目HappyNewYear和MerryChristmas。
在默認的情況下,添加語言文化中性的資源文件會自動生成方便訪問資源條目的代碼。在服務類型的GetString方法中,我就直接使用定義在自動生成的Resources類的靜態屬性ResourceManager(相應類型為System.Resources.ResourceManager)來獲取給定鍵值的相應文本資源的內容。
1: using Artech.WcfServices.Contracts;
2: using Artech.WcfServices.Servicies.Properties;
3: namespace Artech.WcfServices.Servicies
4: {
5: public class ResourceService : IResourceService
6: {
7: public string GetString(string key)
8: {
9: return Resources.ResourceManager.GetString(key);
10: }
11: }
12: }
然後,服務ResourceService以控製台應用作為宿主進行簡單的自我寄宿。下麵是服務端配置和客戶端配置,由於我們自定義的CulturePropagationBehaviorAttribute是以聲明的方式作為契約行為應用到契約接口上,所以配置中並不包含相關的內容。
服務寄宿端配置:
1: <configuration>
2: <system.serviceModel>
3: <services>
4: <service name="Artech.WcfServices.Servicies.ResourceService">
5: <endpoint address = "https://127.0.0.1:3721/resourceservice"
6: binding ="ws2007HttpBinding"
7: contract ="Artech.WcfServices.Contracts.IResourceService"/>
8: </service>
9: </services>
10: </system.serviceModel>
11: </configuration>
客戶端配置:
1: <configuration>
2: <system.serviceModel>
3: <client>
4: <endpoint name ="resourceservice"
5: address = "https://127.0.0.1:3721/resourceservice"
6: binding ="ws2007HttpBinding"
7: contract ="Artech.WcfServices.Contracts.IResourceService"/>
8: </client>
9: </system.serviceModel>
10: </configuration>
客戶端同樣是一個控製台應用,下麵是進行服務調用的代碼。從中我們可以看到,我們一共進行了四次針對GetString操作的服務調用,在調用之前我們對當前線程的CurrentUICulture(它決定了語言的種類和對資源文件的選擇)。前麵兩次和後麵兩次是在CurrentUICulture為en-US和zh-CN情況下進行調用的。從輸出結果我們可以清晰地看到:客戶端得到的資源文本的語言正好是和當前線程的CurrentUICulture一致的,而這正是應用在契約接口上CulturePropagationBehaviorAttribute特性所致。
1: using System;
2: using System.ServiceModel;
3: using System.Threading;
4: using Artech.WcfServices.Contracts;
5: namespace Client
6: {
7: class Program
8: {
9: static void Main(string[] args)
10: {
11: using(ChannelFactory<IResourceService> channelFactory = new ChannelFactory<IResourceService>("resourceservice"))
12: {
13: IResourceService proxy = channelFactory.CreateChannel();
14: Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US");
15: Console.WriteLine(proxy.GetString("HappyNewYear"));
16: Console.WriteLine(proxy.GetString("MerryChristmas")+"\n");
17:
18: Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("zh-CN");
19: Console.WriteLine(proxy.GetString("HappyNewYear"));
20: Console.WriteLine(proxy.GetString("MerryChristmas"));
21: }
22: Console.Read();
23: }
24: }
25: }
輸出結果:
1: Happy New Year!
2: Merry Christmas!
3:
4: 新年快樂!
5: 聖誕快樂!
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-26 16:04:29