NopCommerce使用Autofac實現依賴注入

NopCommerce的依賴注入是用的AutoFac組件,這個組件在nuget能夠獲取,而IOC反轉控制常見的實現手段之一就是DI依賴注入,而依賴注入的方式一般有:接口注入、Setter注入和構造函數注入。html

 

NopCommerce將全部和Autofac注入相關的工做都放到了EngineContext中,在Global.asax的Application_Start函數的第一句代碼便是:web

//initialize engine context
EngineContext.Initialize(false);

從這裏開始EngineContext的初始化工做,初始化時會建立一個新的NopEngine,參數false指定當NopEngine不爲空時是否從新生成一個新的NopEngine。架構

[MethodImpl(MethodImplOptions.Synchronized)]
public static IEngine Initialize(bool forceRecreate)
{
    if (Singleton<IEngine>.Instance == null || forceRecreate)
    {
        var config = ConfigurationManager.GetSection("NopConfig") as NopConfig;
        Debug.WriteLine("Constructing engine " + DateTime.Now);
        Singleton<IEngine>.Instance = CreateEngineInstance(config);
        Debug.WriteLine("Initializing engine " + DateTime.Now);
        Singleton<IEngine>.Instance.Initialize(config);
    }
    return Singleton<IEngine>.Instance;
}

NopEngine使用單例模式,在整個程序運行期間存在一個實例,代碼首先會判斷NopEngine是否爲空,爲空的話則根據web.config中配置的NopConfig節點信息建立一個新的NopEngine實例,而後對該實例進行初始化操做。web.config中的配置信息以下:框架

 <configSections>
    <section name="NopConfig" type="Easy.Core.Configuration.NopConfig, Easy.Core" requirePermission="false" />
  </configSections>
  <NopConfig>
    <DynamicDiscovery Enabled="true" />
    <Engine Type="" />
    <Themes basePath="~/Themes/" />
  </NopConfig>
CreateEngineInstance函數中使用new NopEngine()建立了一個NopEngine實例,在NopEngine的構造函數處對Autofac的容器(Container)做了初始化,以下代碼:
public NopEngine(EventBroker broker, ContainerConfigurer configurer)
{
    var config = ConfigurationManager.GetSection("NopConfig") as NopConfig;
    InitializeContainer(configurer, broker, config);
}
private void InitializeContainer(ContainerConfigurer configurer, EventBroker broker, NopConfig config)
{
    var builder = new ContainerBuilder();
 
    _containerManager = new ContainerManager(builder.Build());
    configurer.Configure(this, _containerManager, broker, config);
}

NopCommerce經過ContainerManager對容器作了一層封裝,方便對其餘類型的IOC框架的擴充和支持。Configure函數完成了全部依賴的注入,同時查找全部實現了IDependencyRegistrar接口的類,並調用其Register方法,註冊內容包括Http context、web helper、controller、data layer、plugin、cache manager、work context、services、settings、event consumers等等。ide

關於ContainerManager/ContainerConfigurer和IDependencyRegistrar是實現IOC的關鍵,下面對這兩個部分作詳細的討論。函數

 

 

IOC和DIpost

IOC中文名被稱做控制反轉(Inversion of Control),DI被稱爲依賴注入(Dependency Injection),可參考Martin Fowler的這篇文章來了解這兩個概念:IoC容器和DependencyInjection模式。使用控制反轉模式開發項目流程是先創建接口,而後再實現類,或許有人不習慣這樣的開發方法,但在規模較大的軟件架構中,這種方法卻能夠有效的下降類之間的互相依賴的狀況,不但能增長架構的彈性,也能有效的下降軟件的複雜度。性能

若是不考慮控制反轉的狀況,採用直接建立類,並直接在應用層調用該類,如此一來,應用層的對象就會與BLL(業務邏輯層)對象高度依賴,這樣的依賴 會致使這兩個類沒法拆開,從而增長了這個類的維護難度,同時致使了單元測試難以進行。爲了解決耦合度問題,從而引入了控制反轉的概念。單元測試

