在本篇文章的第一部分:[原創]深入剖析ASP.NET的編譯原理之一:動態編譯(Dynamical Compilation),詳細討論了ASP.Net如何進行動態編譯的,現在我們來談談另外一種重要的編譯方式:預編譯(Precompilation)。
1.為什麼要進行預編譯
ASP.NET 2.0的編譯方式大體可以分成兩種:動態編譯和預編譯,要回答為什麼要進行預編譯,我們先要看看動態編譯有什麼不好的地方。我們回顧一下上一篇介紹的ASP.NET進行動態編譯的簡單的流程:當來自Browser的一個基於aspx的Http request抵達Web server,IIS handle這個request,通過分析注冊在IIS中的Application Mapping,將Request 傳給aspnet_isapi.dll ISAPI extension。ISAPI extension通過HttpRuntime進入Http Runtime Pipeline,HttpRuntime為每個Request創建一個單獨的HttpContext對象,用於保存request的Context信息。在Http Runtime Pipeline中,Http request會被注冊的一系列的Http module處理,比如OutputCache Module,Session Module,Authentication Module,Authorization,ErrorHandler Module等等。在Pipeline的終端,ASP.Net需要根據request創建對應的HttpHandler對象來處理該Request,並將生成結果Response到ClIEnt。對於一個基於ASPx的Http request,對應的Http handler對象一般就是一個System.Web.UI.Page對象。
ASP.NET會先判斷對應的Page type是否存在於被Cache的Assembly中,如果存在,直接創建Page對象,否則ASP.Net會先對該Page的相關的Source code (包括code behind,Html等等) 進行編譯,我們也說過這種編譯是以Directory為單位的,也就是說,處於同一個Directory下的需要編譯的文件會被編譯到同一個Assembly中。編譯生成的Assembly會被Cache,用於後續的Request。
正是因為對資源的首次訪問會導致一次編譯(這樣說不太准確,因為動態編譯是以directory為單位進行的,應該是對某個Directory下的資源進行首次訪問),這樣會嚴重降低Web Application的響應速度。所以我們為了避免這種情況,需要預先對web site進行編譯,所以提高web site的響應是進行預編譯的最重要的原因。
同時動態編譯就因為Web server上放置的是Source code,而且他們是可被修改的。而對於一個開發完畢的Web Application,我們更希望以Binary Assembly的方式進行部署,這樣Server上部署的都是Binary Assembly,不怕被別人篡改而導致系統的崩潰,從知識產權來講,也更利於保護商業秘密。這也是我們為什麼要進行預編譯的另一個原因。
下面我們就來講講如何進行預編譯,以及與編譯背後的原理。同時在這裡我需要特別提出的是,在上一部分講的一些術語和原理,比如Preservation file,FastObjectFactory,同樣適用於預編譯,重復的內容,在這裡就不必再介紹了。同時我也將沿用上一部的Sample。如果想看看相關的內容,請參閱[原創]深入剖析ASP.Net的編譯原理之一:動態編譯(Dynamical Compilation)。
2.In Place Pre-compilation V.S. Pre-compilation for Deployment
對於預編譯,又可以分為In Place Pre-compilation(本地預編譯)和Pre-compilation for Deployment(部署預編譯),In Place Pre-compilation很簡單,實際上就是把整個Web site編譯到我們一個臨時的目錄下面,這個臨時目錄也就是我們在介紹動態編譯提到的那個臨時目錄。而且這種編譯的方式,包括生成的文件也和動態編譯完全一樣,唯一不同就是編譯的時間:預先編譯,編譯的范圍:整個Web site。這種編譯就是你常用的在VS中的build。這種編譯方式一般用於開發階段。
以部署為目的的編譯是我們今天討論的重點,下面我們就著重來討論Pre-compilation for Deployment。
注:在ASP.NET中的編譯都是通過一個叫做aspnet_compiler的工具執行的,該工具隨ASP.Net 2.0一起發布,你完全可以利用此工具以命令行的方式的執行編譯,並通過傳遞不同的命令行開關設置不同的編譯選項。該工具被置於VS中,使你可以利用VS進行可視化的編譯。
3.Non-updatable Pre-compilation V.S. Updatable Pre-compilation
ASP.NET 2.0為我們提供了幾種不同方式的預編譯和部署。為了弄清楚這些預編譯和部署方式,我們先來回顧一下ASP.NET 1.x下的編譯方式。我們知道在ASP.NET 1.x時代對整個Web site進行編譯,實際上我們只會對所有C#和VB.NET等後台代碼進行編譯,並生成一個單一的Assembly。而Web page的aspx是不會參與編譯的。所以當我們訪問一個Web page的時候,ASP.Net必須對ASPx進行動態編譯。
這一切之所以能夠進行是因為Web page采用的是ASPx + code behind的模式。
<%@ Page Language="C#" AutoEventWireup="false" Codebehind="Default.ASPx.cs" Inherits="Default" %>
public partial class Default : System.Web.UI.Page
從上面我們可以看到aspx和Code behind是一種繼承的關系,aspx繼承和它對應的Code Behind。ASP.Net可以把Code behind和ASPx分開進行編譯,把它們編譯到不同的Assembly中。我們就是上面的Code為例,
我們現在若對該Web site進行編譯的話,Default.aspx.cs會被編譯到一個Assembly中,假設這個Assembly為App_Web.dll. 我們把該Dll和aspx部署到Production Server上。如果我們現在訪問defaut.aspx。ASP.Net
會對aspx進行動態編譯,生成的Assembly可以暫時成為App_Web_aspx.dll。對於Default.ASPx,如果我們如C#代碼來描述的話,應該像下面一樣定義:
{
這種編譯方式,我自己把它叫做對asXx的動態編譯。在ASP.Net2.0 中也沿用了這種編譯方式。這種編譯方式的主要特征是對Code behind和所有的後台代碼進行預編余,ASPx(確切地說應該是asXx:asax,asmx,asax等)原樣部署。由於這種方式的預編譯,asXx是可以修改的(當然這種修改是有一定限制的,因為code behind已經編譯好了,所以這種修改只可能是和code behind無關的修改),所以又叫做Updatable Pre-compilation。
除了Updatable Pre-compilation之外,ASP.Net還提供另外一種高效的預編譯方式,Non-updatable Pre-compilation,之所以叫做不可修改的預編譯,這是因為:這種編譯方式把asXx、Code behind、後台代碼甚至是部分Resource都進行預編譯,從而避免了運行時對asXx的動態編譯,從而最大程度地提高了整個Web site的響應。在部署的時候,我們除了把生成的Assembly進行部署之外,所有的通過編譯生成的asXx也必須進行部署。 不過需要特別說明的是,此時的asXx文件僅僅是一個占位的文件而已,它裡面不具有任何Html。
4.Partial class
在ASP.NET 1.x,由於采用的aspx + code behind的機制,對於任何一個Web page或者其他ASP.Net 基於axXx的對象來說,都是由兩個文件、兩個class組成。兩個文件是指axXx和code behind,兩個class是指Code behind定義的繼承自System.Web.UI.Page的class,和一個繼承自它的由axXx生成的class。
對於使用過ASP.Net 1.x來說,一定會很熟悉這樣一種情況:對於每個在ASPx中通過Html定義的Server Control,在Code behind中必須具有一個對應的protected成員,否則你不能通過編程的方式訪問這個Server control。以不同方式呈現的同一個Server control通過ID關聯起來,如果在Code behind中改了Server control的ID,Server control的Server端的Event handler將會失去原有的作用。
但是在ASP.NET 2.0來說,這種情況發生了改變,在aspx中的Server control在Code behind中卻沒有相應的成員變量,但是我們可以毫無障礙地訪問到每個Server control。這使得我們的code behind更加簡潔,通過避免了Server control在ASPx和code bebind中的不匹配的問題。這一切都得益於.Net Framework 2.0提供的partial class的機制:把同一個class分布於不同文件中進行定義。有了這個概念,我們來看ASP.Net 2.0的code behind機制。
比如我們有這樣的一個Page:
Code behind如下:
而實際上,ASP.Net會為我們創建一個隱藏的.cs文件(這個文件有人把它稱之為Sibling partial class):
}
這個文件會隨著aspx文件的改變而動態變化,所以code behind中的Server control永遠和aspx中的Server control是完全匹配的。所以我們說ASP.Net 2.0的Page是由3個文件、兩個class組成的。
5.編譯的粒度和Assembly的命名
到現在為止,我們所講的ASP.NET的預編譯都是以Directory為單位的,同一個Directory下的所有需要編譯的文件被編譯到同一個Assembly中。ASP.Net還支持以Page為單位的預編譯,也就是每個Page編譯成一個Assembly。
在默認的情況下,ASP.NET預編譯生成的Assembly名稱是隨機生成的,也就是每次生成的Assembly都具有不同的name。所以我們在部署Web site的時候,一般需要把原來的Assembly刪掉,再部署新的Assembly。不過ASP.Net為我們提供了另外一種選擇,使得每次編譯生成的Assembly具有相同的名稱,這樣我們部署的時候就可以直接把新的Assembly 拷貝到Production Server上,自動覆蓋掉同名的Assembly。
6. Sample
我們沿用上一部份是用的Sample,我們通過采用不同的預編譯方式看看程序將如何運行。
6.1 Non-updatable Pre-compilation
除了多了一個Bin目錄和PrecompiledApp.config之外,整個結構和Source code中的結構完全一樣。通過上面的分析,我們知道這種預編譯方式是將asXx、code behind、後台代碼和Resource一起編譯成Assembly。我們說過對於這樣的預編譯方式,aspx僅僅是一個站位的文件而以,其中HTML已經沒有任何意義了,那麼對於編譯後的aspx中到底是什麼東西呢。我們來一探究竟。打開每個ASPx都是一段如下如下一樣文字,並無任何Html。
PrecompiledApp.config裡面具有一段簡短的configuration,表明version和是否可以進行進一步的修改。
所有的Assembly被編譯到Bin目錄中,我們來看看到底生成了一些什麼樣的文件在Bin目錄中。
在Bin目錄由兩類文件構成:Assembly和以complIEd作為擴展名的Preservation file。Preservation file的內容和作用在第一部分已經詳細介紹過了,相信大家不會感到陌生。Preservation file在這裡和動態編譯所起的作用一樣。唯一有一點不同的是,他的結構更加簡潔,去掉的Dependence file的列表,因為對於Non-updatable Pre-compilation來說,每個Page的以來的文件都是不可更改的。
我們來運行以下程序,和動態編譯情況下的輸出結果比較,看看有什麼不同。我們照例先運行Default Page。
輸出的結果印證我們前面的討論:處於同一個目錄下的Default 和Default2被編譯到同一個Assembly中,關注於處理邏輯的code behind的class name為Default和Default2,關注於可視化界面render的aspx對應的class name被加上的_aspx後綴,如果對default_aspx和default2_aspx進行Reflect的話,你會發現他們分別繼承Default和Default2,而後者直接繼承自System.Web.UI.Page。所以default_aspx和default2_ASPx是真正的意義上基於Web page的Http handler。像動態編譯一樣,預編譯生成一個基於Assembly的FastObjectFactory Type,對該對象的描述請參照第一部分。
有了前面的理論基礎,相信大家已經猜到這時候,我浏覽Part I下的Page1和Page2時的輸出是什麼樣子,由於預編譯是以目錄為單位的,我們對Part I下的任何一個page的訪問,都會加載相同的Assembly,所以此時對這兩個Page的訪問會得到一樣的輸出結果:
6.2 Updatable Pre-compilation
1.PrecompiledApp.config:updatable被設置為true。
<precompiledApp version="2" updatable="true"/>
2.asXx和我們進行開發時內容一樣,例如ASPx包含的就是Html,我們可以在部署之後對他們進行和code behind無關的修改。
3.Preversation file中有加上了Page對應的dependence file列表。
<?XML version="1.0" encoding="utf-8"?>
我們來運行一下Web site,看看現在的輸出結果又有何不同。首先打開Default Page:
通過上圖,我們發現此時加載了兩個相關的Assembly。我們來分析一下為什麼會這樣。在分析Updatable Pre-compilation時,我們說過:asXx是不會參與編譯的,只有他們的code behind, 所有的後台代碼,資源文件才會參與編譯。對於一個page 來說,page的code behind被編譯到Assembly中,aspx則不會。Aspx在運行時實行動態編譯,所以aspx是可被修改的。在本例中,我們訪問Default Page,ASP.Net先對aspx進行編譯,其對應的class name為default_aspx,由於default_ASPx繼承於Default,並且Default存在於預編譯生成的Assembly中,所以這個Assembly被加載進來。
由於同一個page最終被編譯到兩個不同的Assembly中,所以我們此時訪問Part I中的Page1或者Page2,又會有兩個Assembly被加載進來:
6.3 以Page為單位進行預編譯
前面我們進行的都是以directory為單位的預編譯,現在我們縮小編譯的粒度,以Page為單位進行編譯。我們選擇了“Use fixed naming and single page assemblIEs”選項。那麼現在進行的是基於單個page的non-updatable pre-compilation。通時由於采用的是fixed naming的編譯方式,每次進行編譯生成的Assembly的名稱都是一樣的。
我們看到編譯器為每個Page生成了一個單獨的Assembly。此時運行程序,你看到的又將不同。如果此時你訪問Default Page,你將看到:
我們知道可以通過一個Public key/Private key pair對Assembly進行簽名,進而把它部署到GAC中,我們來看看如何做。
首先我通過SN.exe生成Public key/Private key pair並保存到一個文件中(比如D:\MyKey.keys),然後進行如下的編譯設置