1. 程式人生 > >為ASP.NET MVC擴充套件非同步Action功能(上)

為ASP.NET MVC擴充套件非同步Action功能(上)

非同步請求處理是ASP.NET 2.0中引入的高階特性,它依託IO Complete Port,對於提高IO密集型應用程式的吞吐量非常重要(詳見原理描述效能測試)。但是目前ASP.NET MVC框架缺少非同步Action功能,這也就是老趙經常掛在嘴邊的那個“目前ASP.NET MVC所缺少的非常重要的功能”。在TechED 2008 China的Session中我曾經給出過一個所謂的“解決方案”,但是它複雜性之高使那個解決方案有太多限制。為了彌補TechED上的遺憾,以及準備.NET開發大會上的ASP.NET MVC最佳實踐的Session,我在春節休假期間仔細思考了一下這方面的問題,得出了一個相對不錯的擴充套件:完整,方便,並且非常輕巧——核心邏輯程式碼只有200行左右,這意味著絕大部分功能將會委託給框架中現成的內容,確保了擴充套件的穩定,高效並且擁有較好的向後相容性。

值得一提的是,我在1/26號便基於ASP.NET MVC的Beta版本寫出了這個擴充套件的第一個版本,而在不久之後微軟釋出了ASP.NET MVC RC。我在移植解決方案的過程中發現ASP.NET MVC RC在框架設計上進行了較大的改進,這使得我在構建擴充套件時的策略發生了些許變化。令人欣喜的是,RC版本的這些變化對於構建一個擴充套件,尤其是現在這種“低端”級別的擴充套件變得更加容易。ASP.NET MVC框架實現了它“到處可擴充套件”的承諾。

那麼我們現在就來詳細分析一下這個擴充套件的實現方式。

請求處理方式的改變

在制定基本改造策略之前,我們需要了解ASP.NET MVC框架目前的架構及請求處理流程。如下:

  1. 在應用程式啟動時(此時還沒有接受任何請求),將針對MVC請求的Route策略註冊至ASP.NET Routing模組。此時每個Route策略(即Route物件)中的RouteHandler屬性為ASP.NET MVC框架中的MvcRouteHandler。
  2. 當ASP.NET Routing模組接收到一個匹配某個Route策略的HTTP請求時,將會呼叫該Route物件中RouteHandler物件的GetHttpHandler以獲取一個HttpHandler,並交由ASP.NET執行。MvcRouteHandler永遠將返回一個MvcHandler物件。
  3. MvcHandler在執行時,將取出RouteData中的controller值,並以此構建一個實現了IController介面的控制器物件,並呼叫IController介面的Execute方法執行該控制器。
  4. 對於一個ASP.NET MVC應用程式來說,大部分控制器將會繼承System.Web.Mvc.Controller型別。Controller類將會從RouteData獲取action值,並交給實現IActionInvoker介面的物件來執行一個Action。
  5. ……

如果我們要將這個流程改造成非同步處理,那麼就要讓它符合ASP.NET架構中的非同步處理方式。ASP.NET架構對於非同步請求的處理可以體現在好幾種方式上,例如非同步頁面,非同步Http Module等,而最適合目前場合的做法自然是非同步Http Handler。為實現一個非同步Handler,我們需要讓處理請求的Handler實現IHttpAsyncHandler介面,而不是傳統的IHttpHandler介面。IHttpAsyncHandler介面中的BeginProcessRequest和EndProcessRequest兩個方法構成了.NET中的APM(Aynchronous Programming Model,非同步程式設計模型)模式,可以使用“二段式”的非同步呼叫來處理一個HTTP請求。

您應該已經發現,如果我們要支援非同步Action,就必須根據當前的請求資訊來確認究竟是執行一個IHttpHandler物件還是IHttpAsyncHandler物件。而在ASP.NET MVC框架在預設情況下是在Http Handler(即MvcHandler物件)內部進行控制器的檢查,構造和呼叫。這為時已晚,我們必須講這些邏輯提前到Routing過程中才行。幸運的是,ASP.NET Routing所支援的IRouteHandler就像是ASP.NET中的IHttpHandlerFactory,可以根據情況生成不同的Handler來執行。因此,我們只要構建一個新的IRouteHandler型別即可。於是就誕生了AsyncMvcRouteHandler——可以想象的出,其中的部分程式碼與框架中的MvcHandler相同,因為在一定程度上我們的確只是把原本在MvcHandler裡做的事情給提前了:

public class AsyncMvcRouteHandler : IRouteHandler
{
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        string controllerName = requestContext.RouteData.GetRequiredString("controller");

        varfactory = ControllerBuilder.Current.GetControllerFactory();
        varcontroller = factory.CreateController(requestContext, controllerName);
        if (controller == null)
        {
            throw new InvalidOperationException(...);
        }

