每個程序都需要初始化的過程,用來讀取配置或者設置一些運行環境(變量),對於ASP.NET程序來說,又該在哪里執行初始化的任務呢?
我想應該絕大多數人都知道在Global.asax中執行初始化的過程, 然而有些細節是我們需要關注的。
本文用例
在這篇博客的示例代碼中,AppInitializer包含了網站的初始化的實現代碼:
這段代碼的意圖很清楚,一定要確保正確的配置了數據庫連接字符串,否則以異常的形式報告出來。
你沒有想到的Global.asax怪事!
或許有些人會這樣寫他們的初始化代碼:
void Application_Start(object sender, EventArgs e) { //在應用程序啟動時運行的代碼 try { AppInitializer.Init(); } catch( Exception ex ) { LogException(ex); // ..................... } }
這段代碼有什么問題呢?
其實問題的線索在於:為什么要加try....catch語句,是因為知道可能會發生異常嗎?
如果真有異常情況發生,這樣處理后,后續的請求是不是會發生各種想像不到的錯誤?
顯然這里不能吃掉異常,要不然后面的請求肯定會有問題,因為它們依賴的設置沒有正確的初始化。
好吧,那我去掉 try.....catch語句,這樣總該行了吧:
void Application_Start(object sender, EventArgs e) { //在應用程序啟動時運行的代碼 AppInitializer.Init(); }
還是看來一下真實的運行情況吧。
噢,抱歉,我還真忘記了配置連接字符串,這個異常提示太給力了。
現在就加上連接字符串嗎?
別急,想像一下,如果這個網站是一個真實的在線網站,會是什么情況呢?
答案有二種:
1. 另一個用戶也發起了一次請求。
2. 當前用戶看到錯誤頁面后,重新刷新了一次當前頁面。
現在我用Opera來扮演第二個瀏覽用戶吧,還是打開同樣的網址。
太奇怪了,第二個用戶居然能打開頁面,好吧,讓他登錄試試。
結果第二個用戶看到的錯誤情況和第一個用戶完全不同。
如果此時第一個用戶刷新他的瀏覽器,發現頁面又可以顯示了,然而登錄時,會看到與第二個用戶一樣的異常信息。
這個示例代碼實在太簡單了,我想維護人員根據NullReferenceException這個線索找下去,很快就能找到答案。 如果初始化代碼再復雜一些,比如SetSqlDependency()中出現異常呢,那么程序仍然能夠正常運行, 但是我們期望的緩存依賴可能就沒有效果了,最終可能會產生性能問題,排查的難度就會大多了。
記得以前做項目時,就遇到過這種情況,當時感到很奇怪,為什么刷新一下就沒黃頁了,不過后面的錯誤就很折騰人了, 最終也讓我總結了這個教訓。 所以我建議:如果在初始化階段出現了異常,干脆就別讓程序繼續運行了,每個請求都直接顯示黃頁,直到排除故障為止。
如何保證初始化異常一直顯示?
當初始化發生異常時,如何保證初始化異常一直顯示呢?
方法其實並不難,我們需要修改一下代碼:
private static Exception s_initException; void Application_Start(object sender, EventArgs e) { try { AppInitializer.Init(); } catch( Exception ex ) { // 記下初始化的異常。 s_initException = ex; } } protected void Application_BeginRequest(object sender, EventArgs e) { // 如果存在初始化異常,就拋出來。 // 直到開發人員發現這個異常,並已解決了異常為止。 if( s_initException != null ) throw s_initException; }
現在不管有多少個用戶來訪問,或者第一個訪問者刷新瀏覽器多少次,都會看到同樣的異常信息:
說明:Global.asax的這個問題在IIS7以上版本的集成模式下並不存在。
還有哪些初始化方法?
除了Global.asax中的Application_Start,還有哪些方法可以在ASP.NET程序執行初始化的任務呢?
目前我知道的還有另三種方法:
1. App_Code中的AppInitialize方法。
2. 寫個專用的HttpModule。
3. ASP.NET 4.0的PreApplicationStartMethodAttribute
App_Code中的AppInitialize方法
ASP.NET允許我們在App_Code中的任何一個類型定義一個AppInitialize方法,用它也能執行初始化的任務。
public class Class1 { public static void appInitialize() { AppInitializer.Init(); } }
如果我此時再次運行示例程序(已注釋掉Global.asax中的代碼),會看到以下顯示:
顯然,我們期望的初始化代碼確實被調用了。
這個AppInitialize方法有什么限制呢?
我們還是來看一下ASP.NET的源代碼吧:
internal class BuildResultMainCodeAssembly : BuildResultCompiledAssembly { private MethodInfo FindAppInitializeMethod(Type t) { return t.GetMethod("AppInitialize", BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase, null, new Type[0], null); }
根據代碼我們可以發現AppInitialize方法的特點有:
1. 必須是一個公開的靜態方法:BindingFlags.Public | BindingFlags.Static
2. 方法名不區分大小寫:BindingFlags.IgnoreCase
3. 方法不允許有傳入參數:new Type[0]
HttpModule也能執行初始化的任務
由於HttpModule總是會在ASP.NET管線中被調用,所以,我們還可以用它來完成初始化的操作。
public class InitAppModule : IHttpModule { public void Init(HttpApplication context) { //注意:Init事件可能被多次調用,所以這個方法會被多次調用。 AppInitializer.Init(); }
正如代碼注釋所說的那樣,這種調用代碼是不對的,除非你能接受初始化代碼被多次調用!
所以,我們應該按單例模式的思路來改寫代碼:
private static readonly object s_lock = new object(); private static bool s_initOK; public void Init(HttpApplication context) { lock( s_lock ) { if( s_initOK == false ) { //保證初始化代碼只執行一次。 AppInitializer.Init(); s_initOK = true; } } }
如果你希望代碼簡單一點,還可以這樣實現:
public class InitAppModule : IHttpModule { static InitAppModule() { AppInitializer.Init(); } public void Init(HttpApplication context) { // 留個空方法, // ASP.NET會調用這個方法,最后能觸發靜態方法的調用。 }
ASP.NET 4.0新增的初始化方法
為了讓一些類庫能自動執行一些初始化,ASP.NET提供了一種新方法,允許為程序集指定一個PreApplicationStartMethodAttribute
為了演示這種用法,我將前面的示例(VS2008開發)移到一個類庫中(用VS2012開發)並設置類庫的命名空間為InitClassLibrary1。
然后,我添加了一個調用類:
namespace InitClassLibrary1 { public class Class1 { public static void InitApp() { AppInitializer.Init(); } } }
最后,我們可以在InitClassLibrary1類庫的AssemblyInfo.cs文件中,增加一個Attribute
[assembly: System.Web.PreApplicationStartMethod( typeof(InitClassLibrary1.Class1), "InitApp")]
當然了,你也可以直接像下面設置,免得多創建一個類型出來:
[assembly: System.Web.PreApplicationStartMethod( typeof(InitClassLibrary1.AppInitializer), "Init")]
這樣設置后,再運行網站,你也能發現我們的初始化代碼確實運行了:黃頁仍然在顯示。
各種初始化方法的差別
前面介紹了4種在ASP.NET執行初始化的方法,你或許想知道它們到底有哪些區別呢?
由於它們都能實現初始化的操作,它們的差別也只有執行的時刻不同而已, 我們可以用簡單的方法區分它們的調用位置:看異常的調用堆棧信息。
AppInitialize方法異常時的調用堆棧信息:
HttpModule異常時的調用堆棧信息:
PreApplicationStartMethodAttribute異常時的調用堆棧信息:
Global.asax的Application_Start事件處理器的調用方式則不同,ASP.NET采用了反射調用, 當異常發生只保留了內部異常,我們看不到調用堆棧(不信的話,自己去試試)。
沒關系,既然ASP.NET不告訴我們調用堆棧信息,我們自己也可以去取,請看下面的代碼:
void Application_Start() { System.Diagnostics.StackTrace stack = new System.Diagnostics.StackTrace(); System.IO.File.WriteAllText("h:\\Application_Start_stack.txt", stack.ToString()); }
再打開文件看一下吧。
說明:Global.asax的Application_Start事件處理器還有幾種等效的方法:
// 這二個方法都可以實現與Application_Start(object sender, EventArgs e)相同的行為。 void Application_OnStart() { } void Application_Start() { }
根據以上分析,可以可以得知:
1. AppInitialize和PreApplicationStartMethodAttribute指向的方法被調用的時機發生在ASP.NET創建宿主環境時,屬於比較早的時刻。
2. Application_Start和HttpModule的調用時刻要晚一點。
這個結論有用嗎?
其實我也感覺意義不大,不過分析它僅僅為了滿足我的求知欲和好奇心而已,你是否也有這樣的好奇心呢?
如果你仍然好奇想知道這4種方法的執行時機的先后順序,我也能告訴你:
1. PreApplicationStartMethodAttribute指向的方法。
2. App_Code中的appInitialize方法。
3. Application_Start。
4. HttpModule
再補充一點:在開發環境中,當我們編譯網站時,PreApplicationStartMethodAttribute指向的方法可能會被調用,這處決於類庫的程序集是否發生了修改。
到底該選擇哪種初始化方法?
今天給大家介紹了4種在ASP.NET中執行初始化的方法,或許有些人會想:到底該選擇哪種初始化方法呢?
的確,方法越多越讓人迷惑。
下面的觀點僅代表我個人的建議,你也可以根據自己的喜好來選擇。
1. 優先選擇Application_Start(雖然IIS的經典模式下需要多寫點代碼),因為任何人找初始化代碼時都會想到那里,便於其他人維護。
2. AppInitialize方法雖然使用簡單,但它並不適合於WebApplication項目。
3. PreApplicationStartMethodAttribute只支持ASP.NET 4.0以上版本,且尤其適合於類庫的內部初始化。
4. 當以上方法都不可行時,HttpModule將成為最后的救命稻草,它適合所有ASP.NET版本。