mvc源码分析 路由(Routing)

前言:

mvc在beta版时就开始接触了,通过博客园里很多人的分享很学到很多,在这里非常感谢他们,mvc很灵活扩展点很多。但如果没有深入了解其源码实现过程,只通一些扩展点文章了解如何扩展,会存在盲区,就是不知道为什么可以这样做。想要加深了解,读熟源码是非常重要的,只有通过其源码了解来龙去脉,才能方便的用自己的方式去扩展,以下是我以前的一次读mvc源码过程记录,很乱,这回算是整理并回顾下。

此文适合己了解asp.net mvc基本流程,想加深认识asp.net mvc的同志,是基于mvc 2.0 的,比较早了,但我觉得很多东东在现在还是差不多的,可供学习参考。如果有讲的不对的地欢迎给我指正。

初用mvc的朋友是否对添加路由规则有点迷茫,他具体是怎么来映射到控制器的。又是怎么来生成url的,要怎么合理的添加路由规则。
mvc默认给了我们很多约定,比如:你的视图文件必须放在 Views/xxx/ 文件夹里面,建一个 Area ,默认给建一个 Areas/xxx/..目录。
然后 AreaRegistration.cs 成为该area的路由注册,这里注册路由是怎么被 Global.asax 里的 AreaRegistration.RegisterAllAreas() 执行的。
是否可以把所有area路由集中放到一个地方按你定制的代码注册。是否可以把 Controller 放到别的地方去,是否想把所有 area 里面的 view 拿出来放一个专门存放 view 的项目中去?...此处省略109.5个字。
在框架各种约定下,有时会觉得不爽,不能自己随意组识自己的文件,但如果你细读源码,就发会现,其实所有的一切,都可以自己定制的。


必备 Reflector 等源码查看工具。

Routing - 路由注册(严格说Routing不只属于mvc,但我这里当作是mvc源码一部分来讲了)
mvc所有的请求都是通过路由规则去映射的,所以mvc的头等大事就是路由规则的注册,也是asp.net mvc第一件要做的事,因为规则确定了,才能知道当前请求应该映射到哪个Controller的哪个Action。

规则的注册是在 Global.asax 的 Application_Start 事件里注册,以下是默认的路由注册代码:

View Code

默认在Application_Start里调用了两个方法。AreaRegistration.RegisterAllAreas() 和 RegisterRoutes(RouteTable.Routes)
前者是注册所有的Area路由规则,area规则通常在添加area后自动产生,如Areas/xxx/xxxAreaRegistration.cs,后者是调用了当前的静态方法,注册了当前非area的路由,可以通过源码看下RegisterRoutes(RouteTable.Routes),了解mvc路由的注册。
这个方法传入了 RouteTable.Routes 这个属性,我们可以通过 Reflector 去查看 RouteTable 类。在 System.Web.Routing.dll 里面, 

View Code

静态构造及Routes属性。

复制代码
static RouteTable()
{
    _instance = new RouteCollection();
}
public static RouteCollection Routes
{
    get
    {
        return _instance;
    }
}
复制代码

通过上面的源码可以看到, RouteTable ,只有一个职责,就是构建一个单例静态 RouteCollection 类,这个类是用来保存路由规则(Route)的集合。RouteTable.Routes 属性就是指向这个静态 RouteCollection 实例,上面就是传入了这个实例,并调用其MapRoute方法添加路由规则。

View Code

