ASP.NET MVC是如何运行的
ASP.NET MVC由于采用了管道式设计,所以具有很好的扩展性,整个ASP.NET MVC应用框架就是通过扩展ASP.NET实现的。
ASP.NET MVC的扩展点主要体现在HttpModule
和HttpHandler
这两个核心组件之上,整个ASP.NET MVC框架就是通过自定义的HttpModule
和HttpHandler
建立起来的。
ASP.NET MVC从接收请求到响应回复的完整流程
ASP.NET MVC 利用路由系统对URL进行解析进而得到目标Controller和Action的名称,以及其他相应的路由数据。并根据Controller的名称解析出目标Controller的真正类型,并将其激活,默认情况下根据类型以反射的机制创建Controller对象。
接下来,ASP.NET MVC利用Action名称解析出定义在目标Controller类型中对应的方法,然后执行激活Controller对象的这个方法。Action方法可以在执行过程中直接对当前请求予以响应,也可以返回一个ActionResult对象来响应请求。对于后者,ASP.NET NVC在完成目标Action方法执行之后,会执行返回的ActionResult对象来对当前请求作最终的响应。
Global.asax
Global.asax全局应用程序类又称为ASP.NET应用程序文件,是一个可选文件,该文件包含响应ASP.NET或HTTP模块所引发的应用程序级别和会话级别事件的代码。
Global.asax文件驻留在ASP.NET应用程序的根目录中,运行时,分析Global.asax并将其编译到一个动态生成的.NET Framework类,该类是从HttpApplication基类派生的。配置ASP.NETE以便自动拒绝对Global.asax文件的任何直接的URL请求,外部用户不能下载或查看其中的代码。
Global.asax文件是可选的,只在希望处理应用程序事件或会话事件时,才创建它。
当IIS接收到一个访问ASP.NET应用程序的请求时,IIS会将请求映射给 aspnet_isapi.dll
,当 aspnet_isapi.dll
接到这个请求后,会新建一个 aspnet_wp.exe
进程(Windows Server 2003下是w3wp.exe
进程),此进程会将请求传递给一个被指定的 AppDomain 。
当这个AppDomain 被创建时,会去加载一些配置文件中的信息,加载的顺序是从 machine.config
文件到 Web.config
。当配置信息加载完毕后,AppDomain 会去获得 HttpApplication
的实例,此时 Global 类就会被编译加载了。接下来 AppDomain 会做一些相关的处理创建Page类的实例,最后页面呈现到客户端浏览器上。
需要注意的是,当配置文件被加载时,并不表示 AppDomain 会加载配置文件中所有的信息,仅仅是加载一些必要的信息。而有些配置信息,只有在需要时才会被 AppDomain 加载。例如Web.config
文件中配置的HttpModule
,当某个HttpModule
被访问时,AppDomain才会去加载并处理这些信息。所以说Web.config
文件和Global.asax
没有先后执行的顺序,只是视具体配置什么时候被加载和处理而定。
/*功能:所有应用、应用状态、程序是否被访问、用户退出...*/
public class Global : System.Web.HttpApplication
{
/*在每个HttpApplication实例初始化时执行*/
protected void Application_Init()
{
}
/*在每个HttpApplication实例被销毁前执行*/
protected void Application_Disposed()
{
}
/**
* 程序初始化时执行
* IIS请求一开始就执行此方法,相当于Main函数。
* 在Web应用程序的生命周期里仅执行一次,存放公用信息如HttpApplicationState。
*/
protected void Application_Start(object sender, EventArgs e)
{
}
/**
* 会话开始执行
* 此处的Session是服务端为每个浏览器保存数据开辟的临时存储空间
* 当浏览器关闭或切换用户时重新开启内存空间用来保存会话
* 每个浏览器访问页面均存在一个Session
* 一个浏览器的一个用户公用一个Session
*/
protected void Session_Start(object sender, EventArgs e)
{
}
/**
* 会话结束或过期时执行
* 断开会话,超时后会被调用
*/
protected void Session_End(object sender, EventArgs e)
{
}
/**
* BeginRequest是在收到Request时第一个触发的事件,此方法第一个执行。
* 每个应用均会触发它
* 如需查看当前请求的URL,可使用 HttpContext.Current.Request.URL
*/
protected void Application_BeginRequest(object sender, EventArgs e)
{
}
/*当安全模块已经建立了当前用户的标识后执行*/
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
}
/*当安全模块已经验证了当前用户的授权时执行*/
protected void Application_AuthorizeRequest()
{
}
/**
* 当ASP.NET完成授权事件使缓存模块从缓存中为请求提供服务时发生
* 从而跳过处理程序或WebService的执行,可改善网站的性能。
* 此事件可用来判断正文是不是从Cache中获得的
*/
protected void Application_ResolveRequestCache()
{
}
/*当ASP.NET获取当前请求所关联的当前状态时执行,如Session*/
protected void Application_AcquireRequestState()
{
}
/*在ASP.NET执行完所有请求处理程序后执行,ReleaseRequestState事件将使当前状态数据被保存。*/
protected void Application_ReleaseRequestState()
{
}
/*当ASP.NET即将把请求发送到处理程序对象或WebService之前执行,此时Session就可以使用了。*/
protected void Application_PreRequestHandleExecute()
{
}
/*当处理程序对象工作完成后执行*/
protected void Application_PostRequestHandlerExecute()
{
}
/*在ASP.NET执行完处理程序后为了后续请求而更新响应缓存时执行*/
protected void Application_UpdateRequestCache()
{
}
/**所有没有处理的错误都会导致此方法的执行
* 异常处理模块,调用后可爆出异常信息。
*/
protected void Application_Error(object sender, EventArgs e)
{
}
/**
* 应用程序结束时,在最后一个HttpApplication销毁后执行。
* 对应Appilcation_Start,在整个生命周期中仅执行一次。
* 程序被关闭时执行一次,IIS被关闭才执行。
*/
protected void Application_End(object sender, EventArgs e)
{
}
/*EndRequest是在响应Request时最后一个触发的事件,此方法自然是最后一个执行的。*/
protected void Application_EndRequest()
{
}
/*向客户端发送HTTP标头前执行*/
protected void Application_PreSendRequestHeaders()
{
}
/*向客户端发送HTTP正文前执行*/
protected void Application_PreSendRequestContent()
{
}
}
-
Application_Init
和Application_Start
事件在应用程序第一次启动时被触发一次。 -
Application_Disposed
和Application_End
事件在应用程序终止时被触发一次。 -
Session_Start
和Session_End
基于会话的事件只有在用户进入和离开站点时被使用。
Request请求相应的事件执行顺序如下
- BeginRequest
- AuthenticateRequest
- AuthorizeRequest
- ResolveRequestCache
- AcquireRequestState
- PreRequestHandlerExecute
- PostRequestHandlerExecute
- ReleaseRequestState
- UpdateRequestState
- EndRequest
需要注意的是只有被设置为“应用程序”的虚拟目录,而非普通的虚拟目录所属的Global.asax
才有效。IIS服务只是Windows的一种服务,当IIS服务开启后第一次访问网站或系统资源紧张时,应用程序域回收或自动重启时,Global.asax
才会执行。只是重启IIS是无法激活Global.asax
执行的。
路由匹配
由于UrlRoutingModule
这个HttpModule
被注册到Web应用中,所以对于每个抵达的请求来说,当代表当前引用的HttpApplication
对象的PostResolveRequestCache
事件被触发的时候,UrlRoutingModule
会利用RouteTable
表示的路由表(实际上RouteTable
的静态属性Routes
返回的RouteDictionary
对象代表这个路由表)针对当前请求实施路由解析。
具体来说,UrlRoutingModule
会调用代表路由表的RouteDictionary
对象的GetRouteData()
方法。如果定义在某个Route
对象上的路由规则与当前请求相匹配,那么该方法执行结束之后会返回一个RouteData
对象,包括目标Controller
和Action
名称的路由变量被包含在这个RouteData
对象之中。
接下来UrlRoutingModule
通过RouteData
对象的RouteHandler
属性得到匹配Route
对象采用的RouteHandler
对象,在默认情况下这是一个MvcRouteHandler
对象。UrlRoutingModule
随后会调用这个MvcRouteHandler
对象的GetHttpHandler()
方法得到一个HttpHandler
对象。UrlRoutingModule
随之调用当前GetHttpHandler()
具体返回的一个MvcHandler
对象。UrlRoutingModule
随之调用当前HTTP上下文的MapHttpHandler()
对得到的HttpHandler
对象实施映射,那么此HttpHandler
将最终接管当前请求的处理。
Controller激活
对于MvcHandler
来说或,但它被用来处理当前请求的时候,会利用RouteData
对象得到目标的名称,并借助于注册的ControllerFactory
激活对应的Controller
对象。目标Controller
被激活之后,它的Execute()
被MvcHandler
调用。
如果被激活的Controller
对象的类型是ControllerBase
的子类,但它的Execute()
被执行的时候,它会调用ActionInvoker
对象的InvokeAction()
方法来执行目标Action()
并对当前请求予以响应。
Action执行
默认采用的ActionInvoker
是一个ControllerActionInvoker
对象,但它的InvokerAction()
方法被执行的时候,它会利用注册的ModelBinder
采用Model
绑定的方式生成目标Action()
方法的参数列表,并利用ActionExecutor
对象以“表达式树”的方式执行目标Action()
方法。
目标Action()
方法执行之后总是会返回一个ActionResult
,对于返回类型不是ActionResult
的Action()
方法来说,ASP.NET MVC总是会将执行的结果转换成一个ActionResult
对象。ControllerActionInvoker
会通过执行此ActionResult
对象来对请求做最终的响应。
路由匹配
对于ASP.NET MVC应用来说,针对HTTP请求的实现在目标Controller类型的某个Action方法中,每个HTTP请求不再像ASP.NET WebForms应用一样是针对一个物理文件,而是针对某个Controller的某个Action方法。
目标Controlller和Action的名称由HTTP请求的URL来决定,但ASP.NET MVC接收到抵达的请求后,其首要任务是通过当前HTTP请求的解析得到目标Controller和Action的名称,这个过程是通过ASP.NET MVC的路由系统来实现的。
RouteDictionary
RouteDictionary表示一个具名的Route对象的列表,继承自泛型的字典类型Dictionary<string, RouteBase>,其中Key表示Route对象的注册名称。在GetRouteData方法中,遍历集合找到指定的HttpContextBase对象匹配的Route对象,并得到对应的RouteData。
using System.Collections.Generic;
using System.Web;
namespace MiniMVC.Framework
{
public class RouteDictionary : Dictionary<string, RouteBase>
{
public RouteData GetRouteData(HttpContextBase httpContext)
{
foreach(var route in this.Values)
{
RouteData routeData = route.GetRouteData(httpContext);
if(routeData != null)
{
return routeData;
}
}
return null;
}
}
}
RouteTable
ASP.NET定义了一个全局的路由表,路由表中的每个Route对象包含了一个路由模板。目标Controller和Action的名称可通过路由变量以占位符的形式定义在路由模板中,也可以作为路由对象的默认值。
namespace MiniMVC.Framework
{
// 全局路由表
public class RouteTable
{
//路由模板,目标Controller和Action的名称通过变量以占位符的形式定义在路由模板中
public static RouteDictionary Routes { get; private set; }
static RouteTable()
{
Routes = new RouteDictionary();
}
}
}
一个Web应用可采用多种不同的URL模式,所以需要注册多个继承自RouteBase的Route对象,多个Route对象组成了一个路由表。路由表通过类型RouteTable表示,RouteTable仅仅具有一个类型为RouteDictionary的Routes属性表示针对整个Web引用的全局路由表。
RequestContext
RequestContext表示当前HTTP请求的上下文,其核心就是对当前HttpContext和RouteData的封装。
using System.Web;
namespace MiniMVC.Framework
{
public class RequestContext
{
public virtual HttpContextBase HttpContext { get; set; }
public virtual RouteData RouteData { get; set; }
}
}
IRouteHandler
ASP.NET MVC本质上是由两个自定义的ASP.NET组建来实现的,一个是自定义的HttpModule,另一个是自定义的HttpHandler。HttpHandler从RouteData对象的RouteHandler属性获得。RouteData的RouteHandler属性类型为IRouteHandler接口。IRouteHandler接口具有唯一的GetHttpHandler方法返回真正用于处理HTTP请求的HttpHandler对象。
using System.Web;
using System.Web.Routing;
namespace MiniMVC.Framework
{
public interface IRouterHandler
{
IHttpHandler GetHttpHandler(RequestContext requestContext);
}
}
IRouteHandler接口的GetHttpHandler方法具有一个类型为RequestContext的参数。顾名思义,RequestContext表示当前HTTP请求的上下文,其核心就是对当前HttpContext和RouteData的封装。
RouteBase
承载路由变量的RouteData对象由路由表中与当前请求相匹配的Route对象生成,可通过RouteData的Route属性获得这个Route对象,该属性的类型为RouteBase。
RouteBase是一个抽象类,仅仅包含一个返回类型为RouteData的GetRouteData方法。RouteBase的GetRouteData方法具有一个类型为HttpContextBase的参数,代表针对当前接收请求的HTTP上下文。
GetRouteData方法被执行的时候,会判断自身定义的路由规则是否与当前请求相匹配,并在成功匹配的情况下实施路由解析,并将路由变量封装成RouteData对象返回。如果路由规则与当前请求不匹配,则该方法直接返回Null。
using System.Web;
using System.Web.Routing;
namespace MiniMVC.Framework
{
/*
* 承载路由变量的RouteData对象
* 由路由表中与当前请求相匹配的Route对象生成
* 可通过RouteData的Route属性获得这个Route对象
* Route属性的类型为RouteBase
*/
public abstract class RouteBase
{
// HttpContextBase代表针对当前接收请求的HTTP上下文
// 该方法被执行时会判断自身定义的路由规则是否与当前请求相匹配,并在匹配成功的情况下实施路由解析,将得到的路由变量封装成RouteData对象返回。
public abstract RouteData GetRouteData(HttpContextBase httpContext);
}
}
RouteData
对于每个抵达的HTTP请求,路由系统会遍历路由表并找到一个具有与当前URL模式相匹配的Route对象,然后利用它解析出以Controller和Action名称为核心的路由数据。
RouteData 定义了两个字典类型的属性Values和DataTokens,他们代表具有不同来源的路由变量,前者是由对请求URL实施路由解析获得。表示Controller和Action名称的属性直接从Values属性表示的字典中提取,对应的Key分别为“controller”和“action”。
using System.Collections.Generic;
using System.Web.Routing;
namespace MiniMVC.Framework
{
public class RouteData
{
// 代表具有不同来源的路由变量:由对请求URL实施路由解析获得,表示Controller和Action名称的属性ControllerName和ActionName
// 直接从Values属性表示的字典中提取,对应的Key分别为controller和action
public IDictionary<string, object> Values { get; private set; }
// 代表具有不同来源的路由变量 :从RouteData对象的RouteHandler属性获得,RouteData的RouteHandler属性类型为IRouteHandler接口
public IDictionary<string, object> DataTokens { get; private set; }
public IRouteHandler RouteHandler { get; set; }
public RouteBase Route { get; set; }
public RouteData()
{
this.Values = new Dictionary<string, object>();
this.DataTokens = new Dictionary<string, object>();
this.DataTokens.Add("namespaces", new List<string>());
}
public string ControllerName
{
get
{
object controllerName = string.Empty;
this.Values.TryGetValue("controller", out controllerName);
return controllerName.ToString();
}
}
public string ActionName
{
get
{
object actionName = string.Empty;
this.Values.TryGetValue("action", out actionName);
return actionName.ToString();
}
}
}
}