MVC之前的那點事兒系列(9):MVC如何在Pipeline中接管請求的?


文章內容

上個章節我們講到了,可以在HttpModules初始化之前動態添加Route的方式來自定義自己的HttpHandler,最終接管請求的,那MVC是這么實現的么?本章節我們就來分析一下相關的MVC源碼來驗證一下我們的這個問題。

先創建一個MVC3的Web Application,選擇默認的模板以便創建以后就默認包含HomeController和AccountController。我們知道MVC要先接管請求才能通過這些Controller來處理,那我們先去Global.asax.cs文件里看代碼(定義接管請求要在初始化HttpModule之前,所以只能到這里來找代碼(或者是利用WebActivator之類的特性來動態添加),Global.asax.cs文件里代碼很少,但是有我們需要的東西,首先在Application_Start的方法里發現一行代碼:

RegisterRoutes(RouteTable.Routes);

這行代碼,看調用的方法名稱RegisterRoutes是注冊Route的意思,但是為什么參數卻是全局的RouteTable.Routes集合呢?找到RegisterRoutes方法來看看具體的內容:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        "Default", // Route name
        "{controller}/{action}/{id}", // URL with parameters
        new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
    );
}

該方法有2行代碼,第一行是忽略一個Route(我們先不看這個),第二行是使用MapRoute方法注冊一個新的Route,默認是映射到Home Controller的Index Action上,我們可能想到了,RouteCollection(也就是剛才傳入的RouteTable.Routes)的MapRoute方法就是提供我們所說的接管請求的入口,但是如何把MVC自己的HttpHandler傳進去的呢?我們Go to一下這個MapRoute方法(需要安裝ReShaper來查找MVC的源碼),調整到了MVC的RouteCollectionExtensions類,發現MapRoute並不是RouteCollection自帶的方法,而是在MVC源碼里提供的一個擴展方法,代碼如下:

public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces)
{
    if (routes == null)
    {
        throw new ArgumentNullException("routes");
    }
    if (url == null)
    {
        throw new ArgumentNullException("url");
    }

    Route route = new Route(url, new MvcRouteHandler())
    {
        Defaults = new RouteValueDictionary(defaults),
        Constraints = new RouteValueDictionary(constraints),
        DataTokens = new RouteValueDictionary()
    };

    if ((namespaces != null) && (namespaces.Length > 0))
    {
        route.DataTokens["Namespaces"] = namespaces;
    }

    routes.Add(name, route);

    return route;
}

該代碼的主要作用是new一個新的Route,然后將該Route添加到我們剛才提到的靜態集合RouteTable.Routes里,以便后期查找Handler的時候使用,OK,這一步符合我們前面章節的分析。

接下來看,Route是如何new出來的,代碼里的參數傳入的分別是我們知道的url,以及一個MVCRouteHandler的實例,這一步也符合我們前面的分析,那我們來看一下MVCRouteHandler的GetHttpHandler方法是如何實現的獲取MVCHandler的:

    public class MvcRouteHandler : IRouteHandler { 
        private IControllerFactory _controllerFactory;
 
        public MvcRouteHandler() { 
        }
 
        public MvcRouteHandler(IControllerFactory controllerFactory) {
            _controllerFactory = controllerFactory;
        }
 
        protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext) {
            requestContext.HttpContext.SetSessionStateBehavior(GetSessionStateBehavior(requestContext)); 
            return new MvcHandler(requestContext); 
        }
 
        protected virtual SessionStateBehavior GetSessionStateBehavior(RequestContext requestContext) {
            string controllerName = (string)requestContext.RouteData.Values["controller"];
            IControllerFactory controllerFactory = _controllerFactory ?? ControllerBuilder.Current.GetControllerFactory();
            return controllerFactory.GetControllerSessionBehavior(requestContext, controllerName); 
        }
 
        #region IRouteHandler Members 
        IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) {
            return GetHttpHandler(requestContext); 
        }
        #endregion
    }

