change set:46759
download:http://oxite.codeplex.com/SourceControl/ListDownloadableCommits.aspx
Web應用程式的初始化我覺得應該分兩類,一類是系統級的初始化,另一類是應用程式級的初始化。兩者也有交叉的部分,如將會談到的Application_Start 和 Application_End ,正是利用其在系統級的特殊性來完成應用程式級的初始化工作。關於系統級的初始化,MSDN上有簡要的描述:《ASP.Net應用程式生命週期概述》http://msdn.microsoft.com/zh-cn/library/ms178473(VS.80).aspx 。本篇主要分析Oxite在配置級的初始化工作。
一、對ASP.Net應用程式生命週期的理解
摘要《ASP.NET 應用程式生命週期概述》中的幾段話:
A:
第一次在應用程式中請求 ASP.NET 頁或進程時,將建立 HttpApplication 的一個新執行個體。不過,為了儘可能提高效能,可對多個請求重複使用 HttpApplication 執行個體。
理解:也就是說,在應用程式定義域中將執行個體化多個HttpApplication對象。每一次請求都將為該請求分配一個HttpApplication對象。而多個請求可能會重複使用同一個HttpApplication對象。但不會存在並發的情況。
B:
Application_Start 和 Application_End 方法是不表示 HttpApplication 事件的特殊方法。在應用程式定義域的生命週期期間,ASP.NET 僅調用這些方法一次,而不是對每個 HttpApplication 執行個體都調用一次。
請求 ASP.NET 應用程式中第一個資源(如頁)時調用。在應用程式的生命週期期間僅調用一次 Application_Start 方法。可以使用此方法執行啟動任務,如將資料載入到緩衝中以及初始化靜態值。
在應用程式啟動期間應僅設定待用資料。由於執行個體資料僅可由建立的 HttpApplication 類的第一個執行個體使用,所以請勿設定任何執行個體資料。
理解:Application_Start 是在應用程式發生第一次請求,建立第一個HttpApplication對象時發生。(在建立了所有模組之後,對 HttpApplication 類的每個執行個體都調用一次Application_Init方法)
C:
在應用程式的生命週期期間,應用程式會引發可處理的事件並調用可重寫的特定方法。若要處理應用程式事件或方法,可以在應用程式根目錄中建立一個名為 Global.asax 的檔案。
如果建立了 Global.asax 檔案,ASP.NET 會將其編譯為從 HttpApplication 類派生的類,然後使用該衍生類別表示應用程式。
理解:我們建立Global.asax 目的是“替換”原來的HttpApplication 類,而為每一次請求分配的將是Global.asax類的執行個體。在Global.asax中,我們可以捕獲相關事件或重寫如Application_Start 和 Application_End 之類的特殊方法。
二、OixteSite項目中Global.asax檔案中的Application_Start和Application_End方法
查看OxiteSite項目的Global.asax檔案,發現其實現代碼在Oxite項目的OxiteApplication類中。在Application_Start方法中對OxiteSite進行了初始化工作。
Application_Start方法具體做了哪些事呢?
一是設定依賴注入容器並將之存入應用程式狀態中(HttpApplicationState)
二是根據配置載入模組
protected void Application_Start()
{
Application["container"] = setupContainer();
Application["bootStrappersLoaded"] = false;
load();
}
Oxite中使用的依賴注入容器為Unity(詳見Enterprise Library 4.0以上版本)。
setupContainer方法設定注入容器並返回一個UnityContainer對象,儲存為Application["container"]。稍後將仔細分析該方法。
Application["bootStrappersLoaded"]用於標認初始化是否完成。
load()方法調用靜態方法Load(HttpContextBase context)。作用是根據配置載入指定模組(Module)。
Application_End方法調用unload()方法。在應用程式結束時,完成某些模組的清理工作。
三、setupContainer方法
預備知識:Unity (IOC/DI)、自訂web.config配置結點
通過setupContainer方法的方法名不難看出是用於設定依賴注入容器的。
在setupContainer方法中,首先定義一個IUnityContainer變數parentContainer:
IUnityContainer parentContainer = new UnityContainer();
首先,將幾個基礎對象註冊為單例:
parentContainer
.RegisterInstance((OxiteConfigurationSection)ConfigurationManager.GetSection("oxite"))
.RegisterInstance(new AppSettingsHelper(ConfigurationManager.AppSettings))
.RegisterInstance(RouteTable.Routes)
.RegisterInstance(System.Web.Mvc.ModelBinders.Binders)
.RegisterInstance(ViewEngines.Engines)
.RegisterInstance(HostingEnvironment.VirtualPathProvider);
OxiteConfigurationSection類,自訂配置節點。其定義位與Oxite.Configuration命名空間下。是Oxite中實現模組化的設定檔。配置的結點單獨放在OxiteSite項目下的oxite.config檔案中。
AppSettingsHelper 類對ConfigurationManager.AppSettings 進行封裝, 提供幾個讀取方法GetInt32、GetString等,用於讀取web.config檔案中的appSettings節點下的值。其實完全可以將這幾個讀取方法放入NameValueCollectionExtensions類(Oxite.Extensions命名空間下)。不過後來想了想,這裡用AppSettingsHelper命名其實也可以明確該類的目的就是為了操作AppSettings結點。
RouteTable.Routes靜態屬性返回一個RouteCollection靜態對象。RouteCollection類在System.Web.Routing程式集中定義。用於儲存URL路由設定。 注入容器的目的是為了單元測試。
ModelBinders.Binders靜態屬性返回一個ModelBinderDictionary靜態對象。用於處理資料繫結相關操作(擷取表單、查詢資料並轉換;產生URL路徑)。
ViewEngines.Engines靜態屬性返回一個ViewEngineCollection靜態對象。用於視圖引擎方面。
HostingEnvironment.VirtualPathProvider靜態屬性返回一個VirtualPathProvider靜態對象。個人猜測可能會用在自訂ViewEngine中,不過目前Oxite版本中好像還沒地方用,注釋掉也沒地方報錯。
RouteTable.Routes、ModelBinders.Binders和ViewEngines.Engines是ASP.NET MVC底層比較基礎性的屬性或對象。值得花時間單獨去學習。
接著,將web.config中的connectionStrings和自訂節點“oxite”(oxite.config檔案)下的connectionStrings註冊為單件。
foreach (ConnectionStringSettings connectionString in ConfigurationManager.ConnectionStrings)
parentContainer.RegisterInstance(connectionString.Name, connectionString.ConnectionString);
foreach (ConnectionStringSettings connectionString in parentContainer.Resolve<OxiteConfigurationSection>().ConnectionStrings)
parentContainer.RegisterInstance(connectionString.Name, connectionString.ConnectionString);
疑問1:在運行時,網站不重啟的情況下,如果oxite結點下的connectionStrings改變後,要怎樣才能更新到依賴注入容器?這裡的處理似乎欠妥。 後來我到Oxite.codeplex.com去問了,Oxite項目組的ErikPorter說目前得重啟網站才行。希望他們儘快修正,不然所謂的模組熱插拔會大打折扣。
接著看setupContainer方法:
parentContainer
.RegisterInstance<IBootStrapperTask>("LoadModules", new LoadModules(parentContainer))
.RegisterInstance<IBootStrapperTask>("LoadBackgroundServices", new LoadBackgroundServices(parentContainer));
LoadModules類和LoadBackgroundServices類位於Oxite.BootStrapperTasks命名空間。從類命名上看,一個是和載入模組相關的,另一個是和載入後台服務相關的,具體是什麼得往後細看了。兩者都實現了Oxite.Infrastructure命名空間下的IBootStrapperTask介面。IBootStrapperTask就兩個方法:Execute和Clearup。IBootStrapperTask我覺得可以直譯為引導程式介面,其執行個體可以稱為引導程式。
在這裡我們只需要知道,Modules執行個體和LoadBackgroundSercies執行個體分別註冊為單件。
接著看setupContainer方法中將一些類型也註冊到依賴注入容器中,除了幾個自訂生命週期的類型外,其他的都只是簡單的映射,這裡就不多說了。
在setupContainer結束傳回值之前,會將web.config檔案中Unity配置結點註冊入容器中。如果配置結點和我們寫入程式碼中的設定重複,則會覆蓋寫入程式碼中的配置。這一特性非常有用,它允許我們在使用程式的預設配置的同時,又提供了一個介面以供我們替換。詳情可以查看相關Unity方面的資料。
四、靜態方法Load
Application_Start中調用私人load方法,load將請求上下封裝成HttpContextBase對象作為參數,調用靜態方法Load。
Load方法雖為靜態,但其接受一個HttpContextBase型參數,從而保證了其安全執行緒。
在靜態方法Load中將會從依賴注入容器中將實現了IBootStrapperTask介面的類的執行個體(引導程式)從依賴注入容器中提取出來,並執行其Execute方法,具體過程如下分析。
通過對setupContainer的分析,我們知道在Load方法中,tasks集合中會有兩個對象:LoadModules類和LoadBackgroundServices類的執行個體。
另外,Application["bootStrapperState"]可能是預留下來(也有可能是遺留下來的),暫時沒有明確其具體的目的(可以自己想想)。
如果Application["bootStrappersLoaded"]為true,表示曾經載入過,則先逐個進行清理工作(Cleanup)。
然後在逐個執行其引導程式的Execute方法。完成後將Application["bootStrappersLoaded"]設為true表示載入完畢。
public static void Load(HttpContextBase context)
{
IEnumerable<IBootStrapperTask> tasks = ((IUnityContainer)context.Application["container"]).ResolveAll<IBootStrapperTask>();
bool bootStrappersLoaded = (bool)context.Application["bootStrappersLoaded"];
IDictionary<string, object> state = (IDictionary<string, object>)context.Application["bootStrapperState"];
if (state == null)
{
context.Application["bootStrapperState"] = state = new Dictionary<string, object>();
}
// If the tasks have been executed previously, call Cleanup on them to rollback any changes
// they caused.
if (bootStrappersLoaded)
{
foreach (IBootStrapperTask task in tasks)
{
task.Cleanup(state);
}
}
foreach (IBootStrapperTask task in tasks)
{
task.Execute(state);
}
context.Application["bootStrappersLoaded"] = true;
}
Oxite的初始化有很大一部分在IBootStrapperTask介面的兩個實作類別中,通過Execute方法來完成的。
五、LoadModules類
打一個不十分恰當的比喻,Oxite就象一個Windows作業系統,在系統啟動時會執行一些引導程式。有一個引導程式根據註冊表的配置,運行一些應用程式,比如殺毒軟體、防火牆等等;另一個引導程式根據“啟動”菜單中儲存的連結,運行另外一些應用程式,如QQ。(Windows引導程式可能不用兩個來完成這兩步操作)
在這裡,LoadModules可以看成是通過註冊表擷取開機啟動程式並啟動的引導程式;LoadBackgroundServices可以看成是通過“啟動”菜單中儲存的連結來啟動程式的引導程式。
Windows平台下的程式能夠實現開機啟動,前提條件它得是Windows程式。
我們換成Oxite模組的角度上考慮,他們都應該實現了某個或某些介面。的確,Oxite中的模組都實現了IOxiteModule介面。
而Oxite中模組,可以看成需要開機啟動並執行程式。
Oxite可以看成是由一個個模組(Module)組成的,而模組指實現了IOxiteModule介面的類。
(圖1:從模組的角度分析Oxite解決方案結構)
AspNetCache、Core、Membership、Oxite.Blogs、Oxite.CMS等都稱之為模組。
LoadModules類實現於介面IBootStrapperTask,其目的正是用於載入模組(Module)。
public void Execute(IDictionary<string, object> state)
{
OxiteConfigurationSection config = container.Resolve<OxiteConfigurationSection>();
IModulesLoaded modulesLoaded = this.container.Resolve<IModulesLoaded>();
RouteCollection routes = this.container.Resolve<RouteCollection>();
IFilterRegistry filterRegistry = this.container.Resolve<FilterRegistry>();
ModelBinderDictionary modelBinders = this.container.Resolve<ModelBinderDictionary>();
filterRegistry.Clear();
modelBinders.Clear();
//todo: (nheskew) get plugin routes registered on load in the right order instead of just clearing the routes before module init
routes.Clear();
foreach (OxiteModuleConfigurationElement module in config.Modules)
{
IOxiteModule moduleInstance = modulesLoaded.Load(config, module);
if (moduleInstance != null)
{
moduleInstance.RegisterWithContainer();
moduleInstance.Initialize();
moduleInstance.RegisterFilters(filterRegistry);
moduleInstance.RegisterModelBinders(modelBinders);
this.container.RegisterInstance(modulesLoaded);
}
}
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.LoadFromModules(modulesLoaded);
routes.LoadCatchAllFromModules(modulesLoaded);
container.RegisterInstance(filterRegistry);
}
首先擷取oxite.config設定檔。Oxite配置結點在setupContainer方法的分析中提到過,他是Oxite模組化的配置。它相當與註冊表,用於儲存需要開機啟動的程式列表。
OxiteConfigurationSection config = container.Resolve<OxiteConfigurationSection>();
接著擷取IModulesLoaded對象(在setupContainer方法中,IModulesLoaded映射為ModulesLoaded類)。
IModulesLoaded modulesLoaded = this.container.Resolve<IModulesLoaded>();
ModulesLoaded的作用是儲存“已經載入的模組”。之後會註冊為單件。
接著擷取RouteCollection對象,即RouteTable.Routes。在setupContainer方法被註冊為單件。
RouteCollection routes = this.container.Resolve<RouteCollection>();
接著擷取IFilterRegistry執行個體。FilterRegistry是和Filter相關的(ActionFilter、ResultFilter等),可能需要單獨的篇幅來分析。
IFilterRegistry filterRegistry = this.container.Resolve<FilterRegistry>();
這裡只是簡單的執行個體化,因為IFilterRegistry 並沒有在依賴注如容器中註冊過。不過在最後,會將IFilterRegistry執行個體註冊為單件。
接著擷取ModelBinderDictionary執行個體。即System.Web.Mvc.ModelBinders.Binders,在setupContainer方法被註冊為單件。
ModelBinderDictionary modelBinders = this.container.Resolve<ModelBinderDictionary>();
本質上FilterRegistry、ModelBinderDictionary、RouteCollection都是集合類。接下來將filterRegistry、modelBinders、routes清空。
接下來是一個foreach迴圈。ModulesLoaded類執行個體modulesLoaded的Load方法將Module進行執行個體化,返回IOxiteModule對象。
foreach (OxiteModuleConfigurationElement module in config.Modules)
{
IOxiteModule moduleInstance = modulesLoaded.Load(config, module);
//...
}
如果正常返回IOxiteModule對象,就調用如下四個方法:
RegisterWithContainer
Initialize
RegisterFilters
RegisterModelBinders
接著:
this.container.RegisterInstance(modulesLoaded);
疑問2:上面這句寫在迴圈裡,為什麼不寫在迴圈外面?
跳出迴圈後,對路由規則進行設定:
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.LoadFromModules(modulesLoaded);
routes.LoadCatchAllFromModules(modulesLoaded);
LoadFromModules和LoadCatchAllFromModules方法是擴充方法。
方法內部會遍曆ModulesLoaded對象中儲存的IOxiteModuel對象並分別調用對象的RegisterRoutes和RegisterCatchAllRoutes方法。詳情請看Oxite.Extensions.RouteCollectionExtensions類。
疑問3,這兩行代碼為什麼沒有像調用ModulesLoaded類的RegisterWithContainer,Initialize,RegisterFilters,RegisterModelBinders這四個方法那樣調用。
我覺得完全可以這樣:
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
foreach (OxiteModuleConfigurationElement module in config.Modules)
{
IOxiteModule moduleInstance = modulesLoaded.Load(config, module);
if (moduleInstance != null)
{
moduleInstance.RegisterWithContainer();
moduleInstance.Initialize();
moduleInstance.RegisterFilters(filterRegistry);
moduleInstance.RegisterModelBinders(modelBinders);
moduleInstance.RegisterRoutes(routes);
moduleInstance.RegisterCatchAllRoutes(routes);
}
}
this.container.RegisterInstance(modulesLoaded);
this.container.RegisterInstance(filterRegistry);
(疑問3的解釋:大家都知道,Routing規則設定的順序非常重要,RegisterRoutes方法中先對Modules進行倒序再註冊,目的是使排在後面的Module的Routing先註冊。我覺得這也帶來一點麻煩,也許你會覆蓋(說成隱藏比較合適)了原本不想覆蓋的,比如新加的模組有可能會覆蓋系統模組的Routing規則。也說明了兩個方面問題,一方面模組之間也有依賴關係,比如其他模組對系統模組的依賴,當然我們要盡量普通模組之間的依賴;另一方面模組在oxite.config中的順序也很重要。)