開篇:上一篇我們瞭解了一個ASP.Net頁面請求的核心處理入口,它經歷了三個重要的入口,分別是:ISAPIRuntime.ProcessRequest()、HttpRuntime.ProcessRequest()以及HttpApplication.Init()。其中,在HttpApplication的Init()方法中觸發了請求處理管道事件的執行,本篇我們就來看看所謂的請求處理管道。
一、所謂“請求處理管道”
HttpApplication物件是ASP.NET中處理請求的重要物件,但是,這種型別的物件例項不是由程式設計師來建立的,而是由ASP.NET幫助我們建立的。為了便於擴充套件處理工作,HttpApplication採用處理管道的方法進行處理,將處理的過程分為多個步驟,每個步驟通過事件的形式暴露給程式設計師,這些事件按照固定的處理順序依次觸發,程式設計師通過編寫事件處理方法就可以自定義每一個請求的擴充套件處理過程。
①傳說中的19個事件
對於HttpApplication來說,到ASP.NET 4.0版本,提供了19個重要的標準事件,如下圖所示:
在整個請求處理管道中,HttpContext上下文被依次傳輸到各個處理事件中,由不同的處理單元(HttpModule、HttpHandler、Page等)進行處理。從這裡可以看出,ASP.NET請求處理管道就像是一個大型的AOP框架。
②HttpModule與HttpHandler
在進一步深入瞭解之前,讓我們先來了解一下什麼是HttpModule和HttpHandlers。他們幫助我們在ASP.NET頁面處理過程的前後注入自定義的邏輯處理。他們之間主要的差別在於:
- 如果你想要注入的邏輯是基於像’.aspx’,’.html’這樣的擴充套件檔案,那麼你可以使用HttpHandler。換句話說,HttpHandler是一個基於處理器的擴充套件。
- HttpHandler總結:在ASP.NET WebForm中,無論是一般處理程式還是WebPage都實現了IHttpHandler介面,而ASP.NET MVC中也有MvcHandler實現了IHttpHandler介面;
- 如果你想要在ASP.NET管道事件中注入邏輯,那麼你可以使用HttpModule。也可以說,HttpModule是一個基於處理器的事件。
- HttpModule總結:剛剛我們說到ASP.NET請求處理管道就像是一個大型的AOP框架,因此我們可以藉助HttpModule自定義地註冊或移除一些事件邏輯,以完成我們想要的效果。ASP.NET預設實現了針對WebForm和MVC的HttpModule,像ASP.NET MVC中預設使用的是UrlRoutingModule。具體實現方式是:通過改寫Global檔案或自定義一個實現IHttpModule介面的類並在Web.config中進行註冊。
1 2 3 4 5 6 7 8 |
<?xml version="1.0"?> <configuration> <system.web> <httpModules> <add name="myHttpModule" type="FirstModule"/> </httpModules> </system.web> </configuration> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class FirstModule : IHttpModule { public void Dispose() { throw new NotImplementedException(); } public void Init(HttpApplication context) { context.BeginRequest += new EventHandler(context_BeginRequest); } void context_BeginRequest(object sender, EventArgs e) { HttpApplication application = sender as HttpApplication; application.Context.Response.Write("第三方過濾器:哇哈哈!"); } } |
③19個事件中我們可以做些什麼?
一個十分有價值的問題就是在什麼事件中我們又可以做些什麼?下表就展示了這個問題的答案:
Section | Event | Description |
HttpModule | BeginRequest | 此事件標誌著一個新的請求,它保證在每個請求中都會被觸發。 |
HttpModule | AuthenticateRequest | 此事件標誌ASP.NET執行時準備驗證使用者。任何身份驗證程式碼都可以在此注入。 |
HttpModule | AuthorizeRequest | 此事件標誌ASP.NET執行時準備授權使用者。任何授權程式碼都可以在此注入。 |
HttpModule | ResolveRequest | 在ASP.NET中我們通常使用OutputCache指令做快取。在這個事件中,ASP.NET執行時確定是否能夠從快取中載入頁面,而不是從頭開始生成。任何快取的具體活動可以被注入這裡。 |
HttpModule | AcquireRequestState | 此事件標誌著ASP.NET執行時準備獲得Session會話變數。可以對Session變數做任何你想要做的處理。 |
HttpModule | PreRequestHandlerExecute | 恰好在ASP.NET 開始執行事件處理程式前發生。可以預處理你想做的事。 |
HttpHandler | ProcessRequest | HttpHandler邏輯被執行。在這個部分我們將為每個頁面擴充套件寫需要的邏輯。 |
Page | Init | 此事件發生在ASP.NET頁面且可以用來: 1、動態地建立控制元件,如果你一定要在執行時建立控制元件; 2、任何初始化設定 3、母版頁及其設定 在這部分中我們沒有獲得viewstate、postedvalues及已經初始化的控制元件。 |
Page | Load | 在這部分ASP.NET控制元件完全被載入且在這裡你可以寫UI操作邏輯或任何其他邏輯。NOTE:這個事件也是我們最常見且最常用的一個事件。 |
Page | Validate | 如果在頁面上你有驗證器,你同樣想在這裡做一下檢查。 |
Page | Render | 是時候將輸出傳送到瀏覽器。如果你想對最終的HTML做些修改,你可以在這裡輸入你的HTML邏輯。 |
Page | Unload | 頁面物件從記憶體中解除安裝。 |
HttpModule | PostRequestHandlerExecute | 可以注入任何你想要的邏輯,在處理程式執行之後。 |
HttpModule | ReleaseRequestState | 如果你想要儲存對某些狀態變數的更改,例如:Session變數的值。 |
HttpModule | UpdateRequestCache | 在結束之前,你是否想要更新你的快取。 |
HttpModule | EndRequest | 這是將輸出傳送到客戶端瀏覽器之前的最後一個階段。 |
④自定義處理邏輯
我們可以通過一個示例程式程式碼來展示以上介紹的那些事件是怎樣被最終觸發的。在這個示例中,我們已經建立了一個HttpModule和HttpHandler,並且也在所有的事件中通過新增自定義邏輯程式碼展示了一個簡單的響應。
下面是HttpModule類,它跟蹤了所有的事件並將其新增到了一個全域性的集合中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
public class clsHttpModule : IHttpModule { ...... void OnUpdateRequestCache(object sender, EventArgs a) { objArrayList.Add("httpModule:OnUpdateRequestCache"); } void OnReleaseRequestState(object sender, EventArgs a) { objArrayList.Add("httpModule:OnReleaseRequestState"); } void OnPostRequestHandlerExecute(object sender, EventArgs a) { objArrayList.Add("httpModule:OnPostRequestHandlerExecute"); } void OnPreRequestHandlerExecute(object sender, EventArgs a) { objArrayList.Add("httpModule:OnPreRequestHandlerExecute"); } void OnAcquireRequestState(object sender, EventArgs a) { objArrayList.Add("httpModule:OnAcquireRequestState"); } void OnResolveRequestCache(object sender, EventArgs a) { objArrayList.Add("httpModule:OnResolveRequestCache"); } void OnAuthorization(object sender, EventArgs a) { objArrayList.Add("httpModule:OnAuthorization"); } void OnAuthentication(object sender, EventArgs a) { objArrayList.Add("httpModule:AuthenticateRequest"); } void OnBeginrequest(object sender, EventArgs a) { objArrayList.Add("httpModule:BeginRequest"); } void OnEndRequest(object sender, EventArgs a) { objArrayList.Add("httpModule:EndRequest"); objArrayList.Add("<hr>"); foreach (string str in objArrayList) { httpApp.Context.Response.Write(str + "<br>") ; } } } |
下面是HttpHandler類的一個程式碼片段,它跟蹤了ProcessRequest事件。
1 2 3 4 5 6 7 |
public class clsHttpHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { clsHttpModule.objArrayList.Add("HttpHandler:ProcessRequest"); context.Response.Redirect("Default.aspx"); } } |
同上,我們也可以跟蹤來自ASP.NET Page頁面的所有事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
public partial class _Default : System.Web.UI.Page { protected void Page_init(object sender, EventArgs e) { clsHttpModule.objArrayList.Add("Page:Init"); } protected void Page_Load(object sender, EventArgs e) { clsHttpModule.objArrayList.Add("Page:Load"); } public override void Validate() { clsHttpModule.objArrayList.Add("Page:Validate"); } protected void Button1_Click(object sender, EventArgs e) { clsHttpModule.objArrayList.Add("Page:Event"); } protected override void Render(HtmlTextWriter output) { clsHttpModule.objArrayList.Add("Page:Render"); base.Render(output); } protected void Page_Unload(object sender, EventArgs e) { clsHttpModule.objArrayList.Add("Page:UnLoad"); } } |
下圖則顯示了上面我們所討論的所有事件的執行順序:
二、WebForm經歷的管道事件概覽
在ASP.NET WebForm應用中,其在請求處理管道中主要經歷了三個重要階段:
①在第八個事件中建立Page類物件並轉換為IHttpHandler介面
從上面的介紹中可以看到,第八個事件是:PostMapRequestHandler。在這個事件中,對於訪問不同的資源型別,ASP.NET具有不同的HttpHandler對其程式處理。對於每個請求,ASP.NET會通過副檔名選擇匹配相應的HttpHandler型別,成功匹配後,該實現被觸發。因此,如果請求的副檔名是.aspx,便會生成Page類物件,而Page類物件是實現了IHttpHandler介面的。
②在第九個到第十事件之間根據SessionId獲取Session
從上面的介紹中可以看到,第九到第十個事件是:AcquireRequestState,PostAcquireRequestState。這期間首先會接收到瀏覽器發過來的SessionId,然後先會將IHttpHandler介面嘗試轉換為IRequiresSessionState介面,如果轉換成功,ASP.NET會根據這個SessionId到伺服器的Session池中去查詢所對應的Session物件,並將這個Session物件賦值到HttpContext物件的Session屬性。如果嘗試轉換為IRequiresSessionState介面不成功,則不載入Session。
③在第十一個事件與第十二個事件之間執行頁面生命週期
從上面的介紹中可以看到,第十一和第十二個事件是:PreRequestHandlerExecute,PostRequestHandlerExecute。在這兩個事件之間,ASP.NET最終通過請求資源型別相對應的HttpHandler實現對請求的處理,其實現方式是呼叫在第八個事件建立的頁面物件的ProcessRequest方法。
在FrameworkInitialize()這個方法內部就開始打造WebForm的頁面控制元件樹,在其中呼叫了ProcessRequestMain方法,在這個方法裡面就執行了整個ASP.NET WebFom頁面生命週期。至於WebForm頁面生命週期的細節,我們在本系列後續的Part 4再來細細研究。
當我們直接使用*.ashx頁面的時候,它的ProcessRequest()方法就直接呼叫了一個FrameworkInitialize(),並最終生成響應報文,傳送回客戶端。
當我們在使用*.aspx頁面的時候,它繼承自Page類,而Page類實現了IHttpHandler介面,然後了呼叫Page類的ProcessRequest()方法,其中會構建頁面控制元件樹,然後一個一個地去呈現。
三、ASP.NET MVC經歷的管道事件概覽
在ASP.NET MVC中,最核心的當屬“路由系統”,而路由系統的核心則源於一個強大的System.Web.Routing.dll元件。
在這個System.Web.Routing.dll中,有一個最重要的類叫做UrlRoutingModule,它是一個實現了IHttpModule介面的類,在請求處理管道中專門針對ASP.NET MVC請求進行處理。首先,我們要了解一下UrlRoutingModule是如何起作用的。
(1)IIS網站的配置可以分為兩個塊:全域性 Web.config 和本站 Web.config。Asp.Net Routing屬於全域性性的,所以它配置在全域性Web.Config 中,我們可以在如下路徑中找到:“$\Windows\Microsoft.NET\Framework\版本號\Config\Web.config“
1 2 3 4 5 6 7 8 9 |
<?xml version="1.0" encoding="utf-8"?> <!-- the root web configuration file --> <configuration> <system.web> <httpModules> <add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" /> </httpModules> </system.web> </configuration> |
(2)通過在全域性Web.Config中註冊 System.Web.Routing.UrlRoutingModule,IIS請求處理管道接到請求後,就會載入 UrlRoutingModule型別的Init()方法。其原始碼入下:
View Code
從原始碼中可以看出,在UrlRoutingModule中為請求處理管道中的第七個事件PostResolveRequestCache註冊了一個事件處理方法:OnApplicationPostResolveRequestCache。從這裡可以看出:ASP.NET MVC的入口在UrlRoutingModule,即訂閱了HttpApplication的第7個管道事件PostResolveRequestCahce。換句話說,是在HtttpApplication的第7個管道事件處對請求進行了攔截。
現在我們將ASP.NET MVC的請求處理分為兩個重要階段來看看:
①在第七個事件中建立實現了IHttpHandler介面的MvcHandler
當請求到達UrlRoutingModule的時候,UrlRoutingModule取出請求中的Controller、Action等RouteData資訊,與路由表中的所有規則進行匹配,若匹配,把請求交給IRouteHandler,即MVCRouteHandler。我們可以看下UrlRoutingModule的原始碼來看看,以下是幾句核心的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public virtual void PostResolveRequestCache(HttpContextBase context) { // 通過RouteCollection的靜態方法GetRouteData獲取到封裝路由資訊的RouteData例項 RouteData routeData = this.RouteCollection.GetRouteData(context); if (routeData != null) { // 再從RouteData中獲取MVCRouteHandler IRouteHandler routeHandler = routeData.RouteHandler; ...... if (!(routeHandler is StopRoutingHandler)) { ...... // 呼叫 IRouteHandler.GetHttpHandler(),獲取的IHttpHandler 型別例項,它是由 IRouteHandler.GetHttpHandler獲取的,這個得去MVC的原始碼裡看 IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext); ...... // 合適條件下,把之前將獲取的IHttpHandler 型別例項 對映到IIS HTTP處理管道中 context.RemapHandler(httpHandler); } } } |
MVCRouteHandler的作用是用來生成實現IHttpHandler介面的MvcHandler:
1 2 3 4 5 6 7 |
namespace System.Web.Routing { public interface IRouteHandler { IHttpHandler GetHttpHandler(RequestContext requestContext); } } |
那麼,MvcRouteHandler從何而來呢?眾所周知,ASP.NET MVC專案啟動是從Global中的Application_Start()方法開始的,那就去看看它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { ...... //這裡要註冊路由了 RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); } } public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); // 玄機就在這了,這個MapRoute位於System.Web.Mvc.RouteCollectionExtensions routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } } |
於是,我們再去看看Route類的這個MapRoute()方法的原始碼:
1 2 3 4 5 6 7 8 9 10 11 12 |
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces) { ...... // 終於等到你,還好我沒放棄。 Route route = new Route(url, new MvcRouteHandler()) { Defaults = new RouteValueDictionary(defaults), Constraints = new RouteValueDictionary(constraints), DataTokens = new RouteValueDictionary() }; ...... return route; } |
從上面的原始碼可以得知為什麼可以從RouteData中拿到MvcRouteHadnler?因為當我們在HttpApplication的第一個管道事件,使用MapRoute()方法註冊路由的時候,已經通過Route類的建構函式把MvcRouteHandler注入到路由中了。
剛剛我們知道MvcRouteHandler是用來生成實現IHttpHandler介面的MvcHandler,那麼我們繼續從UrlRoutingModule的原始碼可以看到,通過HttpHandler的GetHttpHandler()方法獲取到了實現了IHttpHandler介面的MvcHandler:
1 2 3 4 5 |
// 呼叫 IRouteHandler.GetHttpHandler(),獲取的IHttpHandler 型別例項,它是由 IRouteHandler.GetHttpHandler獲取的,這個得去MVC的原始碼裡看 IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext); ...... // 合適條件下,把之前將獲取的IHttpHandler 型別例項 對映到IIS HTTP處理管道中 context.RemapHandler(httpHandler); |
於是,我們進入ASP.NET MVC的原始碼看看MvcHandlerd的實現,這裡我看的是MVC 4.0的原始碼:
1 2 3 4 5 |
protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext) { requestContext.HttpContext.SetSessionStateBehavior(GetSessionStateBehavior(requestContext)); return new MvcHandler(requestContext); } |
可以看出,在這裡建立了MvcHandler例項。換句話說,MvcRouteHandler把請求交給了MvcHandler去做請求處理管道中後續事件的處理操作了。
②在第十一個事件與第十二個事件之間呼叫MvcHandler的ProcessRequest()方法
(1)在WebForm中,此階段會呼叫Page類物件的ProcessRequest()方法。在ASP.NET MVC中,會呼叫MvcHandler的ProcessRequest()方法,此方法會啟用具體請求的Controller類物件,觸發Action方法,返回ActionResult例項。
(2)如果ActionResult是非ViewResult,比如JsonResult, ContentResult,這些內容將直接被輸送到Response響應流中,顯示給客戶端;如果是ViewResult,就會進入下一個渲染檢視環節。
(3)在渲染檢視環節,ViewEngine找到需要被渲染的檢視,View被載入成WebViewPage<TModel>型別,並渲染生成Html,最終返回Html。
TIP:有關此ProcessRequest()處理環節的詳細內容,請等待本系列Part 5中的介紹。
參考資料
致謝:本文參閱了大量園友的文章,也直接使用了大量園友製作的圖,在此對以下各位園友表示感謝。
(1)Darren Ji,《ASP.NET MVC請求處理管道宣告週期的19個關鍵環節》:http://www.cnblogs.com/darrenji/p/3795661.html
(2)木宛城主,《ASP.NET那點不為人知的事兒》:http://www.cnblogs.com/OceanEyes/archive/2012/08/13/aspnetEssential-1.html
(3)Tony He,《ASP.NET請求處理機制》:http://www.cnblogs.com/cilence/archive/2012/05/28/2520712.html
(4)兩會的部落格,《IIS是怎樣處理ASP.NET請求的》:http://www.cnblogs.com/hkncd/archive/2012/03/23/2413917.html
(5)wjn2000,《ASP.NET請求處理過程(IIS6)》:http://www.cnblogs.com/wjn2010/archive/2011/04/21/2024341.html
(6)農村出來的大學生,《ASP.NET網頁請求處理全過程(反編譯)》:http://www.cnblogs.com/poorpan/archive/2011/09/25/2190308.html
(7)碧血軒,《ASP.NET頁面生命週期》,http://www.cnblogs.com/xhwy/archive/2012/05/20/2510178.html
(8)吳秦,《ASP.NET 應用程式與頁面生命週期(意譯)》,http://www.cnblogs.com/skynet/archive/2010/04/29/1724020.html
(9)我自己,《【翻譯】ASP.NET應用程式和頁面宣告週期》:http://www.cnblogs.com/edisonchou/p/3958305.html
(10)Shivprasad koirala,《ASP.NET Application and Page Life Cycle》:http://www.codeproject.com/Articles/73728/ASP-NET-Application-and-Page-Life-Cycle
(11)學而不思則罔,《ASP.NET Routing與MVC之一:請求如何到達MVC》:http://www.cnblogs.com/acejason/p/3869731.html
(12)初心不可忘,《綜述:ASP.NET MVC請求處理管道》:http://www.cnblogs.com/luguobin/archive/2013/03/15/2962458.html