文章內容
根據對Http Runtime和Http Pipeline的分析,我們知道一個ASP.NET應用程式可以有多個HttpModuel,但是隻能有一個HttpHandler,並且通過這個HttpHandler的BeginProcessRequest(或ProcessRequest)來處理並返回請求,前面的章節將到了再MapHttpHandler這個週期將會根據請求的URL來查詢對應的HttpHandler,那麼它是如何查詢的呢?
一起我們在做自定義HttpHandler的時候,需要執行URL以及副檔名匹配規則,然後查詢HttpHandler的時候就是根據相應的規則來查詢哪個HttpHandler可以使用。另一方面我們本系列教材講的MVC就是通過註冊路由(Route)來匹配到對應的Controller和Action上的,例如Global.asax裡的程式碼:
routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }
但是在匹配這個之前,MVC首先要接管請求才能處理,也就是說我們要有對應MVC的HttpHandler(後面知道它的名字叫MvcHandler)被MapRequestHandler週期的處理引擎查詢到並且應用上才行,然後後面才能由 Controller/Action執行。另外一方面,由於該URL地址沒有副檔名,所以無法進入ASP.NET的RunTime,MVC2的實現方式是:註冊萬用字元(*.*)對映到aspnet_ISPAI.dll,然後通過一個自定義的UrlRoutingModuel來匹配Route規則,再繼續處理,但是MVC3的時候,匹配Route規則的處理機制整合到ASP.NET4.0裡了,也就是今天我們這篇文章所要講的主角(UrlRoutingModule)的處理機制。
先來看UrlRoutingModule的原始碼,無容置疑地這個類是繼承於IHttpModule,首先看一下Init方法的程式碼:
protected virtual void Init(HttpApplication application) { ////////////////////////////////////////////////////////////////// // Check if this module has been already addded if (application.Context.Items[_contextKey] != null) { return; // already added to the pipeline } application.Context.Items[_contextKey] = _contextKey; // Ideally we would use the MapRequestHandler event. However, MapRequestHandler is not available // in II6 or IIS7 ISAPI Mode. Instead, we use PostResolveRequestCache, which is the event immediately // before MapRequestHandler. This allows use to use one common codepath for all versions of IIS. application.PostResolveRequestCache += OnApplicationPostResolveRequestCache; }
該程式碼在PostResolveRequestCache週期事件上新增了我們需要執行的方法,用於URL匹配規則的設定,但是為什麼要在這個週期點上新增事件呢?看了註釋,再結合我們前面對Pipeline的瞭解,釋然了,要像動態註冊自己的HttpHandler,那就需要在MapRequestHandler之前進行註冊自己的規則(因為這個週期點就是做這個事情的),但由於IIS6不支援這個事件,所以為了能讓IIS6也能執行MVC3,所以我們需要在這個週期之前的PostResolveRequestCache的事件點上去註冊我們的規則,也許如果IIS6被微軟廢棄以後,就會將這個事件新增到真正的開始點MapRequestHandler上哦。
我們繼續來看註冊該事件的OnApplicationPostResolveRequestCache方法的程式碼:
public virtual void PostResolveRequestCache(HttpContextBase context) { // Match the incoming URL against the route table RouteData routeData = RouteCollection.GetRouteData(context); // Do nothing if no route found if (routeData == null) { return; } // If a route was found, get an IHttpHandler from the route's RouteHandler IRouteHandler routeHandler = routeData.RouteHandler; if (routeHandler == null) { throw new InvalidOperationException( String.Format( CultureInfo.CurrentUICulture, SR.GetString(SR.UrlRoutingModule_NoRouteHandler))); } // This is a special IRouteHandler that tells the routing module to stop processing // routes and to let the fallback handler handle the request. if (routeHandler is StopRoutingHandler) { return; } RequestContext requestContext = new RequestContext(context, routeData); // Dev10 766875 Adding RouteData to HttpContext context.Request.RequestContext = requestContext; IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext); if (httpHandler == null) { throw new InvalidOperationException( String.Format( CultureInfo.CurrentUICulture, SR.GetString(SR.UrlRoutingModule_NoHttpHandler), routeHandler.GetType())); } if (httpHandler is UrlAuthFailureHandler) { if (FormsAuthenticationModule.FormsAuthRequired) { UrlAuthorizationModule.ReportUrlAuthorizationFailure(HttpContext.Current, this); return; } else { throw new HttpException(401, SR.GetString(SR.Assess_Denied_Description3)); } } // Remap IIS7 to our handler context.RemapHandler(httpHandler); }
我已經加粗了4行重要的程式碼,第一行是通過傳遞HttpContext引數,從RouteCollection找到對應的靜態屬性RouteData( GetRouteData方法裡會先判斷真實檔案是否存在,如果不存在才去找RouteData),第二行然後從RouteData的屬性RouteHandler獲取一個IRouteHandler的例項,第三行是從該例項裡獲取對應的IHttpHandler例項,第4行是呼叫HttpContext的RemapHandler方法重新map新的handler(這行程式碼的註釋雖然說是remap IIS7,其實IIS6也是用了,只不過判斷該方法裡對IIS7整合模式多了一點特殊處理而已),然後可以通過HttpContext. RemapHandlerInstance屬性來得到這個例項。
關於Route/RouteData/RouteCollection/IRouteHandler的作用主要就是定義URL匹配到指定的IHttpHandler,然後註冊進去,具體實現我們稍後再講,現在先看一下Http Pipeline裡是如何找到這個IHttpHandler例項的,由於IIS6和IIS7整合模式是差不多的,前面的文章我們提到了都是最終呼叫到IHttpHandlerFactory的例項,然後從中獲取IHttpHandler,所以我們這裡只分析IIS6和IIS7經典模式的實現。
先來看BuildSteps裡查詢HttpHandler的方法MapHandlerExecutionStep的程式碼,只有幾行程式碼,最重要的是:
context.Handler = _application.MapHttpHandler( context, request.RequestType, request.FilePathObject, request.PhysicalPathInternal, false /*useAppConfig*/);
MapHttpHandler就是我們要查詢Handler的方法了,來仔細看看程式碼:
internal IHttpHandler MapHttpHandler(HttpContext context, String requestType, VirtualPath path, String pathTranslated, bool useAppConfig) { // Don't use remap handler when HttpServerUtility.Execute called IHttpHandler handler = (context.ServerExecuteDepth == 0) ? context.RemapHandlerInstance : null; using (new ApplicationImpersonationContext()) { // Use remap handler if possible if (handler != null){ return handler; } // Map new handler HttpHandlerAction mapping = GetHandlerMapping(context, requestType, path, useAppConfig); // If a page developer has removed the default mappings with <httpHandlers><clear> // without replacing them then we need to give a more descriptive error than // a null parameter exception. if (mapping == null) { PerfCounters.IncrementCounter(AppPerfCounter.REQUESTS_NOT_FOUND); PerfCounters.IncrementCounter(AppPerfCounter.REQUESTS_FAILED); throw new HttpException(SR.GetString(SR.Http_handler_not_found_for_request_type, requestType)); } // Get factory from the mapping IHttpHandlerFactory factory = GetFactory(mapping); // Get factory from the mapping try { // Check if it supports the more efficient GetHandler call that can avoid // a VirtualPath object creation. IHttpHandlerFactory2 factory2 = factory as IHttpHandlerFactory2; if (factory2 != null) { handler = factory2.GetHandler(context, requestType, path, pathTranslated); } else { handler = factory.GetHandler(context, requestType, path.VirtualPathString, pathTranslated); } } catch (FileNotFoundException e) { if (HttpRuntime.HasPathDiscoveryPermission(pathTranslated)) throw new HttpException(404, null, e); else throw new HttpException(404, null); } catch (DirectoryNotFoundException e) { if (HttpRuntime.HasPathDiscoveryPermission(pathTranslated)) throw new HttpException(404, null, e); else throw new HttpException(404, null); } catch (PathTooLongException e) { if (HttpRuntime.HasPathDiscoveryPermission(pathTranslated)) throw new HttpException(414, null, e); else throw new HttpException(414, null); } // Remember for recycling if (_handlerRecycleList == null) _handlerRecycleList = new ArrayList(); _handlerRecycleList.Add(new HandlerWithFactory(handler, factory)); } return handler; }
從程式碼可以看出,首先如果當前頁面使用了HttpServerUtility.Execute進行頁面內跳轉,就不使用我們通過路由設定的HttpHandler(也就是HttpContent.RemapHandlerInstance屬性),如果沒有跳轉,就使用,並且優先順序是第一的,只有當不設定任何基於Route的HttpHandler,才走剩餘的匹配規則(也就是之前ASP.NET預設的按照副檔名類匹配的,這部分和我們關係不大就不做詳細分析了)。
好了,知道了UrlRouteModuel的大概機制,我們再回頭看看如何通過Route/RouteData/RouteCollection/IRouteHandler這幾個類來實現動態註冊Route規則的,先來看Route的程式碼:
[TypeForwardedFrom("System.Web.Routing, Version=3.5.0.0, Culture=Neutral, PublicKeyToken=31bf3856ad364e35")] public class Route : RouteBase { public Route(string url, IRouteHandler routeHandler) { Url = url; RouteHandler = routeHandler; } public Route(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler) { Url = url; Defaults = defaults; Constraints = constraints; RouteHandler = routeHandler; } //省略部分程式碼 public override RouteData GetRouteData(HttpContextBase httpContext) { // Parse incoming URL (we trim off the first two chars since they're always "~/") string requestPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo; RouteValueDictionary values = _parsedRoute.Match(requestPath, Defaults); if (values == null) { // If we got back a null value set, that means the URL did not match return null; } RouteData routeData = new RouteData(this, RouteHandler); // Validate the values if (!ProcessConstraints(httpContext, values, RouteDirection.IncomingRequest)) { return null; } // Copy the matched values foreach (var value in values) { routeData.Values.Add(value.Key, value.Value); } // Copy the DataTokens from the Route to the RouteData if (DataTokens != null) { foreach (var prop in DataTokens) { routeData.DataTokens[prop.Key] = prop.Value; } } return routeData; } }
Route程式碼提供了一系列的建構函式過載(我們這裡只列出了兩個),建構函式主要是傳入URL和對應的IRouteHandler例項以及約束規則(比如正則等),然後提供了一個最重要的GetRouteData方法,用於將Route自身和IRouteHandler組裝成RouteData,然後返回(中途也會驗證相應的約束條件,比如是否符合某個正規表示式),RouteData類本身沒有什麼邏輯,只是暴露了Route和RouteHandler屬性。
我們再來看RouteCollection,該類儲存了所有的Route規則(即URL和對應的IRouteHandler),通過靜態屬性RouteTable.Routes來獲取RouteCollection例項,通過UrlRoutingModule裡暴露的RouteCollection屬性我們可以驗證這一點:
public RouteCollection RouteCollection { get { if (_routeCollection == null) { _routeCollection = RouteTable.Routes; } return _routeCollection; } set { _routeCollection = value; } }
還有一個需要注意的,RouteHandler繼承的IRouteHandler的程式碼:
public interface IRouteHandler { IHttpHandler GetHttpHandler(RequestContext requestContext); }
該程式碼只提供了一個GetHttpHandler方法,所有實現這個介面的類需要實現這個方法,MVCHandler就是這麼實現的(下一章節我們再細看)。
至此,我們應該有一個清晰的認識了,我們通過全域性靜態屬性集合(RouteTable.Routes)去新增各種各樣的Route(但應該在HttpModule初始化週期之前),然後通過UrlRoutingModule負責註冊Route以及對應的IRouteHandler例項(IRouteHandler例項可以通過GetHttpHandler獲取IHttpHandler),最終實現根據不同URL來接管不同的HttpHandler。
MVC正是利用HttpApplication建立的週期(Application_Start方法)來新增了我們所需要的Route規則,當然在新增規則的時候帶上了MVCHandler這個重要的HttpHandler,
程式碼如下:
protected void Application_Start() { RegisterRoutes(RouteTable.Routes); } public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); }
MapRoute方法是一個擴充套件方法,通過該擴充套件方法註冊Route是個不錯的方法,下一章節,我們講講解MVC是如何註冊自己的MVCRouteHandler例項以及如何實現MVCHandler的呼叫的。
參考資料:
http://www.cnblogs.com/me-sa/archive/2009/06/01/MVCLifecycle.html
http://www.cnblogs.com/zhaoyang/archive/2011/11/16/2251200.html
同步與推薦
本文已同步至目錄索引:MVC之前的那點事兒系列
MVC之前的那點事兒系列文章,包括了原創,翻譯,轉載等各型別的文章,如果對你有用,請推薦支援一把,給大叔寫作的動力。