Autofac介紹學習

 Autofac是一款IOC框架,比較於其餘的IOC框架,如Spring.NET、Unity、Castle等,它更顯得輕量級,同時保證了高性能。它具備如下優勢:

  1. 和C#語言聯繫緊密,能夠使用C#語言的不少特性,譬如Lambda表達式等;
  2. 較低的學習曲線,只需瞭解IoC和DI的概念以及在什麼時候須要使用它們便可;
  3. XML配置支持;
  4. 自動裝配;
  5. 與ASP.NET MVC3集成;(Orchard也是使用Autofac實現IOC的)

在MVC3項目中使用Autofac

 在MVC3工程中使用Autofac的最好也是最簡單的方法是使用NuGet來安裝Autofac.Mvc3,安裝完成之後,在Global.asax的Application_Start方法中添加以下代碼:

  1. protected void Application_Start()  
  2. {  
  3.     var builder = new ContainerBuilder();  
  4.     builder.RegisterControllers(typeof(MvcApplication).Assembly);  
  5.     var container = builder.Build();  
  6.     DependencyResolver.SetResolver(new AutofacDependencyResolver(container));  
  7.     // Other MVC setup...  

這樣就開啓了Controller的依賴注入功能。其中的DependencyResolver是一個全局靜態類,MVC3提供了對依賴注入的支 持,SetResolver函數用於設置使用哪一個Resolver(解析器)來進行依賴注入,這裏使用的是Autofac的依賴注入解析器。若是要使用自 己的解析器,必須在這裏使用SetResolver函數設置。

1. 註冊Controller

能夠使用下面的方法對特定的Controller進行註冊:

  1. var builder =  new ContainerBuilder();  
  2. builder.RegisterType<HomeController>().InstancePerRequest();  
  1. 同時可使用Autofac提供的RegisterControllers擴展方法來對程序集中全部的Controller一次性的完成註冊:  
  1. var builder = new ContainerBuilder();  
  2. builder.RegisterControllers(Assembly.GetExecutingAssembly());  

2. 註冊Model Binder

與控制器的註冊相似,模型綁定也能夠再Global.asax.cs中註冊。您能夠經過以下操做完成整個程序集的註冊:

  1. var builder = newContainerBuilder();  
  2. builder.RegisterModelBinders(Assembly.GetExecutingAssembly());  
  3. builder.RegisterModelBinderProvider();  

您也必須記住使用RegisterModelBinderProvider擴展方法來註冊RegisterModelBinderProvider。這個方法用是Autofac對IModelBinderProvider接口的實現

由於RegisterModelBinders擴展方法經過掃描程序集來添加模型綁定的,因此您須要指定IModelBuilder註冊的目標類是什麼類型。

  1. [ModelBinderType(typeof(string))]  
  2. public class StringBinder : IModelBinder  
  3. {  
  4.     public override object BindModel(ControllerContext controllerContext,ModelBindingContext bindingContext)  
  5.     {  
  6.         //do implementation here  
  7.     }  
  8. }  

多行的ModelBuilderTypeAttribute實例能夠添加到須要對個類型註冊的類中。

3. 注入HTTP抽象類

MVC集成的Autofac模塊將會爲HTTP抽象類添加HTTP 請求的生命收起範圍內的註冊。包括依稀抽象類:
  • HttpContextBase
  • HttpRequestBase
  • HttpResponseBase
  • HttpServerUtilityBase
  • HttpSessionStateBase
  • HttpApplicationStateBase
  • HttpBrowserCapabilitiesBase
  • HttpCachePolicyBase
  • VirtualPathProvider
須要 使用上面的抽象應該 使用容器的RegisterModule方法來添加AutofacWebTypesModule
  1. builder.RegisterModule(newAutofacWebTypesModule());  

4. 注入View page

您能夠經過在容器建立以前添加 ViewRegistrationSource 到容器中使屬性 注入來使MVC頁面可用。
  1. builder.RegisterSource(newViewRegistrationSource());  
您的viewpage必須繼承MVC類中用於建立,當 使用Razor試圖引擎時將須要繼承WebViewPage類:
  1. public abstract class CustomViewPage : WebViewPage  
  2. {  
  3.     public IDependencyDependency { get; set; }  
  4. }  
使用的是webform的試圖引擎時,ViewPage,ViewMasterPage和ViewUserControl類都獲得相應的支持。
  1. public abstract class CustomViewPage : ViewPage  
  2. {  
  3.     public IDependencyDependency { get; set; }  
  4. }  
必須確保您實際的試圖頁面繼承了您自定義的基類。在Razor視圖引擎.cshtml中能夠 使用@inherits指令來 實現
  1. @inherits Example.Views.Shared.CustomViewPage  
使用webform時能夠作以下設置
  1. <%@ PageLanguage="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="Example.Views.Shared.CustomViewPage" %>  

5. 對Filter Attribute進行屬性注入

爲過濾器 使用屬性 注入必須在容器建立以前調用RegisterFilterProvider方法,並將其傳到AutofacDependencyResolver
  1. ContainerBuilder builder = new ContainerBuilder();  
  2. builder.RegisterControllers(Assembly.GetExecutingAssembly());  
  3. builder.Register(c => new Logger()).As<ILogger>().InstancePerHttpRequest();  
  4. builder.RegisterFilterProvider();  
  5. IContainer container = builder.Build();  
  6. DependencyResolver.SetResolver(new AutofacDependencyResolver(container));  
而後您就能夠爲您的過濾器添加屬性了,而且
  1. public class CustomActionFilter : ActionFilterAttribute  
  2. {  
  3.     public ILogger Logger { get; set; }  
  4.    
  5.     public override void OnActionExecuting(ActionExecutingContext filterContext)  
  6.     {  
  7.         Logger.Log("OnActionExecuting");  
  8.     }  
  9. }  
下面是相似用戶驗證過濾器的自定義特性
  1. public class CustomAuthorizeAttribute : AuthorizeAttribute  
  2. {  
  3.     public ILogger Logger { get; set; }  
  4.    
  5.     protected override bool AuthorizeCore(HttpContextBase httpContext)  
  6.     {  
  7.         Logger.Log("AuthorizeCore");  
  8.         return true;  
  9.     }  
  10. }  
應用以下:
  1. [CustomActionFilter]  
  2. [CustomAuthorizeAttribute]  
  3. public ActionResult Index()  
  4. {  
  5.     // ...  
  6. }  
  1. 關於Autofac更多的信息,能夠參考autofac在google code上的wiki文檔:http://code.google.com/p/autofac/wiki/Mvc3Integration  

NopCommerce是如何使用Autofac實現依賴注入的?

 NopCommerce將全部和Autofac注入相關的工做都放到了EngineContext中,在Global.asax的Application_Start函數的第一句代碼便是:

  1. //initialize engine context  
  2. EngineContext.Initialize(false);  

從這裏開始EngineContext的初始化工做,初始化時會建立一個新的NopEngine,參數false指定當NopEngine不爲空時是否從新生成一個新的NopEngine。

  1. [MethodImpl(MethodImplOptions.Synchronized)]  
  2. public static IEngine Initialize(bool forceRecreate)  
  3. {  
  4.     if (Singleton<IEngine>.Instance == null || forceRecreate)  
  5.     {  
  6.         var config = ConfigurationManager.GetSection("NopConfig") as NopConfig;  
  7.         Debug.WriteLine("Constructing engine " + DateTime.Now);  
  8.         Singleton<IEngine>.Instance = CreateEngineInstance(config);  
  9.         Debug.WriteLine("Initializing engine " + DateTime.Now);  
  10.         Singleton<IEngine>.Instance.Initialize(config);  
  11.     }  
  12.     return Singleton<IEngine>.Instance;  
  13. }  

NopEngine 使用單例模式,在整個程序運行期間存在一個實例,代碼首先會判斷NopEngine是否爲空,爲空的話則根據web.config中配置的 NopConfig節點信息建立一個新的NopEngine實例,而後對該實例進行初始化操做。web.config中的配置信息以下:

  1.   <configSections>  
  2.     <section name="NopConfig" type="Easy.Core.Configuration.NopConfig, Easy.Core" requirePermission="false" />  
  3.   </configSections>  
  4.   <NopConfig>  
  5.     <DynamicDiscovery Enabled="true" />  
  6.     <Engine Type="" />  
  7.     <Themes basePath="~/Themes/" />  
  8.   </NopConfig>  

CreateEngineInstance函數中使用new NopEngine()建立了一個NopEngine實例,在NopEngine的構造函數處對Autofac的容器(Container)做了初始化,以下代碼:

  1. public NopEngine(EventBroker broker, ContainerConfigurer configurer)  
  2. {  
  3.     var config = ConfigurationManager.GetSection("NopConfig") as NopConfig;  
  4.     InitializeContainer(configurer, broker, config);  
  5. }  
  1. private void InitializeContainer(ContainerConfigurer configurer, EventBroker broker, NopConfig config)  
  2. {  
  3.     var builder = new ContainerBuilder();  
  4.    
  5.     _containerManager = new ContainerManager(builder.Build());  
  6.     configurer.Configure(this, _containerManager, broker, config);  
  7. }  

NopCommerce經過ContainerManager對容器作了一層封裝,方便對其餘類型的IOC框架的擴充和支持。Configure函數完成了全部依賴注入,同時查找全部實現了IDependencyRegistrar接 口的類,並調用其Register方法,註冊內容包括Http context、web helper、controller、data layer、plugin、cache manager、work context、services、settings、event consumers等等。

關於ContainerManager/ContainerConfigurer和IDependencyRegistrar是實現IOC的關鍵,下面對這兩個部分作詳細的討論。

// todo:仍需繼續分析具體實現

ContainerManager/ContainerConfigurer

  1. ContainerManagerContainerManager對依賴注入中使用的容器作了一層封裝,提供了這些函數:  
  • AddComponent/AddComponentInstance/AddComponentWithParameters
  • Resolve/ResolveAll/ResovleUnregistered
  • UpdateContainer

DependencyRegistrar

  • web helper
  • controller
  • data layer
  • plugin
  • cache manager
  • work context
  • services
  • settings
  • event consumers

from:aneasystone ==>http://www.cnblogs.com/aneasystone/archive/2012/08/27/2659176.html

IOC和DI

IOC中文名被稱做控制反轉(Inversion of Control),DI被稱爲依賴注入(Dependency Injection),可參考Martin Fowler的這篇文章來了解這兩個概念:IoC容器和DependencyInjection模式。使用控制反轉模式開發項目流程是先創建接口,而後再實現類,或許有人不習慣這樣的開發方法,但在規模較大的軟件架構中,這種方法卻能夠有效的下降類之間的互相依賴的狀況,不但能增長架構的彈性,也能有效的下降軟件的複雜度。

若是不考慮控制反轉的狀況,採用直接建立類,並直接在應用層調用該類,如此一來,應用層的對象就會與BLL(業務邏輯層)對象高度依賴,這樣的依賴 會致使這兩個類沒法拆開,從而增長了這個類的維護難度,同時致使了單元測試難以進行。爲了解決耦合度問題,從而引入了控制反轉的概念。

Autofac介紹

 Autofac是一款IOC框架,比較於其餘的IOC框架,如Spring.NET、Unity、Castle等,它更顯得輕量級,同時保證了高性能。它具備如下優勢:

  1. 和C#語言聯繫緊密,能夠使用C#語言的不少特性,譬如Lambda表達式等;
  2. 較低的學習曲線,只需瞭解IoC和DI的概念以及在什麼時候須要使用它們便可;
  3. XML配置支持;
  4. 自動裝配;
  5. 與ASP.NET MVC3集成;(Orchard也是使用Autofac實現IOC的)

在MVC3項目中使用Autofac

 在MVC3工程中使用Autofac的最好也是最簡單的方法是使用NuGet來安裝Autofac.Mvc3,安裝完成之後,在Global.asax的Application_Start方法中添加以下代碼:

  1. protected void Application_Start()  
  2. {  
  3.     var builder = new ContainerBuilder();  
  4.     builder.RegisterControllers(typeof(MvcApplication).Assembly);  
  5.     var container = builder.Build();  
  6.     DependencyResolver.SetResolver(new AutofacDependencyResolver(container));  
  7.     // Other MVC setup...  

這樣就開啓了Controller的依賴注入功能。其中的DependencyResolver是一個全局靜態類,MVC3提供了對依賴注入的支 持,SetResolver函數用於設置使用哪一個Resolver(解析器)來進行依賴注入,這裏使用的是Autofac的依賴注入解析器。若是要使用自 己的解析器,必須在這裏使用SetResolver函數設置。

1. 註冊Controller

能夠使用下面的方法對特定的Controller進行註冊:

  1. var builder =  new ContainerBuilder();  
  2. builder.RegisterType<HomeController>().InstancePerRequest();  
  1. 同時可使用Autofac提供的RegisterControllers擴展方法來對程序集中全部的Controller一次性的完成註冊:  
  1. var builder = new ContainerBuilder();  
  2. builder.RegisterControllers(Assembly.GetExecutingAssembly());  

2. 註冊Model Binder

與控制器的註冊相似,模型綁定也能夠再Global.asax.cs中註冊。您能夠經過以下操做完成整個程序集的註冊:

  1. var builder = newContainerBuilder();  
  2. builder.RegisterModelBinders(Assembly.GetExecutingAssembly());  
  3. builder.RegisterModelBinderProvider();  

您也必須記住使用RegisterModelBinderProvider擴展方法來註冊RegisterModelBinderProvider。這個方法用是Autofac對IModelBinderProvider接口的實現

由於RegisterModelBinders擴展方法經過掃描程序集來添加模型綁定的,因此您須要指定IModelBuilder註冊的目標類是什麼類型。

  1. [ModelBinderType(typeof(string))]  
  2. public class StringBinder : IModelBinder  
  3. {  
  4.     public override object BindModel(ControllerContext controllerContext,ModelBindingContext bindingContext)  
  5.     {  
  6.         //do implementation here  
  7.     }  
  8. }  

多行的ModelBuilderTypeAttribute實例能夠添加到須要對個類型註冊的類中。

3. 注入HTTP抽象類

MVC集成的Autofac模塊將會爲HTTP抽象類添加HTTP 請求的生命收起範圍內的註冊。包括依稀抽象類:
  • HttpContextBase
  • HttpRequestBase
  • HttpResponseBase
  • HttpServerUtilityBase
  • HttpSessionStateBase
  • HttpApplicationStateBase
  • HttpBrowserCapabilitiesBase
  • HttpCachePolicyBase
  • VirtualPathProvider
須要 使用上面的抽象應該 使用容器的RegisterModule方法來添加AutofacWebTypesModule
  1. builder.RegisterModule(newAutofacWebTypesModule());  

4. 注入View page

您能夠經過在容器建立以前添加 ViewRegistrationSource 到容器中使屬性 注入來使MVC頁面可用。
  1. builder.RegisterSource(newViewRegistrationSource());  
您的viewpage必須繼承MVC類中用於建立,當 使用Razor試圖引擎時將須要繼承WebViewPage類:
  1. public abstract class CustomViewPage : WebViewPage  
  2. {  
  3.     public IDependencyDependency { get; set; }  
  4. }  
使用的是webform的試圖引擎時,ViewPage,ViewMasterPage和ViewUserControl類都獲得相應的支持。
  1. public abstract class CustomViewPage : ViewPage  
  2. {  
  3.     public IDependencyDependency { get; set; }  
  4. }  
必須確保您實際的試圖頁面繼承了您自定義的基類。在Razor視圖引擎.cshtml中能夠 使用@inherits指令來 實現
  1. @inherits Example.Views.Shared.CustomViewPage  
使用webform時能夠作以下設置
  1. <%@ PageLanguage="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="Example.Views.Shared.CustomViewPage" %>  

5. 對Filter Attribute進行屬性注入

爲過濾器 使用屬性 注入必須在容器建立以前調用RegisterFilterProvider方法,並將其傳到AutofacDependencyResolver
  1. ContainerBuilder builder = new ContainerBuilder();  
  2. builder.RegisterControllers(Assembly.GetExecutingAssembly());  
  3. builder.Register(c => new Logger()).As<ILogger>().InstancePerHttpRequest();  
  4. builder.RegisterFilterProvider();  
  5. IContainer container = builder.Build();  
  6. DependencyResolver.SetResolver(new AutofacDependencyResolver(container));  
而後您就能夠爲您的過濾器添加屬性了,而且
  1. public class CustomActionFilter : ActionFilterAttribute  
  2. {  
  3.     public ILogger Logger { get; set; }  
  4.    
  5.     public override void OnActionExecuting(ActionExecutingContext filterContext)  
  6.     {  
  7.         Logger.Log("OnActionExecuting");  
  8.     }  
  9. }  
下面是相似用戶驗證過濾器的自定義特性
  1. public class CustomAuthorizeAttribute : AuthorizeAttribute  
  2. {  
  3.     public ILogger Logger { get; set; }  
  4.    
  5.     protected override bool AuthorizeCore(HttpContextBase httpContext)  
  6.     {  
  7.         Logger.Log("AuthorizeCore");  
  8.         return true;  
  9.     }  
  10. }  
應用以下:
  1. [CustomActionFilter]  
  2. [CustomAuthorizeAttribute]  
  3. public ActionResult Index()  
  4. {  
  5.     // ...  
  6. }  
  1. 關於Autofac更多的信息,能夠參考autofac在google code上的wiki文檔:http://code.google.com/p/autofac/wiki/Mvc3Integration  

NopCommerce是如何使用Autofac實現依賴注入的?

 NopCommerce將全部和Autofac注入相關的工做都放到了EngineContext中,在Global.asax的Application_Start函數的第一句代碼便是:

  1. //initialize engine context  
  2. EngineContext.Initialize(false);  

從這裏開始EngineContext的初始化工做,初始化時會建立一個新的NopEngine,參數false指定當NopEngine不爲空時是否從新生成一個新的NopEngine。

  1. [MethodImpl(MethodImplOptions.Synchronized)]  
  2. public static IEngine Initialize(bool forceRecreate)  
  3. {  
  4.     if (Singleton<IEngine>.Instance == null || forceRecreate)  
  5.     {  
  6.         var config = ConfigurationManager.GetSection("NopConfig") as NopConfig;  
  7.         Debug.WriteLine("Constructing engine " + DateTime.Now);  
  8.         Singleton<IEngine>.Instance = CreateEngineInstance(config);  
  9.         Debug.WriteLine("Initializing engine " + DateTime.Now);  
  10.         Singleton<IEngine>.Instance.Initialize(config);  
  11.     }  
  12.     return Singleton<IEngine>.Instance;  
  13. }  

NopEngine 使用單例模式,在整個程序運行期間存在一個實例,代碼首先會判斷NopEngine是否爲空,爲空的話則根據web.config中配置的 NopConfig節點信息建立一個新的NopEngine實例,而後對該實例進行初始化操做。web.config中的配置信息以下:

  1.   <configSections>  
  2.     <section name="NopConfig" type="Easy.Core.Configuration.NopConfig, Easy.Core" requirePermission="false" />  
  3.   </configSections>  
  4.   <NopConfig>  
  5.     <DynamicDiscovery Enabled="true" />  
  6.     <Engine Type="" />  
  7.     <Themes basePath="~/Themes/" />  
  8.   </NopConfig>  

CreateEngineInstance函數中使用new NopEngine()建立了一個NopEngine實例,在NopEngine的構造函數處對Autofac的容器(Container)做了初始化,以下代碼:

  1. public NopEngine(EventBroker broker, ContainerConfigurer configurer)  
  2. {  
  3.     var config = ConfigurationManager.GetSection("NopConfig") as NopConfig;  
  4.     InitializeContainer(configurer, broker, config);  
  5. }  
  1. private void InitializeContainer(ContainerConfigurer configurer, EventBroker broker, NopConfig config)  
  2. {  
  3.     var builder = new ContainerBuilder();  
  4.    
  5.     _containerManager = new ContainerManager(builder.Build());  
  6.     configurer.Configure(this, _containerManager, broker, config);  
  7. }  

NopCommerce經過ContainerManager對容器作了一層封裝,方便對其餘類型的IOC框架的擴充和支持。Configure函數完成了全部依賴注入,同時查找全部實現了IDependencyRegistrar接 口的類,並調用其Register方法,註冊內容包括Http context、web helper、controller、data layer、plugin、cache manager、work context、services、settings、event consumers等等。

關於ContainerManager/ContainerConfigurer和IDependencyRegistrar是實現IOC的關鍵,下面對這兩個部分作詳細的討論。

// todo:仍需繼續分析具體實現

ContainerManager/ContainerConfigurer

  1. ContainerManagerContainerManager對依賴注入中使用的容器作了一層封裝,提供了這些函數:  
  • AddComponent/AddComponentInstance/AddComponentWithParameters
  • Resolve/ResolveAll/ResovleUnregistered
  • UpdateContainer

DependencyRegistrar

  • web helper
  • controller
  • data layer
  • plugin
  • cache manager
  • work context
  • services
  • settings
相關文章
相關標籤/搜索