http://flier_lu.blogone.net?id=1544105
.NET 1.1中預編譯ASP.NET頁面實現原理淺析
MS在發佈ASP.NET時的一大功能特性是,與ASP和PHP等腳本語言不同,ASP.NET實際上是一種編譯型的快速網頁開發環境。這使得ASP.NET在具有開發和修改的簡便性的同時,不會負擔效率方面的損失。實現上ASP.NET與JSP的思路類似,引擎在第一次使用一個頁面之前,會將之編譯成一個類,自動生成Assembly並載入執行。
而通過《在WinForm程序中嵌入ASP.NET》一文中我們可以瞭解到,ASP.NET引擎實際上是可以無需通過IIS等Web服務器調用而被使用的,這就使得手工預編譯ASP.NET頁面成爲可能。實際上這個需求是普遍存在的,早在ASP時代就層有第三方產品支持將ASP頁面編譯成二進制程序,以提高執行效率和保障代碼安全性,而將伴隨Whidbey發佈的ASP.NET 2.0更是直接內置了預編譯ASP.NET頁面的功能。
實際上網上早就有人討論過在ASP.NET 1.1中模擬預編譯特性的實現方法,例如以下兩篇文章
Pre-Compiling ASP.NET Web Pages
Pre-Compile ASPX pages in .NET 1.1
其思路基本上都是遍歷所有需要預編譯的頁面文件,然後通過模擬Web頁面請求的方式,觸發ASP.NET引擎的自動預編譯機制。這樣做的好處是完全模擬真實情況,無需瞭解ASP.NET引擎的實現原理;但同時也會受到諸多限制,如預編譯結果不透明,無法脫離原始ASP.NET頁面文件使用等等,而且無法使我們從原理上理解預編譯特性的實現。
下面我將分三到四個小節,簡要討論 ASP.NET 自動編譯機制的實現、ASP.NET 頁面文件編譯的實現以及如何在ASP.NET 1.1中實現手動預編譯頁面和相應分發機制。
[1] 自動預編譯機制淺析
本節我們將詳細分析討論.NET 1.1中,ASP.NET引擎內部實現自動頁面預編譯的原理。
首先,我們所說的ASP.NET頁面實際上主要分爲四類:
1.Web 應用程序文件 Global.asax
2.Web 頁面文件 *.aspx
3.用戶自定義控件文件 *.ascx
4.Web 服務程序文件 *.asmx
Web 應用程序文件對於每個Web 應用程序來說是可選唯一的,用來處理ASP.NET應用程序一級的事件,並將被預編譯爲一個System.Web.HttpApplication類的子類;
Web 頁面文件是普通的ASP.NET頁面,處理特定頁面的事件,將被預編譯爲一個System.Web.UI.Page類的子類;
用戶自定義控件文件是特殊的ASP.NET頁面,處理控件自身的事件,將被預編譯爲一個System.Web.UI.UserControl類的子類;
Web 服務程序文件則是與前三者不太相同的一種特殊頁面文件,暫時不予討論。
然後,前三種ASP.NET文件的編譯時機也不完全相同。Web 應用程序文件在此 Web 應用程序文件第一次被使用時自動編譯;Web 頁面文件在此Web頁面第一次被使用時自動編譯,實際上是調用 HttpRuntime.ProcessRequest 函數觸發預編譯;用戶自定義控件文件則在其第一次被 Web 頁面使用的時候自動編譯,實際上是調用 Page.LoadControl 函數觸發預編譯。
在瞭解了以上這些基本知識後,我們來詳細分析一下自動預編譯的實現機制。
HttpRuntime.ProcessRequest 函數是處理Web頁面請求的調用發起者,僞代碼如下:
以下爲引用:
public static void HttpRuntime.ProcessRequest(HttpWorkerRequest wr)
{
// 檢查當前調用者有沒有作爲ASP.NET宿主(Host)的權限
InternalSecurityPermissions.AspNetHostingPermissionLevelMedium.Demand();
if(wr == null)
{
throw new ArgumentNullException("custom");
}
RequestQueue queue = HttpRuntime._theRuntime._requestQueue;
if(queue != null)
{
// 將參數中的Web頁面請求放入請求隊列中
// 並從隊列中使用FIFO策略獲取一個頁面請求
wr = queue.GetRequestToExecute(wr);
}
if(wr != null)
{
// 更新性能計數器
HttpRuntime.CalculateWaitTimeAndUpdatePerfCounter(wr);
// 實際完成頁面請求工作
HttpRuntime.ProcessRequestNow(wr);
}
}
HttpRuntime.ProcessRequestNow函數則直接調用缺省HttpRuntime實例的ProcessRequestInternal函數完成實際頁面請求工作,僞代碼如下:
以下爲引用:
internal static void HttpRuntime.ProcessRequestNow(HttpWorkerRequest wr)
{
HttpRuntime._theRuntime.ProcessRequestInternal(wr);
}
HttpRuntime.ProcessRequestInternal函數邏輯稍微複雜一些,大致可分爲四個部分。
首先檢查當前HttpRuntime實例是否第一次被調用,如果是第一次調用則通過FirstRequestInit函數初始化;
接着調用HttpResponse.InitResponseWriter函數初始化頁面請求的返回對象HttpWorkerRequest.Response;
然後調用HttpApplicationFactory.GetApplicationInstance函數獲取當前 Web 應用程序實例;
最後使用Web應用程序實例完成實際的頁面請求工作。
僞代碼如下:
以下爲引用:
private void HttpRuntime.ProcessRequestInternal(HttpWorkerRequest wr)
{
// 構造 HTTP 調用上下文對象
HttpContext ctxt = new HttpContext(wr, 0);
// 設置發送結束異步回調函數
wr.SetEndOfSendNotification(this._asyncEndOfSendCallback, ctxt);
// 更新請求計數器
Interlocked.Increment(&(this._activeRequestCount));
try
{
// 檢查當前HttpRuntime實例是否第一次被調用
if(this._beforeFirstRequest)
{
lock(this)
{
// 使用 Double-Checked 模式 避免冗餘鎖定
if(this._beforeFirstRequest)
{
this._firstRequestStartTime = DateTime.UtcNow;
this.FirstRequestInit(ctxt); // 初始化當前 HttpRuntime 運行時環境
this._beforeFirstRequest = false;
}
}
}
// 根據配置文件設置,扮演具有較高特權的角色
ctxt.Impersonation.Start(true, false);
try
{
// 初始化頁面請求的返回對象
ctxt.Response.InitResponseWriter();
}
finally
{
ctxt.Impersonation.Stop();
}
// 獲取當前 Web 應用程序實例
IHttpHandler handler = HttpApplicationFactory.GetApplicationInstance(ctxt);
if (handler == null)
{
throw new HttpException(HttpRuntime.FormatResourceString("Unable_create_app_object"));
}
// 使用Web應用程序實例完成實際的頁面請求工作
if((handler as IHttpAsyncHandler) != null)
{
IHttpAsyncHandler asyncHandler = ((IHttpAsyncHandler) handler);
ctxt.AsyncAppHandler = asyncHandler;
// 使用異步處理機制
asyncHandler.BeginProcessRequest(ctxt, this._handlerCompletionCallback, ctxt);
}
else
{
handler.ProcessRequest(ctxt);
this.FinishRequest(ctxt.WorkerRequest, ctxt, null);
}
}
catch(Exception E)
{
ctxt.Response.InitResponseWriter();
this.FinishRequest(wr, ctxt, E);
}
}
HttpRuntime.ProcessRequestInternal函數中,涉及到文件預編譯的有兩部分:一是獲取當前 Web 應用程序實例時,會根據情況自動判斷是否預編譯Web 應用程序文件;二是在完成實際頁面請求時,會在第一次使用某個頁面時觸發預編譯行爲。
首先來看看對 Web 應用程序文件的處理。
HttpRuntime.ProcessRequestInternal函數中調用了HttpApplicationFactory.GetApplicationInstance函數獲取當前 Web 應用程序實例。System.Web.HttpApplicationFactory是一個內部類,用以實現對多個Web應用程序實例的管理和緩存。GetApplicationInstance函數返回的是一個IHttpHandler接口,提供IHttpHandler.ProcessRequest函數用於其後對Web頁面文件的處理。僞代碼如下:
以下爲引用:
internal static IHttpHandler HttpApplicationFactory.GetApplicationInstance(HttpContext ctxt)
{
// 定製應用程序
if(HttpApplicationFactory._customApplication != null)
{
return HttpApplicationFactory._customApplication;
}
// 調試請求
if(HttpDebugHandler.IsDebuggingRequest(ctxt))
{
return new HttpDebugHandler();
}
// 判斷是否需要初始化當前 HttpApplicationFactory 實例
if(!HttpApplicationFactory._theApplicationFactory._inited)
{
HttpApplicationFactory factory = HttpApplicationFactory._theApplicationFactory;
lock(HttpApplicationFactory._theApplicationFactory);
{
// 使用 Double-Checked 模式 避免冗餘鎖定
if(!HttpApplicationFactory._theApplicationFactory._inited)
{
// 初始化當前 HttpApplicationFactory 實例
HttpApplicationFactory._theApplicationFactory.Init(ctxt);
HttpApplicationFactory._theApplicationFactory._inited = true;
}
}
}
// 獲取 Web 應用程序實例
return HttpApplicationFactory._theApplicationFactory.GetNormalApplicationInstance(ctxt);
}
在處理特殊情況和可能的實例初始化之後,調用HttpApplicationFactory.GetNormalApplicationInstance函數完成獲取Web應用程序實例的實際功能,僞代碼如下:
以下爲引用:
private HttpApplication HttpApplicationFactory.GetNormalApplicationInstance(HttpContext context)
{
HttpApplication app = null;
// 嘗試從已施放的 Web 應用程序實例隊列中獲取
lock(this._freeList)
{
if(this._numFreeAppInstances > 0)
{
app = (HttpApplication)this._freeList.Pop();
this._numFreeAppInstances--;
}
}
if(app == null)
{
// 構造新的 Web 應用程序實例
app = (HttpApplication)System.Web.HttpRuntime.CreateNonPublicInstance(this._theApplicationType);
// 初始化 Web 應用程序實例
app.InitInternal(context, this._state, this._eventHandlerMethods);
}
return app;
}
構造新的 Web 應用程序實例的代碼很簡單,實際上就是對Activator.CreateInstance函數的簡單包裝,僞代碼如下:
以下爲引用:
internal static object HttpRuntime.CreateNonPublicInstance(Type type, object[] args)
{
return Activator.CreateInstance(type, BindingFlags.CreateInstance | BindingFlags.Instance |
BindingFlags.NonPublic | BindingFlags.Public, null, args, null);
}
internal static object HttpRuntime.CreateNonPublicInstance(Type type)
{
return HttpRuntime.CreateNonPublicInstance(type, null);
}
至此一個 Web 應用程序實例就被完整構造出來,再經過InitInternal函數的初始化,就可以開始實際頁面處理工作了。而HttpApplicationFactory實例的_theApplicationType類型,則是結果預編譯後的Global.asax類。實際的預編譯工作在HttpApplicationFactory.Init函數中完成,僞代碼如下:
以下爲引用:
private void HttpApplicationFactory.Init(HttpContext ctxt)
{
if(HttpApplicationFactory._customApplication != null)
return;
using(HttpContextWrapper wrapper = new HttpContextWrapper(ctxt))
{
ctxt.Impersonation.Start(true, true);
try
{
try
{
this._appFilename = HttpApplicationFactory.GetApplicationFile(ctxt);
this.CompileApplication(ctxt);
this.SetupChangesMonitor();
}
finally
{
ctxt.Impersonation.Stop();
}
}
catch(Object)
{
}
this.FireApplicationOnStart(ctxt);
}
}
GetApplicationFile函數返回Web請求物理目錄下的global.asax文件路徑;CompileApplication函數則根據此文件是否存在,判斷是預編譯之並載入編譯後類型,還是直接返回缺省的HttpApplication類型,僞代碼如下:
以下爲引用:
internal static string HttpApplicationFactory.GetApplicationFile(HttpContext ctxt)
{
return Path.Combine(ctxt.Request.PhysicalApplicationPath, "global.asax");
}
private void HttpApplicationFactory.CompileApplication(HttpContext ctxt)
{
if(FileUtil.FileExists(this._appFilename))
{
ApplicationFileParser parser;
// 獲取編譯後的 Web 應用程序類型
this._theApplicationType = ApplicationFileParser.GetCompiledApplicationType(this._appFilename, context, out parser);
this._state = new HttpApplicationState(parser1.ApplicationObjects, parser.SessionObjects);
this._fileDependencies = parser.SourceDependencies;
}
else
{
this._theApplicationType = typeof(HttpApplication);
this._state = new HttpApplicationState();
}
this.ReflectOnApplicationType();
}
分析到這裏我們可以發現,內部類型System.Web.UI.ApplicationFileParser的GetCompiledApplicationType函數是實際上進行Web應用程序編譯工作的地方。但現在我們暫且打住,等下一節分析編譯過程時再詳細解說。 :)
然後我們看看對 Web 頁面文件的處理。
在前面分析HttpRuntime.ProcessRequestInternal函數時我們曾瞭解到,在獲得了Web應用程序實例後,會使用此實例的IHttpAsyncHandler接口或IHttpHandler接口,完成實際的頁面請求工作。而無論有否Global.asax文件,最終返回的Web應用程序實例都是一個HttpApplication類或其子類的實例,其實現了IHttpAsyncHandler接口,支持異步的Web頁面請求工作。對此接口的處理僞代碼如下:
以下爲引用:
private void HttpRuntime.ProcessRequestInternal(HttpWorkerRequest wr)
{
...
// 使用Web應用程序實例完成實際的頁面請求工作
if((handler as IHttpAsyncHandler) != null)
{
IHttpAsyncHandler asyncHandler = ((IHttpAsyncHandler) handler);
ctxt.AsyncAppHandler = asyncHandler;
// 使用異步處理機制
asyncHandler.BeginProcessRequest(ctxt, this._handlerCompletionCallback, ctxt);
}
else
{
handler.ProcessRequest(ctxt);
this.FinishRequest(ctxt.WorkerRequest, ctxt, null);
}
...
}
HttpRuntime.ProcessRequestInternal函數通過調用HttpApplication.IHttpAsyncHandler.BeginProcessRequest函數開始頁面請求工作。而HttpApplication實際上根本不支持同步形式的IHttpHandler接口,僞代碼如下:
以下爲引用:
void HttpApplication.ProcessRequest(System.Web.HttpContext context)
{
throw new HttpException(HttpRuntime.FormatResourceString("Sync_not_supported"));
}
bool HttpApplication.get_IsReusable()
{
return true;
}
而在HttpApplication.IHttpAsyncHandler.BeginProcessRequest函數中,將完成非常複雜的異步調用後臺處理操作,這兒就不多羅嗦了,等有機會寫篇文章專門討論一下ASP.NET中的異步操作再說。而其最終調用還是使用System.Web.UI.PageParser對需要處理的Web頁面進行解析和編譯。
最後我們看看對用戶自定義控件文件的處理。
Page類的LoadControl函數實際上是在抽象類TemplateControl中實現的,僞代碼如下:
以下爲引用:
public Control LoadControl(string virtualPath)
{
virtualPath = UrlPath.Combine(base.TemplateSourceDirectory, virtualPath);
Type type = UserControlParser.GetCompiledUserControlType(virtualPath, null, base.Context);
return this.LoadControl(type1);
}
實際的用戶自定義控件預編譯操作還是在UserControlParser類中完成的。
至此,在這一節中我們已經大致瞭解了ASP.NET自動預編譯的實現原理,以及在什麼時候對頁面文件進行預編譯。下一節我們將詳細分析ApplicationFileParser、PageParser和UserControlParser,瞭解ASP.NET是如何對頁面文件進行預編譯的。