前言
本文假定读者对 HttpModule 、HttpHandler和IIS的处理流程有一定的了解,如果为了解可以参考以下链接。文中大部分代码通过Reflector反编译 System.Web.dll 得到,.net 版本为4.0
IIS 5.0 和 6.0 的 ASP.NET 应用程序生命周期概述
Asp.net MVC 程序虽然开发的模式不同,但是其本质上还是 Asp.net。其利用了HttpModule 和 HttpHandler 做了扩展,可以参考博客园里的大牛——Artech 相关系列文章。
本文主要关注UrlRoutingModule 、MvcRouteHandler 两个类的源代码,进而分析客户的请求是如何到达MvcHandler 的。
Asp.net MVc 程序启动流程 需要关注的行为
- 1、Application启动时先通过RouteTable把URL映射到Handler
- 2、通过UrlRouting Module 这个HttpModule 拦截用户请求。
我们知道,HttpModule 是注册在 Web.config 中的,可是当你打开Asp.net MVc 程序的Web .Config 时 却没有发现该配置节,原因是:"它已经默认的写在全局的中"。应此 你可以在 “$\Windows\Microsoft.NET\Framework\版本号\Config\Web.config“ 中找到 " <add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" />”
UrlRoutingModule 源码
UrlRoutingModule 位于 System.web.dll 文件中,利用Reflector 可以查看到其源码:
UrlHttpModule 实现了 IHttpModule 接口
HTTP Module在应用程序发出请求时被调用的并进行特定的事件处理。 HTTP
Module作为请求管道的一部分调用,它们能够访问请求过程中请求周期中的各种管线事件。
Http
Module必须经过注册才能从请求管道接收通知。 注册 HTTP 模块的最常用方法是在应用程序的 Web.config 文件中进行注册。 在 IIS 7.0
中,统一的请求管道使您还可以通过其他方式注册模块,其中包括通过 IIS 管理器和 Appcmd.exe 命令行工具。
当 ASP.NET
创建表示您的应用程序的 HttpApplication 类的实例时,将创建已注册的任何模块的实例。 在创建模块时,将调用它的 Init
方法,并且模块会自行初始化。在模块的 Init 方法中,可以注册各种应用程序事件的处理程序(如 BeginRequest 或
EndRequest)。
可以看到 UrlHttpModule 在 init 方法中注册了PostResolveRequestCache
事件的处理程序。
关于Asp.net的生命周期事件可以参考:
http://msdn.microsoft.com/zh-cn/library/ms178472
http://msdn.microsoft.com/zh-cn/library/bb470252
http://msdn.microsoft.com/zh-cn/library/ms178473
PostResolveRequestCache
该事件在完成缓存解析并投递时触发。
在UrlRoutingModule中它主要是进行上下文的初始化,同时根据传递过来的路由信息获取指定IHttpHandler
(其实就是我们的MvcHandler类)
最后通过 context.RemapHandler() 代码将HttpHandler 处理程序映射到
管线处理中。
在 PostResolveRequestCache 之前分别触发的事件有:
引发 BeginRequest
事件。
引发 AuthenticateRequest 事件。
引发 PostAuthenticateRequest 事件。
引发
AuthorizeRequest 事件。
引发 PostAuthorizeRequest 事件。
引发 ResolveRequestCache
事件。
http://i.msdn.microsoft.com/dynimg/IC5405.png
核心逻辑代码:
1 //获取路由信息 2 RouteData routeData = this.RouteCollection.GetRouteData(context); 3 IRouteHandler routeHandler = routeData.RouteHandler; 4 //构建请求上下文 5 RequestContext requestContext = new RequestContext(context, routeData); 6 context.Request.RequestContext = requestContext; 7 IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext); 8 //将MvcHandler 实例 映射到管线中(通常我们是利用web.config 进行配置的,但是MvcHandler 没有默认无参构造函数,所以直接通过向其传递一个实例 9 //进行映射) 10 context.RemapHandler(httpHandler);
如何获取到MvcRouteHandler?
MvcRouteHandler 实现了 IRouteHandler接口。 上文的 IRouteHandler
routeHandler=routeData.RouteHandler; 在Asp.net MVc
程序中实际上获取的是MvcRouteHandler实例。
RouteData 类中包含了 IRouteHandler实例的引用,它通过 RouteData
的构造函数:
public RouteData(RouteBase route, IRouteHandler routeHandler);
或者 属性
public IRouteHandler RouteHandler { get; set; }
进行注入。
我们再往回搜索,RouteData实例是通过 RouteCollection.GetRouteData(Context) 方法获取的。查看该方法的主要逻辑实现:
using (this.GetReadLock()) { foreach (RouteBase base2 in this) { RouteData routeData = base2.GetRouteData(httpContext); if (routeData != null) { return routeData; } } }
可以看到通过 RouteBase 类的 GetRouteData(HttpContext)获取了 RouteData实例,并且将第一个部位Null的值返回。 我们需要深入查看RouteBase GetRouteData方法。 RouteBase 是抽象类,其方法是在 Route上具体实现的。(这里又引出一个问题,程序是何时将 Route实例绑定到了 RouteBase上)。
Route类
深入到Route 类中 发现其和 RouteData 一样, IRouteHandler 也是通过 构造参数 或 属性对 IRouteHandler 进行了注入。
public override RouteData GetRouteData(HttpContextBase httpContext) { string virtualPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo; RouteValueDictionary values = this._parsedRoute.Match(virtualPath, this.Defaults); if (values == null) { return null; } RouteData data = new RouteData(this, this.RouteHandler); if (!this.ProcessConstraints(httpContext, values, RouteDirection.IncomingRequest)) { return null; } foreach (KeyValuePair<string, object> pair in values) { data.Values.Add(pair.Key, pair.Value); } if (this.DataTokens != null) { foreach (KeyValuePair<string, object> pair2 in this.DataTokens) { data.DataTokens[pair2.Key] = pair2.Value; } } return data; }
关于Asp.net Mvc 中的 MapRoute() 方法
在Asp.net MVc 程序Global 文件的RegisterRoutes 方法里,RouteCollection 类使用的是MapRoute 方法添加的路由,该方法是一个扩展方法。它位于System.Web.Mvc 的RouteCollectionExtensions类中。
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; }
查看上面的关键行:
Route route = new Route(url, new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(defaults),
Constraints = new
RouteValueDictionary(constraints),
DataTokens = new
RouteValueDictionary()
};
可以清楚看到,当我们在Asp.net MVc 程序里使用MapRoute()添加路由时,会生成一个 Route 类的实例,并且该实例在生成时会被注入 MvcRouteHandler 实例。 最后被添加到 RouteCollection的集合里(Route 和 MvcRouteHandler 都是以多态形式存在于 RouteCollection中的),这样就建立了一个关系映射表,只有请求的上下文通过该上下文的验证,就可以返回对应MvcRouteHandler实例。
- MvcRouteHandler 实现了 IRouteHandler接口
- Route 继承了 RouteBase 抽象类。
- RouteCollection 中维护着一个 RouteBase 集合。
如何根据路由信息获取MvcHandler
在Asp.net Mvc 程序 启动时,会触发 Appliction_start 方法,该方法调用了 RouteTable,生成一个全局RouteCollection(单件模式),并将其作为参数传递到RegisterRoutes 方法中。
registerRoutes 方法里通过MapRoute 向 RouteCollection添加路由。 添加的路由是包含MvcRouteHandler实例的Route。
当用户的请求到达IIS后,由于Asp.net MVC 注册了一个HttpModule(UrlRoutingModule)
,应此会触发对应的管线事件处理方法。
1、根据上下文获取RouteData方法, 其内部
实现是:从RouteCollection中遍历RouteBase(实际为Route)实例,并调用RouteBase的GetRouteData()
方法,如果上下文与路由匹配
该方法就会构造一个 RouteData实例,并将 this.IRouteHandler的实例
注入(在Application映射路由时,每个Route实例都包含一个MvcRouteHandler实例的引用)。
2、从 RouteData中 获取IRouteHandler 实例,即 MvcRouteHandler
3、构建上下文,创建RequestContext 该类仅包含 HttpContextBase 和
RouteData 。调用MvcHandler的构造函数需要传递该参数(可以知道请求的Controller、action名等信息),MvcHandler
实现了 IHttpHandler,注意它与MvcHttpHandler 是不相同的:
MvcHandler . 此处理程序负责启动用于 MVC 应用程序的 ASP.NET 管道。 它从 MVC 控制器工厂接收 Controller 实例;此控制器处理请求的进一步处理。 注意,即使 MvcHandler 实现 IHttpHandler,也不能将其映射为处理程序(例如,.mvc 文件扩展名),因为该类不支持无参数构造函数。 (它唯一的构造函数需要一个 RequestContext 对象。)
因此 在 UrlRoutenModule 中 是 通过 HttpContext.RemapHttp(HttpHandler) 。 直接将 一个实例 映射到处理程序上 。(不需要通过系统对其实例化)。
MvcHttpHandler . 此处理程序用于在不通过路由模块的情况下帮助直接处理程序映射。 如果您希望一个文件的扩展名(如 .mvc)直接映射到一个 MVC 处理程序,这很有用。 在内部,MvcHttpHandler 执行 ASP.NET 路由通常执行的相同任务(通过 MvcRouteHandler 和 MvcHandler)。 但是,它将这些任务作为处理程序而不是模块来执行。 UrlRoutingModule 为所有请求启用时,通常不使用此处理程序。
)
4、调用IRouteHandler的 GetHttpHandler 方法 获取
IHttpHandler实例。(即调用了MvcRouteHandler实例的GetHttpHandler 方法,生成了一个 MvcHandler 实例)
5、向当前上下文注册 IHttpHandler 实例,进入 Controller 处理。
经过以上步骤,我们就大致了解到了Asp.net Mvc程序启动后,用户的请求是如何到达HttpHandler的。
RouteTable 类
这个类很简单只包含一个静态的RouteCollection 属性,是一个单件类。
在Asp.net MVc 中application_start 方法里 调用了RouteTable来获取唯一的RouteCollection实例,
所以在UrlRouteModuel中可以通过RouteTable.Routes获取所配置的路由集合。
RouteCollection类
是一个集合类,内部维护着一个RouteBase 以路由名作为Key的字典集合, 所以我们可以给路由命名。主要的属性和方法有:
RouteData GetRouteData(HttpContextBase httpContext)
该方法遍历集合内部的RouteBase实体,并返回第一个非Null的RouteData ,具体的RouteData实例,是由所遍历的RouteBase 通过调用 RouteBase.GetRouteData(HttpContext)方法获取的 。
返回的RouteData 中包含一个 IRouteHandler 对象,该接口的只有一个方法,GetHttpHandler,用于获取IHttpHandler 对象。
VirtualPathData GetVirtualPath(...)
该方法具有多个重载,当您使用 ASP.NET 路由框架生成 URL 时,GetVirtualPath 方法将返回 VirtualPathData 类的一个实例。 VirtualPathData 类包含与所提供的上下文匹配路由的相关信息。
Route MapPageRoute(...)
该方法用于向路由集合中添加路由,提供此方法是为了方便编码, 它等效于调用 Add 方法。其内部实现的主要代码为:
Route item = new Route(routeUrl, defaults, constraints, dataTokens, new PageRouteHandler(physicalFile, checkPhysicalUrlAccess));
注册使用 PageRouteHandler 类创建的 Route 对象。
在Asp.net Mvc 方法中,并没有利用该方法向集合中添加路由而是通过了了 MapRoute()方法 这是一个扩展方法,定义在了 RouteCollectionExtensions 类中。
static Route MapRoute(this RouteCollection routes, ......)
这是一个扩展方法,并且具有多个重载,目的是方便编码,用于向RouteCollection中 添加路由。其内部主要是 生成了一个 以 MvcRouteHandler 为 路由处理的Route 并将其加入到集合中。
Route route = new Route(url, new MvcRouteHandler()) { Defaults = new RouteValueDictionary(defaults), Constraints = new RouteValueDictionary(constraints), DataTokens = new RouteValueDictionary() };
IRouteHandler 接口
接口的定义很简单,只有一个 GetHttpHandler 方法用于返回 一个 IHttpHandler 对象。在 Route 对象,及 RouteData对象中 都包含该对象的引用。该接口定义了一种协议, 指定了 Route 即路由应该有哪一个处理对象进行处理。 它是 Route 到 Handler的 重要桥梁。
RouteBase 类
这是一个抽象类,RouteBase 类用于定义应用程序中的路由。 在定义路由时,通常使用 Route 类,Route 类是从 RouteBase 类派生的。 但是,如果要提供与 Route 类所提供的功能不同的功能,则可以创建一个从 RouteBase 派生的类,并实现所需的属性和方法。
主要方法有:GetRouteData 在派生类中重写时,会返回有关请求的路由信息。
GetVirtualPath
在派生类中重写时,会检查路由是否与指定值匹配,如果匹配,则生成一个 URL,然后检索有关该路由的信息。
Route 类
可以通过 Route 类指定 ASP.NET 应用程序中路由的处理方式。 可以为要映射的每个 URL 模式创建一个 Route 对象,该类可处理与该模式相对应的请求。 当应用程序收到请求时,ASP.NET 路由会循环访问 Routes 集合中的路由,以查找与该 URL 模式匹配的第一个路由。
可将 Url 属性设置为 URL 模式。 该 URL 模式包含某些分段,这些分段位于 HTTP 请求中应用程序名称之后。 例如,在 URL http://www.contoso.com/products/show/beverages 中,该模式应用于 products/show/beverages。 包含三个分段的模式(如 {controller}/{action}/{id})与 URL http://www.contoso.com/products/show/beverages 匹配。 每个分段由 / 字符分隔。 如果分段位于大括号({ 和 })内,则表明该分段是一个 URL 参数。 ASP.NET 路由会检索请求中的值并将其分配给 URL 参数。 在上面的示例中,URL 参数 action 被赋予值 show。 如果该分段不在大括号内,则该值被视为文本值。将 Defaults 属性设置为 RouteValueDictionary 对象,该对象包含当 URL 中缺少某个参数时所使用的值,或者包含用于设置 URL 中未参数化的其他值的值。 将 Constraints 属性设置为 RouteValueDictionary 对象,该对象包含的值为正则表达式或 IRouteConstraint 对象。 这些值用于确定参数值是否有效。
public override RouteData GetRouteData(HttpContextBase httpContext) { string virtualPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo; RouteValueDictionary values = this._parsedRoute.Match(virtualPath, this.Defaults); if (values == null) { return null; } RouteData data = new RouteData(this, this.RouteHandler); if (!this.ProcessConstraints(httpContext, values, RouteDirection.IncomingRequest)) { return null; } foreach (KeyValuePair<string, object> pair in values) { data.Values.Add(pair.Key, pair.Value); } if (this.DataTokens != null) { foreach (KeyValuePair<string, object> pair2 in this.DataTokens) { data.DataTokens[pair2.Key] = pair2.Value; } } return data; }
该方法重写了父类的方法,返回一个RouteData 作。方法一开始 首先通过 ParsedRoute 类的 Match 方法进行路由匹配,匹配成功后则生成一个RouteData对象实例。
ParsedRoute 类
该类是一个内部类,用于匹配、绑定URL,大家可以参考:
http://www.cnblogs.com/JeffreyZhao/archive/2009/08/24/more-on-ParsedRoute.html
http://www.cnblogs.com/JeffreyZhao/archive/2009/08/19/use-the-internal-feature.html
http://www.cnblogs.com/JeffreyZhao/archive/2009/08/24/domain-parser-based-on-parsedroute.html
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) { BoundUrl url = this._parsedRoute.Bind(requestContext.RouteData.Values, values, this.Defaults, this.Constraints); if (url == null) { return null; } if (!this.ProcessConstraints(requestContext.HttpContext, url.Values, RouteDirection.UrlGeneration)) { return null; } VirtualPathData data = new VirtualPathData(this, url.Url); if (this.DataTokens != null) { foreach (KeyValuePair<string, object> pair in this.DataTokens) { data.DataTokens[pair.Key] = pair.Value; } } return data; }
该方法返回与路由相关联的URL信息。其内部是通过ParsedRoute 类的Bind 把RouteData 绑定到一个BoundUrl 对象中。 如果有约束在则进行验证,最后返回一个 VirtualPathData 对象。
protected virtual bool ProcessConstraint(HttpContextBase httpContext, object constraint, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { object obj2; IRouteConstraint constraint2 = constraint as IRouteConstraint; if (constraint2 != null) { return constraint2.Match(httpContext, this, parameterName, values, routeDirection); } string str = constraint as string; if (str == null) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, SR.GetString("Route_ValidationMustBeStringOrCustomConstraint"), new object[] { parameterName, this.Url })); } values.TryGetValue(parameterName, out obj2); string input = Convert.ToString(obj2, CultureInfo.InvariantCulture); string pattern = "^(" + str + ")$"; return Regex.IsMatch(input, pattern, RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.IgnoreCase); }
这是一个路由约束的处理方法,可以看到如果传递的 object constraint 是一个 IRouteConstraint 类型 则直接调用 其Match 方法。
如果正则表达式字符串,利用正则表达式进行验证,否则抛出异常。
造一个简陋的*
不要再造*是软件设计的一个准则,但是在学习研究时,造一个简单的*能更好的帮我们了解其原理。
创建一个HttpHandler 类,让Asp.net程序通过路由,运行我们指定的HttpHandler,同时在Handler中能够获取路由信息。
1、创建一个类库项目,在项目里创建一个WheelHandler类并实现IHttpHandler接口,这个类只有一个构造函数构造函数需要传递一个 RequestContext
public class WheelHandler : IHttpHandler { public WheelHandler(RequestContext requestContext) { this.RequestContext = requestContext; } #region IHttpHandler Members public bool IsReusable { get { return true; } } public void ProcessRequest(HttpContext context) { context.Response.Write(String.Format("this is a Wheel for {0}Controller and {1}action " , this.RequestContext.RouteData.Values["Controller"] , this.RequestContext.RouteData.Values["Action"])); context.Response.End(); } #endregion public RequestContext RequestContext { get; private set; } }
WheelHandler没有默认的无参构造函数,所以不能直接在Web.config 中注册。ProcessRequest对象很简单,就是输出传入的路由信息。
我们需要定义一个IRouteHandler对象,当路由被捕获时,返回一个WheelHandler,然后将其映射到Http处理中。
2.定义WheelRouteHandler
public class WheelRouteHandler : IRouteHandler { public IHttpHandler GetHttpHandler(RequestContext requestContext) { return new WheelHandler(requestContext); } IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) { return this.GetHttpHandler(requestContext); } }
3、映射路由和WheelRouteHandler。
我们可以自定义一个派生自RouteBase 的类,进行特定的路由处理,但是本例是一个简单的"*",因此继续使用Route类,只需要在生成Route时 向其注入 WheelRouteHandler即可。
因此我们需要修改添加路由的方式,这里模仿MVc 利用扩展方法。当然你也可以不用扩展方法,在添加路由是直接使用Add方法,记得注入WheelRouteHandler便行。
public static Route MapWheelRoute(this RouteCollection routes, string name, string url, object defaults) { return MapWheelRoute(routes, name, url, defaults, null, null); } public static Route MapWheelRoute(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 与 WheelRouteHandler的映射关系 Route route = new Route(url, new WheelRouteHandler()) { 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; }
MapWheelRoute 方法里 调用了Route构造函数,并传入一个WheelRouteHandler对象。
4、创建HttpModule 对象
自定义的HttpModule的责任是,构建上下文,创建HttpHandler对象并将它映射到Http处理程序里去。
public void Init(HttpApplication context) { context.PostResolveRequestCache += new EventHandler(context_PostResolveRequestCache); } void context_PostResolveRequestCache(object sender, EventArgs e) { HttpContextBase context = new HttpContextWrapper(((HttpApplication)sender).Context); this.PostResolveRequestCache(context); } private void PostResolveRequestCache(HttpContextBase context) { RouteData routeData = RouteTable.Routes.GetRouteData(context); if (routeData == null) { throw new InvalidOperationException(); } IRouteHandler routeHandler = routeData.RouteHandler; if (routeHandler == null) { throw new InvalidOperationException(); } RequestContext requestContext = new RequestContext(context, routeData); IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext); if (httpHandler == null) { throw new InvalidOperationException("无法创建对应的HttpHandler对象"); } context.RemapHandler(httpHandler); }
这里我们也是模仿Mvc 捕获的是PostResolveRequestCache事件进行处理,处理时首先将 包装HttpContext对象为 HttpContextBase对象。然后通过GetRouteData 获取RouteData对象,它包含有IRouteHandler对象。获取到IRouteHandler对象后,需要构造 一个RequestContext 对象(该对象很简单就是包含上下文和路由信息)因为 创建WheelHandler 需要该对象。
创建好IHttpHandler对象后,利用RemapHandler方法将其映射为处理程序。
5、注册路由 和Module
新建一个空的Asp.net 项目,移除里面所有的Aspx文件,事实上只要保留Global和 Web.Config文件即可。在Global 里注册路由:
完成最后我们还需要组成自定义的HttpModule
在Web.Config 中添加如下配置:
<system.webServer> <modules runAllManagedModulesForAllRequests="true"> <add name="WheelRouting" type="WheelRouting.WheelRoutingModule,WheelRouting"/> </modules> </system.webServer>
关于 HttpModule的介绍 可以参考:演练:创建和注册自定义 HTTP 模块
下面是运行结果:
Controller默认值为 Home Action 默认值为 index
源码:点击下载