我们再查看 RouteCollection.MapRoute(... 添加规则。当看到这个类的源码时,并没有发现有 MapRoute 这些注册方法。
通过vs里跟综可以发现他是在 System.Web.Mvc.RouteCollectionExtensions 这个类的扩展方法里的。可能是 Routing 现在作为一个单独的组件,考滤到可以在不同的需求里扩展不同的注册规则方式。

View Code

上面可以看到有IgnoreRoute 方法,默认的第一条路由就是通过这个方法注册,如: routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 这个。
这也是一个注册路由方法,这样的规则是用来干嘛?这里先不作分析,等下几篇会详细再讲。可以看到上面己经有MapRoute
这个注册方法了,都是一些重载,我们点开最后一个看具体实现。

View Code

上面的代创建了一个 Route ,Route就是一条具体的路由,下次再单独详细Route分析这个类,因为Route是路由映射中一个很重要的成员。这里我们只要知道Route就是具体的一条路由规则,通过包装传入的参数后被添加到了RouteTable.Routes(全局路由集合),这是在整个网站的生命周期里一直随时可以访问路由集合。为后面的请求或是生成url起着重要的作用。这些后续会去分析。

我们可以在Application_BeginRequest打印所有己注册的规则url,同时以后我们可以这样去测试url的有效性及匹配情况。这些后续会提到。

复制代码
        protected void Application_BeginRequest()
        {
            RouteCollection routes = RouteTable.Routes;
            foreach (Route route in routes)
            {
                Response.Write(route.Url + "<br />");
            }
        }
复制代码

上面就是默认非area的路由注册,我们再看下area的注册又是如何进行的。

自从asp.net mvc2.0 就引入了area,可以让我们把不同的模块分格开来,我们默认添加一个 area,自动在 Areas 文件下创建。然后又为每个area添加了一个注册规则的文件,如:Areas/Admin/AdminAreaRegistration.cs

那么这里面的规则又是如何被注册到的呢,这里的又和前面的有什么区别,可以把area的路由注册放到 Application_Start 里去注册吗?

我们在Application_Start里看到先是调用了AreaRegistration.RegisterAllAreas(),这个就是先注册所有area的路由。我们去看下这个方法。

AreaRegistration 这个类是在 System.Web.Mvc.dll ,你可以通过下载mvc的源码查看,我这里是用 Reflector 查看,这个类被定义为abstract。实际我们创建的Areas里的xxxAreaRegistration.cs,就是继承这个类,并实现了RegisterArea(AreaRegistrationContext context)  , AreaName { get; }

 AreaRegistration 类

View Code

首先看下他的静态方法先后调用 

View Code

可以看到最终是是通过TypeCacheUtil.GetFilteredTypesFromAssemblies过滤找所有MVC-AreaRegistrationTypeCache.xml这样一个常量标识的类,而上面的AreaRegistration 里恰好有这个值为 MVC-AreaRegistrationTypeCache.xml 常量,可能是做为识别标识,这部分比较复杂,而且也没太大必要去细做研究。我们只要大概猜到,这里就是取所有继承了AreaRegistration的类(就是我们创建area后自带的注册路由的类)。并创建实例,然后调用 CreateContextAndRegister(routes, state); routes是前讲到的全局路由集合,state,这里上面是传入了null。我们再去看下AreaRegistration的这个CreateContextAndRegister方法。

View Code

首先是创建了一个 AreaRegistrationContext,这个类我们可以理解为一个打包。就是把 AreaName Routes state这些东东打包,以下构造方法。

View Code

然后如果非null就把空间命名也加入到AreaRegistrationContext的Namespaces属性集合中。
最后把包装好的数据传入调用了 this.RegisterArea(context); 这就是我们 Areas/xxx/xxxAreaRegistration.cs 里实现了AreaRegistration的方法,完成了当前area的路由规则注册。

View Code

再看一下这里的RegisterArea里的注册是通过 AreaRegistrationContext 类的 MapRoute 方法了,而不是上篇非area注时册的RouteCollection扩展MapRoute方法进行注册了,我们去看下 AreaRegistrationContext 这个类的MapRoute方法是怎样的。

View Code

看标注的那行。this.Routes ,就是前打包 AreaRegistrationContext 的值,实际还是 RouteTable.Routes,也就是最后还是和非area一样。只是后面多了些操作。就是往 route.DataTokens 属性里,加了两项值。这也是与非area规注注册的一点区别。

route.DataTokens["area"] = this.AreaName;
bool flag = (namespaces == null) || (namespaces.Length == 0);
route.DataTokens["UseNamespaceFallback"] = flag;

这里我们基本可以确定,只要 Route 的 DataTokens 属性里,有 area 这项值,就是一个area规则。
UseNamespaceFallback 这项值我以前的记录里没有,我记得以前整个mvc流程完了,好像也没太看到这个属性用在哪,这里先不管,不加也一样。分析到再说吧。我们现在可以试一下,把area 里的 AdminAreaRegistration.cs 删了。把area的路由规则放在Application_Start()里去注册,如下:

View Code

然后访问 /admin/home 我们可以发现可以正常请求area的。
这样如果喜欢把规则放到一起注册的朋友可以这样了,不必再在每个 xxxAreaRegistration.cs 文件里注册了。
或者用你自己的方式,反正简单的说,注册路由规则,就是往 RouteCollection 集合里添加 Route,而area的注册,就是 Route的DataTokens["area"] = "Admin"; 指定其area名称。而当 DataTokens 含有 area 值,mvc后面生成url和指定view都会用到这个值,后续我们会看到。

结尾语:
本文会跟着mvc的整个执行流程依次把多数源码分析下去。其中结合了自己的一些见解,如果有什么不正确的地方还请指出。
转载请注明出处,谢谢。

这节主要分析一个具体的请求是如何通过我们注册的路由映射到具体的操作上。这节的知识需要了解asp.net的生命周期,如果您对这个还不了解,可以去看下asp.net 生命周期这篇文章,作为asp.net开发人员,这个必须要了解。

了解了asp.net生命周期我们知道asp.net在请求过程中会激发HttpApplication的的一系列事件,而我们可以通过实现IHttpModule去订阅这些事件并做些事,甚至是指定此次请求的HttpHandler

复制代码
<httpModules>
    ...
    <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
</httpModules>
复制代码

Mvc项目配置文件httpModules节点里加入了Routing的UrlRoutingModle,也是做为一个mvc请求的入口点,去看下具体的实现。

View Code

系统会先调用所有实现了IHttpModule 的 Init 方法来注册生命周期事件的订阅。下面是UrlRoutingModule的细节。

复制代码
void IHttpModule.Init(HttpApplication application)
{
    this.Init(application);
}
protected virtual void Init(HttpApplication application)
{
    application.PostResolveRequestCache += new EventHandler(this.OnApplicationPostResolveRequestCache);
    application.PostMapRequestHandler += new EventHandler(this.OnApplicationPostMapRequestHandler);
}
复制代码

UrlRoutingModule订阅了两个事件,不太熟悉的朋友可以去搜下asp.net生命周期,有很多这样的文章里,说明了所有事件的执行顺序及作用。我们打开会先执行到的事件方法。

View Code

可以看到调用了我上一篇提到的 RouteCollection类的GetRouteData(context) 方法返回一个RouteData,而这里的RouteCollection实际就是上篇讲到的RouteTable.Routes

可以点开看下 this.RouteCollection

复制代码
public RouteCollection RouteCollection
{
    get
    {
        if (this._routeCollection == null)
        {
            this._routeCollection = RouteTable.Routes;
        }
        return this._routeCollection;
    }
    set
    {
        this._routeCollection = value;
    }
}
复制代码

到这里,暂时先不往下分析了,我们有必要先去了解 Routing 组件的几个主要成员。这样才能更好的了解asp.net mvc整个执行过程。

RouteTalbe
RouteCollection
RouteBase
Route
RouteData
RouteValueDictionary

RouteTalbe 职责比较简单,在上篇注册中我们分析的比较清楚了。这里就不再复述。不清楚的,可去看下。

先来回顾一下上篇分析到的注册路由的情况。

View Code

在RouteCollection扩展方法MapRoute中,首先创建了一个 Route,并传入了规则url 和 new MvcRouteHandler()
然后为Route属性赋值了MapRoute中传来的参数,这里几个参数是被转化成RouteValueDictionary

我们简单说下RouteValueDictionary,它实际是对 Dictionary<string, object> 进行了包装。他能把object参数解析为key,value形式,并且设置为不区分大小写。下面是上面构造RouteValueDictionary用到的构造方法。

View Code

如可以把 new { controller = "Main", action = "Index", id = UrlParameter.Optional } 这样的参数转化为 key,value,这个类不去做过多的分析了。

接着再看上面的添加路由
Route item = route2; 这一句我没看明白这样写的好处,可能是个人喜好吧。
然后下面接着判断有没有传入 namespaces 参数,就是路由规则限定controller的空间命名(new string[] { "MVCTest.Controllers" })
有果有就存入Route的DataTokens里,我们记得上一篇讲注册area规则,也是存在这个属性里面存入area名称:route.DataTokens["area"] = "Admin";
最后routes.Add(name, item); 把创建好的路由Route加入了RouteCollection集合,也就是RouteTable.Routes那个静态单例的全局路由集合。

打开RouteCollection的Add方法如下

复制代码
public void Add(string name, RouteBase item)
{
    if (item == null)
    {
        throw new ArgumentNullException("item");
    }
    if (!string.IsNullOrEmpty(name) && this._namedMap.ContainsKey(name))
    {
        throw new ArgumentException(string.Format(CultureInfo.CurrentUICulture, RoutingResources.RouteCollection_DuplicateName, new object[] { name }), "name");
    }
    base.Add(item);
    if (!string.IsNullOrEmpty(name))
    {
        this._namedMap[name] = item;
    }
}
复制代码

里面做了重复路由名称验证。同时发现这里实际的路由参数签名是 RouteBase,RouteBase是路由规则的基类,这个类被定义为 abstract,而且从名字上看也能知道,这个类主要供别的类实现,里面定义了 GetRouteData,GetVirtualPath两个抽象方法。这个抽象类本身自己没有任何功能实现,从这点看,其实这个类应该定义为接口 IRoute 更合理。


通过路由规则基类的两个抽象方法,可以看出,路由规则主要用来做两件事:

一、解析请求url,提取数据。如:/home/index 得到:controller/home,action/index (当然首先要看是否匹配本规则,这个下面再细讲)
提取到的数据会包装成 RouteData ,上面的RouteData routeData = this.RouteCollection.GetRouteData(context); 就是得到提取到的数据。这些下面稍后再细讲。

二、生成url,如我们在页面中 url.Action("Edit", new { id = 5}) //这个我们以后再说

我们先来看下RouteBase

View Code

Route是MS帮我们默认实现了RouteBase的类。我们自己也可以实现 RouteBase,然后传给RouteCollection进行路由注册,以后再单独讲如何自己实现,和自己实现的一些利弊。

View Code

简单了解一下Route几个属性。


Constraints,保存规则约束,如: new { id = @"^\d{1,2}$" } //这里都用RouteValueDictionary转化了为 key/value 形式。


DataTokens,附加参数,上一篇说到,注册 area 路由就是为这个属性添加area项。还有刚才前面提到的指定controller的空间命名也是放在这里面。item.DataTokens["Namespaces"] = namespaces;


Defaults,这个保存的规则默认值。new { action = "index" }


RouteHandler 上面创建Route己经看到了,是 new MvcRouteHandler(), MvcRouteHandler 是实现了 IRouteHandler。具体作用我们以后会讲到。


Url 规则url

-------------------

Route构造方法没什么说的了,都是赋值。

主要要看Route是如何实现了 RouteBase 的两个抽象方法,来完成路由规则的两项任务的。(解析请求url提取数据 和 生成url)
由于篇幅太长,这个我想放到下篇再细看,我们回到本篇的篇首的

RouteData routeData = this.RouteCollection.GetRouteData(context);


UrlRoutingModule 是调用了 RouteCollection 的 GetRouteData,而不是Route中的GetRouteData。这是因为随意一个请求不可能直接认定它是匹配哪一条路由规则。看下 RouteCollection 的 GetRouteData 方法

View Code

上面的代码可以看到,最后是通过 RouteBase 循环了自己所有的路由规则,分别调用我们所有注册在RouteTable.Routes(RouteCollection)的Route的GetRouteData方法,如果为null就进行下一条路由规则,循环匹配路由规则,直到找到一条符合当前请求的路由为止。

从这里我们也可以看到,路由是有优先级的,当一条请求url同时符合两条路由规则时,当找到第一条符合规则的路由时就不往下找了。所以我们在注册路由时一定要考滤到优先级的情况。

我们再看下上面循环用到的 this.GetReadLock() //是一个读写锁,这里不做去分析了。有兴趣的可以详细看下。

我们还注意到上面有这么一段: 

复制代码
if (!this.RouteExistingFiles)
    {
        string appRelativeCurrentExecutionFilePath = httpContext.Request.AppRelativeCurrentExecutionFilePath;
        if (((appRelativeCurrentExecutionFilePath != "~/") && (this._vpp != null)) && (this._vpp.FileExists(appRelativeCurrentExecutionFilePath) || this._vpp.DirectoryExists(appRelativeCurrentExecutionFilePath)))
        {
            return null;
        }
    }
复制代码

RouteCollection 有这么一个属性RouteExistingFiles.当为false时,就检测请求的路径地址是否己经存在文件或目录,如果存在,则直接不走路由了,直接返回null,默认就是false,我们可以实验一下。当然这里是忽略了根目录的,不然默认我们 http://www.xxx.com/ 也不能访问了。

复制代码
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
                new string[] { "MVCTest.Controllers" }
            );
        }