看以上的粗體代碼,MvcRouteHandler在實現了IRouteHandler的GetHttpHandler,該方法調用了MvcRouteHandler自身定義的GetHttpHandler虛方法,而在這個虛方法里我們看到了一個非常重要而又期待已久的代碼——返回MvcHandler的實例,大概看一下MvcHandler這個類,得到它就是我們所猜想的:繼承於IHttpHandler接口的一個類,並且也繼承了 IHttpAsyncHandler接口,我們先不管MvcHandler內部是如何實現的,但我們前面幾章節的全部分析終於得到了驗證,也就說在這里得到了Mvc的專用處理Handler,然后調用它的BeginProcessRequest方法進入Mvc自身的Pipeline進行處理了。

 

至此,我們終於弄明白了Mvc在整個ASP.NET Runtime是如何接管請求的了,也應該大概清楚整個ASP.NET Runtime的運行機制了,至於MvcHandler的實現方式,我們會在后面的很多章節逐一給大家分析每行代碼,今天我們還有一個小任務,那就是:看完了Mvc的實現機制,我們能否自己來寫一個自定義的HttpHandler通過Route動態注冊進去,來實現我們自己的自定義擴展,我們來嘗試着做一下吧。

第一步:建立HttpHandler類

    public class TomHandler : IHttpHandler
    {
        public TomHandler(RequestContext requestContext)
        {
            // do nothing
        }

        public virtual void ProcessRequest(HttpContext context)
        {
            string url = context.Request.Url.AbsoluteUri;
            context.Response.Write("當前地址為:" + url);
            context.Response.End();
            // 這里我們什么都不做,只輸出URL地址
        }

        public virtual bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }

第二步:建立RouteHandler類

public class TomRouteHandler : IRouteHandler
{
    IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext)
    {
        return new TomHandler(requestContext);
    }
}

在GetHttpHandler實現里,返回我們定義的TomHandler實例。

 

第三步:注冊我們的Route和RouteHandler

protected void Application_Start(object sender, EventArgs e)
{
    Route route = new Route("tom/{other}", new TomRouteHandler());
    RouteTable.Routes.Add(route);
}

 

我們設置成,只要訪問tom文件夾下的任意文件或者子目錄,都提示該URL。下面是我的測試結果:

訪問:Http//localhost/tom/

結果:沒有提示我們預想的結果(原因是不符合我們的規則)

訪問:Http//localhost/tom/123/

結果:輸出正常(說明TomHandler已經接管了該請求)

訪問:Http//localhost/tom/123.aspx?id=123

結果:輸出正常(也說明TomHandler已經接管了該請求)

 

在建立真實的tom文件夾,然后在里面建立一個 index.html文件(內容為123),然后訪問Http//localhost/tom/index.html,規則符合我們的Route定義,但輸出結果卻不是我們預期的結果,而是123,怎么回事?還記得上一章節里談到的RouteCollection的GetRouteData方法么?該方法是先判斷URL對應的文件是否真實存在,如果存在就直接輸出,如果不存在就再來找RouteData的數據,這就解釋了上面的index.html路徑為什么不是我們期望結果的原因了吧?。

 

注:如果你建立一個index.aspx文件,並在index.aspx.cs文件里寫Response.Write代碼輸出123的話,該文件也會按照aspx頁面的正常周期來執行(也就是說輸出123字符串),如果你在<system.web>里的httpHandlers節點用remove命令把*.aspx的匹配設置去掉,那結果就只會輸出index.aspx這個文件里的字符串了(包括里面內嵌的任何C#代碼)。

 

最后總結了,到這里,我們已經知道了MvcHandler是如何接管請求的了,而且自己也做了一個簡單的例子來驗證這套機制。重新回顧一下前面的這么多篇文章,我們應該大概對ASP.NET RunTime, Pipeline以及ASP.NET MVC切入點應該有個整體的了解了。

同步與推薦

本文已同步至目錄索引:MVC之前的那點事兒系列

MVC之前的那點事兒系列文章,包括了原創,翻譯,轉載等各類型的文章,如果對你有用,請推薦支持一把,給大叔寫作的動力。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM