.NET 1.1中預編譯ASP.NET頁面實現原理淺析 [1] 自動預編譯機制淺析


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是如何對頁面文件進行預編譯的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章