复制代码

这里是默认的路由注册,按理说我们访问 home 时,会去到 home controller 的 index,但是我们在在项目里加一个 home 目录,如下图。

我们再访问:http://localhost:2144/home/ 我们发现,无法找到该资源,也就是检测到home这个目录存在时,就不走路由了。

接下来,我们去把 RouteCollection 的 RouteExistingFiles 设为 true,如下:

复制代码
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
                new string[] { "MVCTest.Controllers" }
            );

            routes.RouteExistingFiles = true;
        }
复制代码

我们再访问:http://localhost:2144/home/ ,发现可以访问的了,走路由了。

这篇就讲到这吧,主要是介绍了Routing各成员的关系,另外 Route 具体实现了 RouteBase 的 GetRouteData,和GetVirtualPath,还有 RouteData 类 的详细分析,以及UrlRoutingModule 里得到了 RouteData
RouteData routeData = this.RouteCollection.GetRouteData(context);
接下来流程下次再分析。

最后我画了一个简易图来总结我理解的Routing各成员的关系。也是应前一篇文章一个朋友的要求,加点图,画的很不好,见凉,此处也再申明一下,Routing是一个单独的组件,但我这里是把它当做mvc源码的一部分来了解了。

结尾语:
本文会跟着mvc的整个执行流程依次把多数源码分析下去。其中结合了自己的一些见解,如果有什么不正确的地方还请指出。
转载请注明出处,谢谢。

原文地址:https://www.cnblogs.com/Leo_wl/p/2665359.html