作為一名 ASP.NET 開發人員,您可能非常清楚 ASP.NET 如何處理 .aspx 資源中的代碼,如何對標記進行分析並將其動態轉換成 Visual Basic 或 C# 類,等等。但是接下來呢?ASP.NET 產生的檔案儲存在哪裡?如何利用它們滿足頁面請求?從上個月起,我開始關注這一過程。在本月的內容中,我將對伺服器上所發生的操作進行分析,以便您能夠避免某些常見的問題隱患。 我將討論 ASP.NET 臨時檔案的儲存以及動態產生的用於為頁面響應提供服務的類的原始碼。此外,我還將構建一個可以與任何 ASP.NET 2.0 或 ASP.NET AJAX(原代號為“Atlas”)應用程式一同使用的資源管理員工具,以查看和調試您的頁面執行的實際代碼。但在此之前,您需要瞭解幾個事項。(和上月專欄一樣,本部分內容主要以那些沒有文檔記錄的 ASP.NET 工作原理細節為基礎來展開。這些實施細節在未來的 Microsoft .NET Framework 版本中可能會發生變化。)
Temporary ASP.NET Files 檔案夾中儲存的是什麼內容?
ASP.NET 頁面請求的處理過程需要使用一些臨時檔案。當您在 Web 服務器上安裝 ASP.NET 2.0 時,所建立的資料夾階層如下:
%WINDOWS%\Microsoft.NET\Framework\v2.0.50727
這裡的版本號碼指的是 ASP.NET 2.0 的零售版。ASP.NET 的每個發布版本(包括每個過渡性的組建)都有一個唯一的版本號碼,並且會建立不同的資料夾樹狀目錄,以便支援不同版本的並存執行。因此,您務必要指定您的應用程式所適用的 ASP.NET 版本,這一點極為重要。在 ASP.NET 1.x 和 ASP.NET 2.0 下啟動並執行應用程式基底於物理形式上獨立的檔案夾。在 Microsoft.NET\Framework 檔案夾下,您會找到與已安裝的 ASP.NET 版本數相同數量的 vX.X.XXXX 子檔案夾(請參見圖 1)。
圖 1 ASP.NET 1.0、1.1、2.0 和 3.0 運行庫檔案 (單擊該映像獲得較小視圖)
圖 1 ASP.NET 1.0、1.1、2.0 和 3.0 運行庫檔案 (單擊該映像獲得較大視圖)
在已安裝版本的根資料夾下,您會看到許多子目錄。CONFIG 檔案夾包含電腦設定檔,其中包括 machine.config 和用於所有網站的基本 web.config 檔案。名為 ASP.NETWebAdminFiles 的檔案夾包含構成網站管理工具的源檔案,您可從 Visual Studio 2005 內部運行該工具。最後,Temporary ASP.NET Files 檔案夾包含為頁面和資源提供服務而建立的所有臨時檔案和程式集。要找到為您的 Web 頁面動態建立的檔案,您需要查看此檔案夾子樹。請注意,Temporary ASP.NET Files 目錄是存放動態建立的檔案的預設位置,但可以使用 web.config 檔案中的 <compilation> 部分按應用程式對其進行配置:
<compilation tempDirectory="d:\MyTempFiles" />
當應用程式第一次在電腦上執行時,在臨時檔案目錄下就會建立一個新的子檔案夾。編譯子檔案夾的名稱與應用程式的 IIS 虛擬目錄的名稱相同。如果您只是使用 Visual Studio 2005 內嵌的 Web 服務器測試應用程式,那麼子檔案夾會採用該 Web 應用程式的根資料夾的名稱。如果您從 Web 服務器的根資料夾調用頁面,您將在根子檔案夾下找到它們的臨時檔案(請參見圖 2)。
圖 2 Web 測試伺服器上的 WebApp (單擊該映像獲得較小視圖)
圖 2 Web 測試伺服器上的 WebApp (單擊該映像獲得較大視圖)
在應用程式的編譯子檔案夾下,有一組使用散列名稱的目錄。此處顯示了通常可以找到臨時檔案的路徑。(最後兩個目錄包含的是假名稱,但實際顯示的就是這樣的名稱。)
\v2.0.50727\Temporary ASP.NET Files\MyWebApp\3678b103\e60405c7
您可以使用以下語句,以編程的方式檢索指定應用程式的臨時檔案所在的子檔案夾的路徑:
Dim tempFilesFolder As String = HttpRuntime.CodegenDir
ASP.NET 會定期在應用程式發生改變、需要重新編譯時間清理編譯檔案夾並刪除陳舊的資源,但 Temporary ASP.NET Files 目錄下的子樹的大小可能會顯著地增加,在測試電腦上更是如此。 作為管理員,您應密切關注 Temporary ASP.NET Files 下的目錄,並確保所有目錄都是與當前活動的應用程式有關的。如果您無意間刪除了一個處於活動狀態的應用程式的子樹,不必驚慌。您將丟失所有先行編譯的頁面和資源並會將應用程式重設到其最初的編譯狀態;但下一個請求將觸發對每個頁面或一批頁面(具體取決於配置)執行新的編譯過程,因此最終不會丟失任何資訊或頁面,只不過使用者在處理下一個請求時將感覺到首次命中延遲。現在,我們來看某一應用程式的編譯檔案夾的內容。
保留檔案
對於應用程式中的每個頁面,頁面編譯進程會產生一個下述名稱的檔案:
[page].aspx.[folder-hash].compiled
[page] 預留位置代表 .aspx 資源的名稱。[folder-hash] 預留位置是一個散列值,它使檔案名稱保持唯一,避免與原本屬於其他檔案夾的同名檔案混淆。這種檔案稱為保留檔案,因為它們包含有重要的資訊,這些資訊可協助 ASP.NET 運行庫快速檢索程式集以及檢索將用於為頁面請求提供服務的 HTTP 處理常式的類型名稱。此外,保留檔案還包含一個檔案散列值,用於檢測自從上次訪問後檔案的內容是否發生了改變。
構成某一應用程式的所有 .aspx 頁面在同一個臨時檔案夾中進行編譯,即使它們名稱相同且位於不同的檔案夾中也是如此處理。這一點如何??假設您的應用程式套件組合含兩個名為 test.aspx 的頁面,位於不同的檔案夾 - Folder1 和 Folder2 中。兩個頁面將在同一臨時檔案夾中進行編譯,但可以通過它們的散列值對其進行區分,由於散列值是根據路徑資訊而不只是檔案名稱計算出來的,因此它們的散列值是不同的。因而最終,兩個 test.aspx 頁面的保留檔案名稱只在檔案夾散列值部分有所不同:
Test.aspx.cdcab7d2.compiledTest.aspx.9d86a5d7.compiled
散列值的內部儲存緩衝使 ASP.NET 運行庫可以識別任何指定頁面 URL 的散列值並快速找到相應的保留檔案。如果沒有找到保留檔案,ASP.NET 會動態編譯頁面。當您部署沒有先行編譯的應用程式時就會發生這種情況。另一方面,當您對一個網站進行先行編譯時,每個組成頁面的保留檔案被建立並放置在 Bin 檔案夾中。
保留檔案為純 XML 檔案。圖 3 顯示了一個樣本保留檔案的內容。
圖 4 具體列出了檔案的屬性。<fileDeps> 部分列出了當前頁面所依賴的檔案。對任何依存關係所做的任何改動都將導致頁面重新編譯。FileHash 值代表依存關係狀態的快照,而 Hash 代表當前分頁檔狀態的快照。值得注意的是,當您停止或重新啟動 Web 應用程式時,完全基於檔案更改通知來檢測檔案動態更改的機制會失敗。按照散列值儲存頁面和依存關係的狀態,使您可以隨時檢測到更改。
類型 (Type) 屬性設定動態建立的類(將用於為請求提供服務)的名稱。預設情況下,類型名稱是 ASP.[page]_aspx,其中 [page] 代表分頁檔的名稱。但是請注意,您可以通過設定您的 .aspx 檔案的 @Page 指令中的 ClassName 屬性來更改此名稱。根命名空間不會更改,因此類型名稱可以是 ASP.[ClassName]。
程式集 (Assembly) 屬性指示動態建立的程式集的名稱,該程式集包含用於為請求提供服務的頁面類。此類程式集的名稱和內容取決於 web.config 檔案的 <compilation> 部分中的設定。
預設情況下,應用程式頁面以批處理模式編譯,這意味著 ASP.NET 會嘗試在一個程式集中容納儘可能多的未編譯頁面。使用 maxBatchSize 和 maxBatchGeneratedFileSize 屬性可以限制一個程式集中封裝的頁面數量以及程式集的總大小。預設情況下,每個批處理編譯將擁有不超過 1000 個頁面,並且所有程式集都不大於 1MB。一般來說,當第一次編譯大量頁面時,您不應讓使用者等待太長時間。同時,您不應該在記憶體中載入大型程式集而只是為一個小頁面來提供服務,或者為每個頁面啟動編譯。 maxBatchSize 和 maxBatchGeneratedFileSize 屬性可協助您在首次命中延遲和記憶體使用量之間找到良好的平衡。 如果您選擇網站先行編譯(請參閱本雜誌 2006 年 1 月 Fritz Onion 的 Extreme ASP.NET 專欄),那麼您不必擔心首次命中延遲,但您仍應考慮最佳的批處理參數,以避免 Web 服務器的記憶體過載。
當批處理開啟時,應用程式中的前 1000 個頁面(實際數量取決於 maxBatchSize)被編譯為名為 App_Web_[random] 的程式集,其中 [random] 是由八個字元組成的隨機序列。 如果關閉批處理,則每個頁面將產生各自的程式集。程式集的名稱如下:
App_Web_[page].aspx.[folder-hash].[random].dll
要關閉批處理,可向 web.config 檔案添加以下內容:
<compilation batch="false" />
如果您對一個應用程式範例的編譯檔案夾進行查看,您會找到名稱中包含 CBMResult 的附帶保留檔案,還有一個具有相同名稱的 .ccu 檔案,如下所示:
test.aspx.cdcab7d2.compiledtest.aspx.cdcab7d2_CBMResult.ccutest.aspx.cdcab7d2_CBMResult.compiled
列表中的第一個檔案是保留檔案。那麼其他兩個作何用途? CCU 代表代碼編譯單元 (Code Compile Unit),是指用於產生動態網頁面類的原始碼而建立的 CodeDOM 樹。 CCU 檔案是二進位檔案,包含經序列化的頁面 CodeDOM 樹。CBMResult 檔案是保留檔案,用於檢查 CCU 是否最新、其所在的位置以及它基於哪些檔案。
CBMResult 檔案由與 ClientBuildManager 類通訊的模組(例如,Visual Studio 2005 設計器和 IntelliSense)來使用。這些模組查詢頁面的結構來擷取語句結束資訊。CCU 檔案會保留準備為這些請求提供服務的頁面的最新 CodeDOM 結構副本。
頁面類動態原始碼
正如上面提到的,.aspx 資源被解析為 Visual Basic 或 C# 類。該類繼承自 System.Web.UI.Page,或者很可能繼承自某個從 System.Web.UI.Page 繼承而來的類。事實上,在大多數常見情形下,動態網頁面類具有以下原型:
Namespace ASPPublic Class test_aspxInherits Test : Implements System.Web.IHttpHandler...End ClassEnd Namespace
在此例中,Test 類在頁面的代碼檔案類中定義,它包括您在頁面的附帶類檔案中寫入的任何事件處理常式和協助器常式。在您使用 Visual Studio 2005 時可能已經注意到,此代碼檔案類缺少頁面成員的定義。對於您在 .aspx 源檔案中找到的每個 runat=server 標記,在代碼檔案中應定義有相應類型的成員。ASP.NET 運行庫系統會產生 Test 部分類別,包含所有這些成員以及兩個額外的屬性 - Profile 和 ApplicationInstance。圖 5 顯示了參與為某一 .aspx 資源的請求提供服務的類集。
圖 5 中的類跨兩個不同的源檔案。第一個包含部分類別,用於完善代碼檔案中的類和由此派生出的用於為請求提供服務的實際頁面類。第二個檔案是您在項目中建立的代碼檔案的副本。這些檔案根據程式集名稱而命名。名稱的結構如下:[assembly].X.vb。(如果您使用 C#,則為 .cs)X 為從 0 開始的遞增索引值,可確保檔案名稱唯一。
如果您查看樣本 test.aspx 頁面的編譯檔案夾的內容,您會發現建立了第三個檔案,如下例中所示:
Namespace __ASPFriend Class FastObjectFactory_app_web_test_aspx_cdcab7d2_xg83msu0Private Sub New()MyBase.NewEnd SubShared Function Create_ASP_test_aspx() As ObjectReturn New ASP.test_aspxEnd FunctionEnd ClassEnd Namespace
類名稱是以字串 FastObjectFactory 為首碼的頁面程式集的名稱。該類具有一個名為 Create_XXX 的共用函數(如果以 C# 編寫則為靜態函數),其中的 XXX 是要執行個體化的頁面類的名稱。顧名思義,這是一個協助器類,ASP.NET 運行庫利用其來加速頁面執行個體的建立 - 這是一個非常常見的操作。與編譯一個頁面相比,建立這種類所花費的時間非常短。另一方面,使用工廠類比使用 Activator.CreateInstance 間接建立對象要快得多。
根據批處理編譯設定,工廠類的內容會有所變化。在預設情況下,當批處理開啟時,工廠類包含與批處理頁面相同數量的 Create_XXX 函數。工廠類的名稱與批次程式集的名稱相同:
' Used to serve test.aspxShared Function Create_ASP_test_aspx() As ObjectReturn New ASP.test_aspxEnd Function' Used to serve default.aspxShared Function Create_ASP_default_aspx() As ObjectReturn New ASP.default_aspxEnd Function
如果批處理關閉,則工廠類與單個頁面程式集的名稱相同,並且只包含一個共用函數 - 具體頁面的 facotry。在這種情況下,應用程式中的每個頁面將有自己的工廠類。
運行庫公用 API
藉助上面討論的資訊,探究編譯檔案夾的內容就不是非常困難了。但通過一個工具來協助您快速找到您所需的資訊還是非常方便。 我待會兒將設計一個用來導航動態產生的 ASP.NET 應用程式原始碼的資源管理員工具,但首先我們來看一看 .NET Framework 2.0 中的一些運行庫 API。特別是,以下兩個類可能是您更希望瞭解的:HttpRuntime 和 ClientBuildManager。
HttpRuntime 類具有大量共用屬性,可返回關於包括當前應用程式的 Bin 檔案夾、ASP.NET 安裝路徑、編譯檔案夾和當前 AppDomain ID 在內的各種系統路徑的資訊。您還可以使用以下代碼輕鬆擷取當前 AppDomain 中載入的程式集列表:
Dim listOfAssemblies() As AssemblylistOfAssemblies = AppDomain.CurrentDomain.GetAssemblies()
此代碼並非特定於 ASP.NET,但當從 ASP.NET 應用程式內部調用時,它將返回包含 AppDomain 中的程式集的數組,其中包括為您的頁面產生的所有程式集。
ClientBuildManager 類沒有多少資訊一類的屬性,CodeGenDir 屬性除外,該屬性返回與 HttpRuntime 的 CodeGenDir 屬性相同的資訊。但 ClientBuildManager 具有許多讀取配置資訊(如支援的瀏覽器)的方法和先行編譯應用程式的方法。Get 是該類中的一個方法,它返回一列應用程式的目錄(在這些目錄中監視那些會引發關閉 AppDomain 應用程式的重要更改)。這些目錄是:App_Browsers、App_Code、App_GlobalResources、App_WebReferences 和 Bin。
構建資源管理員工具
對於調試,能夠快速存取正在啟動並執行頁面的原始碼和其他運行時資訊往往非常有用。任何提供這種功能的工具都必須與所有 ASP.NET 應用程式相容,並且只要求進行有限的配置或根本無需配置。Nikhil Kothari 出色的 Web Development Helper 工具如果能夠提供 ASP.NET 運行庫資訊,那就非常完美了。該工具作為瀏覽器協助對象 (BHO) 來實現,BHO 是一種用於 Microsoft Internet Explorer 使用者介面的基於 COM 的外掛程式。BHO 對於我在本專欄中構建的監視工具將是非常好的宿主環境,但可惜我偷了些懶,並沒有這樣做。因此我將我的工具編寫為位於頁面和瀏覽器之間的一個 HTTP 模組,它可尋找查詢字串,如果是顯式調用就可發揮作用。在 ASP.NET 應用程式中安裝 HTTP 模組只需在 web.config 中增加一行語句,而且可以非常容易地開啟和關閉安裝:
<httpModules><add name="AspExplorerModule" type="Samples.AspExplorerModule" /></httpModules>
圖 6 顯示了 Explorer HTTP 模組的大部分代碼。該模組註冊使用 PostMapRequestHandler 應用程式事件並與頁面類掛接。PostMapRequestHandler 事件會在 ASP.NET 運行庫確定了為請求提供服務所需的 HTTP 處理常式對象時觸發。如果請求的查詢字串中包含 source=true 參數,並且處理常式是從 System.Web.UI.Page 繼承的一個類,那麼模組將開始工作。
ASP Explorer 模組會與頁面類掛接,並為 PreRenderComplete 事件註冊其自己的處理常式。這樣的設計使得 HTTP 模組不會改變請求的運行時處理,也不會幹預頁面的編譯。當查詢字串指定了 source 參數並將其設定為 true 時,模組就會發揮作用。 6 中所示,模組所要做的就是使用不太常見的“頁面”類方法 SetRenderMethodDelegate 為頁面註冊呈現委派 (rendering delegate)。當為頁面指定了呈現委派時,所封裝的方法會替代標準呈現處理。換言之,一旦安裝了該模組,如果使用 test.aspx 進行調用,您將看到頁面的標準輸出;如果您使用 test.aspx?source=true 進行調用,您將看到模組可收集的所有與頁面有關的運行時資訊。
ASP Explorer 原始碼定義了一個類,以映射當前頁的保留檔案的內容。它會讀取保留檔案,並複製圖 7 所示的類中的所有資訊。SourceFiles 屬性是設計用於包含頁面使用的所有源檔案的一個集合。此集合包含從編譯檔案夾獲得的保留檔案中所沒有的資訊。特別是其中包括與某個頁面相關的 .vb 或 .cs 格式的所有源檔案,這些檔案名稱以動態網頁面程式集的名稱開頭。GetWebPageInfo 方法(請參見圖 6)捕獲所有資訊並為 source 模式的請求構建輸出內容。頁面輸出包括運行時資訊和動態網頁面類的原始碼。圖 8 顯示了實際運行中的 ASP Explorer。
圖 8 實際運行中的 ASP Explorer 模組 (單擊該映像獲得較小視圖)
圖 8 實際運行中的 ASP Explorer 模組 (單擊該映像獲得較大視圖)
樣本頁面分析
既然有了可使用的工具,那麼讓我們簡要查看一下 ASP.NET 為每個 .aspx 檔案產生的程式碼的結構。值得注意的是,如果沒有 ASP.NET 運行庫提供的分析和編譯工具,您就必須親自編寫代碼來運行 ASP.NET 頁面!
動態網頁面類(圖 5 中的 test_aspx 類)改寫了 System.Web.UI.Page 類中的幾個方法:FrameworkInitialize、ProcessRequest 和 GetTypeHashCode。ProcessRequest 沒有什麼變化,它只是調用它的基類方法。GetTypeHashCode 返回頁面的散列代碼,該代碼可唯一標識頁面的控制項階層。當對頁面進行編譯時間,會動態計算散列值,並將其作為常量插入到源檔案。
最值得關注的是對 FrameworkInitialize 的改寫。該方法控制頁面的控制項樹的建立,並調入一個名為 __BuildControlTree 的私人方法。此方法使用與 .aspx 源檔案中的 runat=server 標記相對應的控制項的新執行個體來填充頁面類的 Control 集合。__BuildControlTree 會分析所有伺服器端標記並為每個標記構建一個對象。
<asp:textbox runat="server" id="TextBox1" text="Type here" />
以下是為上述標記擷取的典型代碼:
Private Function __BuildControlTextBox1() As TextBoxDim __ctrl As New TextBox()Me.TextBox1 = __ctrl__ctrl.ApplyStyleSheetSkin(Me)__ctrl.ID = "TextBox1"__ctrl.Text = "Type here"Return __ctrlEnd Function
如果控制項有事件處理常式或資料繫結運算式,會怎樣?讓我們首先來考慮帶“單擊”事件處理常式的按鈕。您需要增加一行語句:
__ AddHandler __ctrl.Click, AddressOf Me.Button1_Click
對於資料繫結運算式 <%# … %>,除了使用了 DataBinding 事件,產生的程式碼與之類似:
AddHandler __ctrl.DataBinding, AddressOf Me.DataBindingMsg
與處理常式相關的代碼取決於綁定的控制項的屬性和要綁定的代碼。對於 Label 控制項的 Text 屬性,代碼類似於:
Public Sub DataBindingMsg(ByVal sender As Object, ByVal e As EventArgs)Dim target As Label = DirectCast(sender, Label)target.Text = Convert.ToString(..., _CultureInfo.CurrentCulture);End Sub
傳遞給 Convert.ToString 的運算式就是 <%# … %> 運算式中的代碼。強制類型轉換還取決於所涉及的類型。
如果存在主版頁面和主題,那麼源檔案的數量和依存關係列表就會增大,但藉助 ASP Explorer 工具,您可以隨意對其進行跟蹤。
總結
ASP.NET 對其擁有的資源類型執行按需動態代碼編譯。此功能大大促進了 Web 應用程式的快速反覆式開發法,但需要 ASP.NET 才能將檔案寫到磁碟。編譯檔案夾是一個重要的檔案夾,ASP.NET 的許多神奇之處都在此體現。您可以只是出於興趣對此檔案夾研究一番,有時卻可以利用它來診斷和調試棘手的問題。當然,這裡討論的大部分功能是 ASP.NET 內部的功能,因此來說,這些功能在未來版本中可能會未經提醒即變更。 但截至目前為止,ASP.NET 2.0 的工作原理就是如本文所述的這樣。順便提一下,可以將 ASP Explorer 工具與 ASP.NET AJAX 應用程式一起使用,這一點也請放心。 該工具的運行效果非常好。