        varcoreController = controller as Controller;
        if (coreController == null)
        {
            return new SyncMvcHandler(controller, factory, requestContext);
        }
        else
        {

            string actionName = requestContext.RouteData.GetRequiredString("action");
            return IsAsyncAction(coreController, actionName, requestContext) ?
                (IHttpHandler)new AsyncMvcHandler(coreController, factory, requestContext) :
                (IHttpHandler)new SyncMvcHandler(controller, factory, requestContext);
        }
    }

    internal static bool IsAsyncAction(
        Controller controller, string actionName, RequestContext requestContext)
    {
        ...
    }
}

在GetHttpHandler方法中,我們先從RouteData的controller欄位中獲取控制器的名字,並通過註冊在ControllerBuilder上的Factory來建立一個實現了IController介面的控制器物件。由於我們需要使用Controller類中包含的ActionInvoker來輔助檢測Action的非同步需求,因此我們會設法將其轉化為Controller型別。如果轉換成功,就會取出RouteData中的action欄位的值,並通過IsAsyncAction方法來確認當前Action是否應該非同步執行。如果是,則返回一個實現了IHttpAsyncHandler的AsyncMvcHandler物件,否則就返回一個實現IHttpHandler的SyncMvcHandler物件。

至於AsyncMvcRouteHandler的使用,只需在MapRoute時將Route Handler重新設定一下即可:

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 = "" }  // Parameter defaults
    ).RouteHandler = new AsyncMvcRouteHandler();
}

檢查是否為非同步Action

從上面的程式碼中我們已經形成了一個約定:如果要執行一個非同步Action,那麼控制器物件必須為Controller型別。這個約定的目的是為了使用Controller類中包含的IActionInvoker——確切地說,是ControllerActionInvoker型別裡的功能。因此,另一個約定便是Controller的ActionInvoker物件必須返回一個ControllerActionInvoker的例項。

ControllerActionInvoker中有一些輔助方法,能夠返回對於一個Controller或Action的描述物件。從一個Action描述物件中我們可以獲取關於這個Action的各種資訊,而它是否被標記了AsyncActionAttribute,就是我們判斷這個Action是否應該被非同步執行的依據。如下:

private static object s_methodInvokerMutex = new object();
private static MethodInvoker s_controllerDescriptorGetter;

internal static bool IsAsyncAction(
    Controller controller, string actionName, RequestContext requestContext)
{
    var actionInvoker = controller.ActionInvoker as ControllerActionInvoker;
    if (actionInvoker == null) return false;

    if (s_controllerDescriptorGetter == null)
    {
        lock (s_methodInvokerMutex)
        {
            if (s_controllerDescriptorGetter == null)
            {
                BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;
                MethodInfo method = typeof(ControllerActionInvoker).GetMethod(
                    "GetControllerDescriptor", bindingFlags);
                s_controllerDescriptorGetter = new MethodInvoker(method);
            }
        }
    }

    var controllerContext = new ControllerContext(requestContext, controller);
    var controllerDescriptor = (ControllerDescriptor)s_controllerDescriptorGetter.Invoke(
        actionInvoker, controllerContext);
    var actionDescriptor = controllerDescriptor.FindAction(controllerContext, actionName);
    return actionDescriptor == null ? false :
        actionDescriptor.GetCustomAttributes(typeof(AsyncActionAttribute), false).Any();
}

ControllerActionInvoker型別中有個protected方法GetControllerDescriptor,它接受一個ControllerContext型別的引數,並返回一個ControllerDescriptor物件來描述當前控制器,而從該描述物件中可以通過FindAction方法獲得一個ActionDescriptor物件來描述即將執行的Action。如果是一個不存在的Action,那麼就返回false,最後就通過SyncMvcHandler物件來執行預設的行為。當且僅當該Action上擁有AsyncActionAttribute標記時,才說明它應該被非同步執行,返回true。此外,這段程式碼中用到了MethodInvoker,這是一個輔助類,它來源於Fast Reflection Library,它實現了反射呼叫功能,但是它的效能十分接近於方法的直接呼叫,我在這篇文章中詳細描述了這個專案的功能和使用。

這段程式碼便涉及到ASP.NET MVC RC版本在Beta版本基礎上的改進。在原先的ControllerActionInvoker類中只有獲取Action方法的MethodInfo,而沒有RC中各描述物件這樣的抽象型別。從目前的設計上來看,我們使用的都是基於反射的抽象描述型別的子類。例如預設情況下,我們通過ActionDescriptor抽象型別訪問的實際上是ReflectedActionDescriptor型別的例項。這是一個很有用的改進,由於我們通過描述物件進行抽象,於是我們就可以:

  • 使用不同的實現方式來描述各物件,預設情況下是使用基於反射(也就是“約定”)的實現,如果需要的話我們也可以使用基於配置檔案的方式替換現有實現。
  • 使用特定物件的描述方式可以不拘泥於內部細節,例如一個非同步的Action可能就由兩個方法組成。
  • 有了特定的描述物件,也方便新增額外的屬性,例如該Action是否應該非同步執行,是否應該禁用Session State等等。
  • ……