MVC系列——MVC原始碼學習:打造自己的MVC框架(二:附原始碼)
前言:上篇介紹了下 MVC5 的核心原理,整篇文章比較偏理論,所以相對比較枯燥。今天就來根據上篇的理論一步一步進行實踐,通過自己寫的一個簡易MVC框架逐步理解,相信通過這一篇的實踐,你會對MVC有一個更加清晰的認識。
MVC原始碼學習系列文章目錄:
這篇博主打算從零開始一步一步來加上MVC裡面用到的一些技術,整篇通過三個版本,逐步完善。
一、版本一:搭建環境,實現MVC請求
通過上篇的介紹,我們知道,MVC裡面兩個最核心的部件:MvcHandler和UrlRoutingModule。現在我們就來一步一步實現它們。為了更加真實,我們完全從零開始。
1、新建一個類庫專案,我們暫且命名為Swift.MVC.
2、新建MvcHandler和UrlRoutingModule
我們新建兩個檔案,然後實現IHttpHandler和IHttpModule。我們知道這兩個介面都在System.Web裡面,首先我們在類庫專案裡面引用Syste.Web這個dll,然後來看具體的程式碼。
MvcHandler.cs程式碼:
namespace Swift.MVC { public class MvcHandler : IHttpHandler { public bool IsReusable { get { returnfalse; } } public void ProcessRequest(HttpContext context) { context.Response.Write("當前頁面地址:" + context.Request.Url.AbsoluteUri + " "); context.Response.Write("Hello MVC"); } } }
UrlRoutingModule.cs程式碼:
namespace Swift.MVC {public class UrlRoutingModule : IHttpModule { public void Dispose() { //throw new NotImplementedException(); } public void Init(HttpApplication app) { app.PostResolveRequestCache += app_PostResolveRequestCache; } void app_PostResolveRequestCache(object sender, EventArgs e) { var app = (HttpApplication)sender; app.Context.RemapHandler(new MvcHandler()); } } }
如果你看過博主的上篇,這個應該很好理解。UrlRoutingModule註冊PostResolveRequestCache事件,通過這個事件攔截當前的請求,攔截到請求之後,再交由MvcHandler去處理當前的http請求。整個過程就是這麼簡單,我們最最基礎的“框架”就搭好了。
3、新建一個空的Web專案測試Swift.MVC
第一步,新建一個空的Web專案,新增對Swift.MVC的引用,或者直接將Swift.MVC.dll拷貝到web專案的bin目錄下面,兩種方式都行,這裡為了方便測試,我們直接新增解決方案中的專案引用。
第二步,配置Web專案的web.config檔案。上篇我們就介紹過,HttpHandler和HttpModule的實現類要生效,就必須要在Web.config裡面註冊。註冊之後整個Web.config的內容如下:
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.web> <compilation debug="true" targetFramework="4.5" /> <httpRuntime targetFramework="4.5" /> </system.web> <system.webServer> <handlers> <add name="swifthandler" verb="*" path="*" type="Swift.MVC.MvcHandler, Swift.MVC" preCondition="integratedMode" /> </handlers> <modules> <add name="swiftmodule" type="Swift.MVC.UrlRoutingModule, Swift.MVC" preCondition="integratedMode" /> </modules> </system.webServer> </configuration>
得到結果
這裡博主想要說明兩點:
- 如果你除錯程式你會發現,app_PostResolveRequestCache()和ProcessRequest()都進了兩遍,這是因為在web.config裡面配置的是所有的請求都會被攔截,新增監視發現原來當我們訪問http://localhost:16792/Home/Index地址的時候實際上是發了兩次請求:
- 上篇我們就介紹過 app.Context.RemapHandler(new MvcHandler()); 這一句表示將當前請求交給Mvchandler這個去處理,既然是這裡指定的MvcHandler,那我們在Web.config裡面配置的 <handlers> 節點還有什麼意義呢?也就是說,這裡配置的即使是另外一個HttpHandler,那麼最終程式還是會轉給MvcHandler,是不是這樣呢?既然我們感覺這裡的handlers節點配置了也沒用,那我們將handlers節點去掉再試試呢?結果是去掉handlers節點之後,仍然得到的是上面的結果。這一點說明app.Context.RemapHandler()這一句的優先順序要比web.config裡面的handlers節點的高。
這裡通過以上實現和配置,我們的Swift.MVC已經具有處理http請求的能力,但還不能算一個完整意義上的框架,下面來繼續完善。
二、版本二:完善MvcHandler和UrlRoutingModule
這個版本,UrlRoutingModule我們還是沿用的System.Web.Routing裡面的機制,我們主要來看看MvcHandler這部分的實現。
1、UrlRoutingModule的完善
UrlRoutingModule.cs的完整程式碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web; using System.Web.Routing; namespace Swift.MVC { public class UrlRoutingModule : IHttpModule { #region Property private RouteCollection _routeCollection; [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This needs to be settable for unit tests.")] public RouteCollection RouteCollection { get { if (_routeCollection == null) { _routeCollection = RouteTable.Routes; } return _routeCollection; } set { _routeCollection = value; } } #endregion public void Dispose() { //throw new NotImplementedException(); } public void Init(HttpApplication app) { app.PostResolveRequestCache += app_PostResolveRequestCache; } void app_PostResolveRequestCache(object sender, EventArgs e) { var app = (HttpApplication)sender; //0.將HttpContext轉換為HttpContextWrapper物件(HttpContextWrapper繼承HttpContextBase) var contextbase = new HttpContextWrapper(app.Context); PostResolveRequestCache(contextbase); } public virtual void PostResolveRequestCache(HttpContextBase context) { //1.傳入當前上下文物件,得到與當前請求匹配的RouteData物件 RouteData routeData = this.RouteCollection.GetRouteData(context); if (routeData == null) { return; } //2.從RouteData物件裡面得到當前的RouteHandler物件。 IRouteHandler routeHandler = routeData.RouteHandler; if (routeHandler == null) { return; } //3.根據HttpContext和RouteData得到RequestContext物件 RequestContext requestContext = new RequestContext(context, routeData); context.Request.RequestContext = requestContext; //4.根據RequestContext物件得到處理當前請求的HttpHandler(MvcHandler)。 IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext); if (httpHandler == null) { return; } //5.請求轉到HttpHandler進行處理(進入到ProcessRequest方法)。這一步很重要,由這一步開始,請求才由UrlRoutingModule轉到了MvcHandler裡面 context.RemapHandler(httpHandler); } } }
上述程式碼基本都是從Framework原始碼裡面拷貝出來的,註釋中的0、1、2、3、4、5分別對應著MVC路由過程中的各個步驟,詳見上篇。
這裡我們自定義了一個實現IRouteHandler的型別,用來返回處理請求的HttpHandler是哪個,比如這裡我們定義的MvcRouteHandler返回的HttpHandler是MvcHandler。它的程式碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web.Routing; namespace Swift.MVC { public class MvcRouteHandler:IRouteHandler { public System.Web.IHttpHandler GetHttpHandler(RequestContext requestContext) { return new MvcHandler(); } } }
2、MvcHandler部分的完善
首先還是丟擲MvcHandler.cs的原始碼:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web; namespace Swift.MVC { public class MvcHandler : IHttpHandler { public virtual bool IsReusable { get { return false; } } public virtual void ProcessRequest(HttpContext context) { //寫入MVC的版本到HttpHeader裡面 //AddVersionHeader(httpContext); //移除引數 //RemoveOptionalRoutingParameters(); //步驟1.從上下文的Request.RequestContext中取到RouteData物件。這裡和UrlRoutingModule裡面的context.Request.RequestContext = requestContext;對應。 var routeData = context.Request.RequestContext.RouteData; //步驟2.從當前的RouteData裡面得到請求的控制器名稱 string controllerName = routeData.GetRequiredString("controller"); //步驟3.得到控制器工廠 IControllerFactory factory = new SwiftControllerFactory(); //步驟4.通過預設控制器工廠得到當前請求的控制器物件 IController controller = factory.CreateController(context.Request.RequestContext, controllerName); if (controller == null) { return; } try { //步驟5.執行控制器的Action controller.Execute(context.Request.RequestContext); } catch { } finally { //步驟6.釋放當前的控制器物件 factory.ReleaseController(controller); } } } }
關於上述程式碼,我們說明以下幾點。
2.1、關於控制器工廠
上述程式碼註釋中的步驟1、2不難理解,就是從配置的路由規則中獲取當前請求控制器的名稱。要理解步驟3,需要先說一說MVC原始碼裡面的控制器工廠。先來看看原始碼裡面這段如何實現:
在原始碼裡面的MvcHandler的ProcessRequest方法裡面有這麼一句: factory = ControllerBuilder.GetControllerFactory(); 。在MvcHandler裡面ControllerBuilder這樣定義:
internal ControllerBuilder ControllerBuilder { get { if (_controllerBuilder == null) { _controllerBuilder = ControllerBuilder.Current; } return _controllerBuilder; } set { _controllerBuilder = value; } }
原來在MvcHandler中建立控制器工廠並不是直接使用IControllerFactroy的實現,而是使用了ControllerBuilder這個物件,這個物件採用了單例模式的實現;MvcHandler通過ControllerBuilder物件獲取到一個例項,然後通過ControllerBuilder創建出IControllerFactory實現,ControllerBuilder管理著IControllerFactory的建立過程。
關於ControllerBuilder裡面的GetControllerFactory()方法的實現,我們不必細究,但是我們需要知道的是在MVC裡面有一個預設的控制器工廠的實現類DefaultControllerFactory。我們來看看
IControllerFactory介面的定義:
public interface IControllerFactory { IController CreateController(RequestContext requestContext, string controllerName); SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName); void ReleaseController(IController controller); }
DefaultControllerFactory的定義:
public class DefaultControllerFactory : IControllerFactory { public virtual IController CreateController(RequestContext requestContext, string controllerName) { if (requestContext == null) { throw new ArgumentNullException("requestContext"); } if (String.IsNullOrEmpty(controllerName) && !requestContext.RouteData.HasDirectRouteMatch()) { throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName"); } Type controllerType = GetControllerType(requestContext, controllerName); IController controller = GetControllerInstance(requestContext, controllerType); return controller; } public virtual void ReleaseController(IController controller) { IDisposable disposable = controller as IDisposable; if (disposable != null) { disposable.Dispose(); } } //...... }
上述的兩個方法CreateController()和ReleaseController()通過名字都可以很好理解,分別對應著建立控制器和釋放控制器。
瞭解了上述MVC裡面控制器工廠的實現細節,我們自己也來建一個自己的控制器工廠,不過為了簡化,我們這裡直接去new了一個工廠的實現類。先來看看我們Swift.MVC的控制器工廠。
控制器工廠介面:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web.Routing; namespace Swift.MVC { //控制器建立工廠 public interface IControllerFactory { //建立控制器 IController CreateController(RequestContext requestContext, string controllerName); //釋放控制器 void ReleaseController(IController controller); } }
控制器工廠實現類:
using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace Swift.MVC { public class SwiftControllerFactory:IControllerFactory { #region Public //通過當前的請求上下文和控制器名稱得到控制器的物件 public IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName) { if (requestContext == null) { throw new ArgumentNullException("requestContext"); } if (string.IsNullOrEmpty(controllerName)) { throw new ArgumentException("controllerName"); } //得到當前的控制型別 Type controllerType = GetControllerType(requestContext, controllerName); if (controllerType == null) { return null; } //得到控制器物件 IController controller = GetControllerInstance(requestContext, controllerType); return controller; } //釋放控制器物件 public void ReleaseController(IController controller) { IDisposable disposable = controller as IDisposable; if (disposable != null) { disposable.Dispose(); } } #endregion #region Privates //得到當前請求的控制器例項 private IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType) { var oRes = Activator.CreateInstance(controllerType) as IController; return oRes; } //得到當前請求的控制器型別 private Type GetControllerType(System.Web.Routing.RequestContext requestContext, string controllerName) { //從路由配置資訊裡面讀取名稱空間和程式集 object routeNamespaces; object routeAssembly; requestContext.RouteData.Values.TryGetValue("namespaces", out routeNamespaces); requestContext.RouteData.Values.TryGetValue("assembly", out routeAssembly); //通過反射得到控制器的型別 var type = Assembly.Load(routeAssembly.ToString()).GetType(routeNamespaces.ToString() + "." + controllerName + "Controller"); return type; } #endregion } }
這裡博主主要用到了反射去例項化控制器例項。
2.2、控制器的父類實現
上述介紹了控制器工廠的實現。除了控制器工廠,還有我們的控制器介面以及父類的相關實現。
控制器介面的定義:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web.Routing; namespace Swift.MVC { public interface IController { void Execute(RequestContext requestContext); } }
控制器抽象Base類的實現:(這個抽象類的作用更多在於定義一些約束、檢查之類)
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Swift.MVC { //這個類主要定義約束 public abstract class ControllerBase:IController { public abstract void Execute(System.Web.Routing.RequestContext requestContext); } }
控制器抽象子類的實現:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Swift.MVC { public abstract class Controller:ControllerBase,IDisposable { public override void Execute(System.Web.Routing.RequestContext requestContext) { //反射得到Action方法 Type type = this.GetType(); string actionName = requestContext.RouteData.GetRequiredString("action"); System.Reflection.MethodInfo mi = type.GetMethod(actionName); //執行該Action方法 mi.Invoke(this, new object[] { });//呼叫方法 } public void Dispose() { //throw new NotImplementedException(); } } }
這裡讓Controller類實現IDispose介面,照應了上文控制器工廠裡面的ReleaseController()方法,主要起到釋放資源的作用。
3、測試及程式碼釋疑
由於上述程式碼用到了System.Web.Routing裡面的元件,所以,需要在測試專案裡面配置路由規則,這裡需要注意一點,我們上面的MvcRouteHandler就是在這裡注入進去的。在測試專案裡面新建一個全域性配置檔案如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Routing; using System.Web.Security; using System.Web.SessionState; namespace MyTestMVC { public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { RouteTable.Routes.Add("defaultRoute", new Route("{controller}/{action}/{id}", new RouteValueDictionary(new { controller = "Home", action = "Index", id = "", namespaces = "MyTestMVC.Controllers", assembly = "MyTestMVC" }), new Swift.MVC.MvcRouteHandler())); } protected void Application_BeginRequest(object sender, EventArgs e) { } } }
然後在測試專案裡面模擬MVC新建一個Controllers資料夾,裡面新建一個測試的控制器HomeController:
using Swift.MVC; using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace MyTestMVC.Controllers { public class HomeController : Controller { public void Index() { HttpContext.Current.Response.Write("Hello MVC"); } } }
然後啟動專案,訪問http://localhost:16792/Home/Index。執行過程以及程式碼釋疑如下:
(1)來看看 RouteData routeData = this.RouteCollection.GetRouteData(context); 這一句
通過上圖可知,this.RouteCollection裡面儲存的是上述全域性配置檔案裡面新增進去的路由規則,然後呼叫GetRouteData(context)方法,傳入當前請求的上下文,得到當前請求的RouteData物件,我們可以看到在這個RouteData物件裡面,已經包含了當前請求的控制器和action的名稱。
(2)監視 IRouteHandler routeHandler = routeData.RouteHandler;
通過上圖可以看到在routeData裡面我們的RouteHandler已經是MvcRouteHandler物件了,還記得我們在全域性配置檔案裡面有這樣一個配置:
RouteTable.Routes.Add("defaultRoute", new Route("{controller}/{action}/{id}", new RouteValueDictionary(new { controller = "Home", action = "Index", id = "", namespaces = "MyTestMVC.Controllers", assembly = "MyTestMVC" }), new Swift.MVC.MvcRouteHandler()));
在Add方法的最後需要傳一個IRouteHandler的物件,我們上文定義過一個MvcRouteHandler去實現了IRouteHandler,這個MvcRouteHandler在這裡就派上用場了,原來我們的RouteHandler是可以自定義的。就是因為這裡配置過這個,所以在GetRouteData()方法裡面,就將MvcRouteHandler物件給了routeData物件的RouteHandler屬性,終於知道這裡的MvcRouteHandler是如何過來的了。這裡可配置IRouteHandler也說明了MVC原理的靈活性,我們可以自定義RouteHandler,然後再IRouteHandler介面的GetHttpHandler()方法裡面自定義處理當前請求的HttpHandler。
(3) RequestContext requestContext = new RequestContext(context, routeData);context.Request.RequestContext = requestContext; 這兩句看上去不起眼,就是封裝了一個RequestContext物件,然後將它給到了當前上下文的Request.RequestContext。實際上,這裡非常重要,因為這個requestContext物件包含了我們當前請求的路由資訊,後面MvcHandler裡面需要從這裡取到當前請求的控制器和Action的名稱,待會看了後面的程式碼,你會更加清晰。
(4)再來看 IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext); 這一句
上文註釋(2) 裡面說了,routeHandler物件實際上是一個MvcRouteHandler物件,當它呼叫GetHttpHandler(),看下定義即可明白:
public class MvcRouteHandler:IRouteHandler { public System.Web.IHttpHandler GetHttpHandler(RequestContext requestContext) { return new MvcHandler(); } }
這裡就是返回了一個MvcHandler,用來處理Http請求。
(5) context.RemapHandler(httpHandler); 這一句自然不必多說,請當前攔截到的請求交給MvcHandler的ProcessRequest方法處理,這一句執行完成之後,請求便轉入到MvcHandler的ProcessRequest方法裡面。縱觀上述幾個過程,可以說是一環扣一環,每一句都有它的意義所在,最後封裝完成之後,真正處理請求還是在MvcHandler裡面。接下來我們來看看ProcessRequest裡面的程式碼。
(6)下面我們來看看MvcHandler類裡面ProcessRequest方法這一句: var routeData = context.Request.RequestContext.RouteData; 。還記得上述註釋3中封裝的RequestContext物件嗎,沒錯,這裡就用到了這個物件,我們從這個物件裡面取到當前請求的RouteData物件。
(7) string controllerName = routeData.GetRequiredString("controller"); 這一句不難理解:取到當前請求的的Controller名稱。結果如下:
(8)得到控制器工廠這個沒什麼說的,為了簡化,我們直接new了一個預設的控制器工廠。下面重點來看看 IController controller = factory.CreateController(context.Request.RequestContext, controllerName); 這一句。在控制器工廠的實現類裡面實現了CreateController()這個方法。
public IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName) { if (requestContext == null) { throw new ArgumentNullException("requestContext"); } if (string.IsNullOrEmpty(controllerName)) { throw new ArgumentException("controllerName"); } //得到當前的控制型別 Type controllerType = GetControllerType(requestContext, controllerName); if (controllerType == null) { return null; } //得到控制器物件 IController controller = GetControllerInstance(requestContext, controllerType); return controller; } //得到當前請求的控制器例項 private IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType) { var oRes = Activator.CreateInstance(controllerType) as IController; return oRes; } //得到當前請求的控制器型別 private Type GetControllerType(System.Web.Routing.RequestContext requestContext, string controllerName) { //從路由配置資訊裡面讀取名稱空間和程式集 object routeNamespaces; object routeAssembly; requestContext.RouteData.Values.TryGetValue("namespaces", out routeNamespaces); requestContext.RouteData.Values.TryGetValue("assembly", out routeAssembly); //通過反射得到控制器的型別 var type = Assembly.Load(routeAssembly.ToString()).GetType(routeNamespaces.ToString() + "." + controllerName + "Controller"); return type; }
原理不難理解,主要還是反射,因為我們當前請求的控制器類在測試專案裡面,所以反射的時候需要指定當前測試專案的程式集,通過這裡的程式碼可以看出,在UrlRoutingModule裡面封裝的RequestContext物件實在是太重要了,因為各個地方都需要用到它。博主覺得這裡還有待優化,等想到更好的辦法再來逐步優化。此步得到結果:
(9)得到控制器物件之後,就是執行Action方法了: controller.Execute(context.Request.RequestContext); 。這裡博主按照原始碼裡面的構造封裝了IController、ControllerBase、Controller三個級別的介面以及父類。Execute方法的實現在Controller裡面:
public abstract class Controller:ControllerBase,IDisposable { public override void Execute(System.Web.Routing.RequestContext requestContext) { //反射得到Action方法 Type type = this.GetType(); string actionName = requestContext.RouteData.GetRequiredString("action"); System.Reflection.MethodInfo mi = type.GetMethod(actionName); //執行該Action方法 mi.Invoke(this, new object[] { });//呼叫方法 } }
這裡再次用到了RequestContext物件,由此可以看出,RequestContext物件幾乎貫穿整個MvcHandler,再次應徵了上述註釋(3)中說的它的重要性。
上述程式碼就是通過反射Action方法,然後執行該方法,之後請求就會盡到我們HomeController的Index()方法裡面。
(10)執行Index()方法
請求進入到Index()方法之後,然後就是從Model裡面獲取資料,再然後就是返回View,整個MVC的原理就是如此。當然博主這裡的Swift.MVC還只是將請求轉到了Index裡面,剩餘的Model可以自己寫,但是View的部分還完全沒有,待有時間完善。
(11)執行完Action之後,最後就是釋放當前的Controller物件了。在finally裡面有這麼一句: factory.ReleaseController(controller); 。還是來看看ReleaseController方法的定義:
//釋放控制器物件 public void ReleaseController(IController controller) { IDisposable disposable = controller as IDisposable; if (disposable != null) { disposable.Dispose(); } }
至此,我們通過地址http://localhost:16792/Home/Index訪問,整個請求的開始、執行、資源釋放就基本結束。現在回過頭來理解這個原理,你覺得還難嗎~~
4、向Swift.MVC裡面加入Model
如果你還嫌上述例子太簡單,我們可以來個稍微複雜點的例子。我們向測試專案裡面引入jquery、bootstrap等元件,新增Models資料夾,向下面加入User.cs
namespace MyTestMVC.Models { public class User { public int Id { get; set; } public string UserName { get; set; } public int Age { get; set; } public string Address { get; set; } public string Remark { get; set; } } }
然後我們向HomeController裡面另一個Action定義如下: