细说 HttpHandler 的映射过程

在ASP.NET编程模型中,一个来自客户端的请求要经过一个称为管线的处理过程。 在整个处理请求中,相对于其它对象来说,HttpHandler的处理算得上是整个过程的核心部分。 由于HttpHandler的重要地位,我前面已经有二篇博客对它过一些使用上的介绍。【用Asp.net写自己的服务框架】中谈到了它的一般使用方法。【细说ASP.NET的各种异步操作】又详细地介绍了异步HttpHandler的使用方式。

今天的博客将着重介绍HttpHandler的配置,创建以及重用过程,还将涉及HttpHandlerFactory的内容。

回顾HttpHandler

HttpHandler其实是一类统称:泛指实现了IHttpHandler接口的一些类型,这些类型有一个共同的功能,那就是可以用来处理HTTP请求。IHttpHandler的接口定义如下:

// 定义 ASP.NET 为使用自定义 HTTP 处理程序同步处理 HTTP Web 请求而实现的协定。
public interface IHttpHandler
{
    // 获取一个值,该值指示其他请求是否可以使用 System.Web.IHttpHandler 实例。
    //
    // 返回结果:
    //     如果 System.Web.IHttpHandler 实例可再次使用,则为 true;否则为 false。
    bool IsReusable { get; }

    // 通过实现 System.Web.IHttpHandler 接口的自定义 HttpHandler 启用 HTTP Web 请求的处理。
    void ProcessRequest(HttpContext context);
}

有关HttpHandler的各类用法,可参考我的博客【用Asp.net写自己的服务框架】, 本文将不再重复说明了。它还有一个异步版本:

// 摘要:
//     定义 HTTP 异步处理程序对象必须实现的协定。
public interface IHttpAsyncHandler : IHttpHandler
{
    // 摘要:
    //     启动对 HTTP 处理程序的异步调用。
    //
    // 参数:
    //   context:
    //     一个 System.Web.HttpContext 对象,该对象提供对用于向 HTTP 请求提供服务的内部服务器对象(如 Request、Response、Session
    //     和 Server)的引用。
    //
    //   extraData:
    //     处理该请求所需的所有额外数据。
    //
    //   cb:
    //     异步方法调用完成时要调用的 System.AsyncCallback。如果 cb 为 null,则不调用委托。
    //
    // 返回结果:
    //     包含有关进程状态信息的 System.IAsyncResult。
    IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData);
    //
    // 摘要:
    //     进程结束时提供异步处理 End 方法。
    //
    // 参数:
    //   result:
    //     包含有关进程状态信息的 System.IAsyncResult。
    void EndProcessRequest(IAsyncResult result);
}

IHttpAsyncHandler接口的二个方法该如何使用,可参考我的博客【细说ASP.NET的各种异步操作】, 本文也将不再重复讲解。

如果我们创建了一个自定义的HttpHandler,那么为了能让它处理某些HTTP请求,我们还需将它注册到web.config中,就像下面这样:

<httpHandlers>
    <add path="*.fish" verb="*" validate="true" type="MySimpleServiceFramework.AjaxServiceHandler"/>
</httpHandlers>

虽然我以前的博客中也曾多次涉及到HttpHandler,但感觉还是没有把它完整地说清楚,今天的博客将继续以前的话题,因为我认为HttpHandler实在是太重要了。

HttpHandler的映射过程

在博客【用Asp.net写自己的服务框架】中, 我从MSDN中摘选了一些ASP.NET管线事件,在这些事件中,第10个事件【根据所请求资源的文件扩展名(在应用程序的配置文件中映射),选择实现 IHttpHandler 的类,对请求进行处理】 就是本文要介绍的重点事件。这个事件也是HttpHandler创建的地方。

由于IIS6,7在管线事件触发机制上的有一定的差别,本文将以ASP.NET 2.0以及IIS6的运行方式来介绍这个过程,IIS7的集成模式下只是触发机制不同,但事件的绝大部分是一样的。 管线事件由HttpApplication控制,由MapHandlerExecutionStep负责封装这个事件的执行过程:

internal class MapHandlerExecutionStep : HttpApplication.IExecutionStep
{
    private HttpApplication _application;

    void HttpApplication.IExecutionStep.Execute()
    {
        HttpContext context = this._application.Context;
        HttpRequest request = context.Request;

        //我删除了一些与本文无关的调用代码

        context.Handler = this._application.MapHttpHandler(
                context, request.RequestType, request.FilePathObject, request.PhysicalPathInternal, false);
    }

Execute()最终调用了MapHttpHandler()方法,这个映射过程是由HttpApplication实现的。 MapHttpHandler方法的实现如下:注意代码中我加入的注释。

/// <summary>
/// 根据虚拟路径映射到具体的HttpHandler对象
/// </summary>
/// <param name="context">HttpContext的实例</param>
/// <param name="requestType">GET, POST ...</param>
/// <param name="path">一个虚拟路径映射的字符串: /abc.aspx</param>
/// <param name="pathTranslated">要请求的物理文件的路径</param>
/// <param name="useAppConfig"></param>
/// <returns>具体的HttpHandler对象</returns>
internal IHttpHandler MapHttpHandler(HttpContext context, string requestType, 
                                    VirtualPath path, string pathTranslated, bool useAppConfig)
{
    // 说明:这段代码是我精简后的代码。为了便于理解,我去掉了一些与请求无关的代码行。

    IHttpHandler handler = (context.ServerExecuteDepth == 0) ? context.RemapHandlerInstance : null;

    //用于将当前线程的运行帐号切换为web.config指定的帐号
    using( new ApplicationImpersonationContext() ) {

        // 如果之前调用过context.RemapHandler(),则直接返回那个HttpHandler
        // 把这个判断放在这里似乎没有意义,应该放在using前面,那样岂不是更快吗?【fish li个人意见】
        if( handler != null )
            return handler;

        // 到 <httpHandlers> 配置中查找一个能与requestType以及path匹配的配置项
        HttpHandlerAction mapping = this.GetHandlerMapping(context, requestType, path, useAppConfig);
        if( mapping == null ) 
            throw new HttpException("Http_handler_not_found_for_request_type");

        // 获取IHttpHandlerFactory对象,这是比较重要的调用。
        IHttpHandlerFactory factory = this.GetFactory(mapping);

        // 尝试转换成IHttpHandlerFactory2对象。设计二个IHttpHandlerFactory的原因与参数的可见类型有关。
        IHttpHandlerFactory2 factory2 = factory as IHttpHandlerFactory2;

        // 调用IHttpHandlerFactory对象的GetHandler方法获取具体的HttpHandler对象
        if( factory2 != null ) 
            handler = factory2.GetHandler(context, requestType, path, pathTranslated);                
        else 
            handler = factory.GetHandler(context, requestType, path.VirtualPathString, pathTranslated);

        // 将这二个对象都添加到【临时】回收列表。在管线处理的结尾处,会调用HandlerWithFactory.Recycle()
        // 进而会调用factory.ReleaseHandler(),最后会丢掉_handlerRecycleList的引用。
        // 因此factory的重用与这里无关,它只可能会影响handler
        this._handlerRecycleList.Add(new HandlerWithFactory(handler, factory));
    }
    return handler;
}

今天的代码将着重分析这段代码,以揭示HttpHandler的映射过程。

在这段代码中,有3个主要的调用过程: 1. this.GetHandlerMapping(context, requestType, path, useAppConfig) 2. this.GetFactory(mapping) 3. factory.GetHandler(context, requestType, path.VirtualPathString, pathTranslated) 后面将着重分析这三个调用。

HttpContext.RemapHandler()

在MapHttpHandler()方法的开始处,有这么一段代码,不知您注意没有?

IHttpHandler handler = (context.ServerExecuteDepth == 0) ? context.RemapHandlerInstance : null;

if( handler != null )
    return handler;

这段代码是什么意思呢? 为了能让您较为简单地理解这段代码的意义,请看我准备的一个示例: 我创建了一个TestRemapHandler.ashx文件:

<%@ WebHandler Language="C#" Class="TestRemapHandler" %>

using System;
using System.Web;

public class TestRemapHandler : IHttpHandler {
    
    public void ProcessRequest (HttpContext context) {
        context.Response.ContentType = "text/plain";
        context.Response.Write("Hello TestRemapHandler");
    }
 
    public bool IsReusable {
        get {
            return false;
        }
    }
}

至于这个文件在运行时能输出什么,我想我就不用截图了。 接下来,我再创建一个Global.asax文件,并写了一个事件处理方法:

//protected void Application_PostResolveRequestCache(object sender, EventArgs e)
//{
//    HttpApplication app = (HttpApplication)sender;
//    if( app.Request.FilePath.EndsWith(".ashx", StringComparison.OrdinalIgnoreCase) )
//        app.Context.RemapHandler(new MyTestHandler());
//}

// 或者是下面这样

protected void Application_PostResolveRequestCache(object sender, EventArgs e)
{
    HttpApplication app = (HttpApplication)sender;
    if( app.Request.FilePath.EndsWith("/TestRemapHandler.ashx", StringComparison.OrdinalIgnoreCase) )
        app.Context.RemapHandler(new MyTestHandler());
}

MyTestHandler是我定义的一个自定义的HttpHandler,后面的示例也会用到它,所以,我现在就将它的实现代码贴出来:

public class MyTestHandler : IHttpHandler
{
    private Counter _counter = new Counter();

    public bool IsReusable
    {
        get { return true; }
    }

    public void ProcessRequest(HttpContext context)
    {
        _counter.ShowCountAndRequestInfo(context);
    }
}

类型Counter是一个简单的计数器,它的实现如下:

/// <summary>
/// 一个简单的计数器
/// </summary>
public class Counter
{
    private int _count;

    public void ShowCountAndRequestInfo(HttpContext context)
    {
        _count++;
        context.Response.ContentType = "text/plain";
        context.Response.Write("count: " + _count.ToString());
        context.Response.Write("\r\n");
        context.Response.Write(context.Request.RawUrl);
    }
}

现在,您可以想像一下当我再次访问TestRemapHandler.ashx时,会在浏览器中看到什么?

以下是我看到的结果:

细说 HttpHandler 的映射过程_第1张图片

从截图中,我们可以看出,服务端的输出并不是TestRemapHandler.ashx产生的,而是由MyTestHandler产生的。 这也说明我调用Context.RemapHandler()确实影响了后面的MapHttpHandler过程。 现在我们再回过头来再看一下前面那段代码:

IHttpHandler handler = (context.ServerExecuteDepth == 0) ? context.RemapHandlerInstance : null;

if( handler != null )
    return handler;

这段代码也主要是为配合HttpContext.RemapHandler()一起工作的。 这里要补充的是:context.ServerExecuteDepth的判断是说有没有调用HttpServerUtility.Execute()

小结:如果我们在MapHttpHandler事件前调用HttpContext.RemapHandler(),将会直接影响后面的映射过程, 或者说,我们可以直接指定一个HttpHandler,而不是让ASP.NET来替我们来决定。

HttpContext.RemapHandler()的另类用途

我在【用Asp.net写自己的服务框架】曾演示过ASP.NET URL路由的设计思路,虽然那个示例能较好地反映微软的Routing组件的工作方式,但对于示例来说,有些代码还是可以简化的。 今天我将使用HttpContext.RemapHandler()来简化那段代码,简化后的版本如下:

internal class MyServiceUrlRoutingModule2 : IHttpModule
{
    public void Init(HttpApplication app)
    {
        app.PostResolveRequestCache += new EventHandler(app_PostResolveRequestCache);
    }

    private void app_PostResolveRequestCache(object sender, EventArgs e)
    {
        HttpApplication app = (HttpApplication)sender;

        // 根据HttpContext,获取合适的HttpHandler,这个过程俗称路由
        // 通常就是根据URL去主动寻找一个合适的HttpHandler

        // 查找过程仍使用上一版的方法
        MyServiceHandler handler = MyServiceUrlRoutingModule.GetHandler(app.Context);
        if( handler != null )
            // 直接设置已找到的HttpHandler
            app.Context.RemapHandler(handler);
    }

    public void Dispose()
    {
    }
}

在MyServiceUrlRoutingModule2这个新版本中,基本上跳过了MapHttpHandler()中所有复杂的处理逻辑,因此会更快。

这种方法虽然更简单,但它只能设置一个参数IHttpHandler,因此,如果有其它的参数需要一起传递,则要修改相关的HttpHandler类型, 以便容纳需要传递的参数。 例如,在【我的服务框架】中,MyServiceHandler类型就专门定义了一个属性ServiceInfo来保存要调用的方法信息。MyServiceUrlRoutingModule.GetHandler()的实现代码如下:

internal static MyServiceHandler GetHandler(HttpContext context)
{
    NamesPair pair = FrameworkRules.ParseNamesPair(context.Request);
    if( pair == null )
        return null;

    InvokeInfo vkInfo = ReflectionHelper.GetInvokeInfo(pair);
    if( vkInfo == null )
        ExceptionHelper.Throw404Exception(context);


    MyServiceHandler handler = MyServiceHandlerFactory.GetHandler(vkInfo);
    handler.ServiceInfo = new ServiceInfo(pair, vkInfo);
    return handler;
}

写到这里,有个问题把我难住了:为什么ASP.NET 3.5 UrlRoutingModule不用RemapHandler()而是采用较复杂的方法呢? 难道是为了能保留二个参数吗?但那个【context.Request.Path】没有必要保存呢。 实在是没有想通,最后想到ASP.NET 4.0 UrlRoutingModule会不会不一样了? 于是,只好再次安装 .net framework 4.0 了(没办法,老家的旧机器上还真没有.net 4.0), 结果当我用Reflector.exe找到4.0版本的UrlRoutingModule时,发现它已被移到System.Web.dll中,而且,ASPL.NET 4.0也在使用RemapHandler() ! 看来微软当初忘记了这个方法。

GetHandlerMapping()

在MapHttpHandler()方法中,有下面这个调用:

// 到 <httpHandlers> 配置中查找一个能与requestType以及path匹配的配置项
HttpHandlerAction mapping = this.GetHandlerMapping(context, requestType, path, useAppConfig);

接下来,我们再来看一下这个调用的实现代码:

private HttpHandlerAction GetHandlerMapping(HttpContext context, string requestType, VirtualPath path, bool useAppConfig)
{
    CachedPathData pathData = null;
    HandlerMappingMemo cachedHandler = null;
    HttpHandlerAction mapping = null;
    if (!useAppConfig)
    {
        // 先从缓存中查找
        pathData = context.GetPathData(path);
        cachedHandler = pathData.CachedHandler;
        if ((cachedHandler != null) && !cachedHandler.IsMatch(requestType, path))
        {
            cachedHandler = null;
        }
    }
    if (cachedHandler == null)
    {
        // 根据配置文件,查找映射
        mapping = (useAppConfig ? RuntimeConfig.GetAppConfig().HttpHandlers 
                                : RuntimeConfig.GetConfig(context).HttpHandlers)
            .FindMapping(requestType, path);

        if (!useAppConfig)
        {
            // 将查找到的结果放入缓存
            cachedHandler = new HandlerMappingMemo(mapping, requestType, path);
            pathData.CachedHandler = cachedHandler;
        }
        return mapping;
    }
    return cachedHandler.Mapping;
}

代码中的重要阶段,我加了一点注释。我们尤其要注意的FindMapping(requestType, path)的那个调用。 但前面的那个RuntimeConfig.GetAppConfig().HttpHandlers 和RuntimeConfig.GetConfig(context).HttpHandlers)又可以得到什么结果呢? 为了能让您更容易地理解这个调用,我准备了一段代码,可以直观地显示这个调用结果:

public partial class TestRuntimeConfig : System.Web.UI.Page
{
    protected string HttpHandlers;

    protected void Page_Load(object sender, EventArgs e)
    {
        string typeName = typeof(HttpRequest).AssemblyQualifiedName
                                        .Replace("HttpRequest", "Configuration.RuntimeConfig");
        Type type = Type.GetType(typeName);

        bool useAppConfig = Request.QueryString["useAppConfig"] == "1";

        // 由于RuntimeConfig类型的可见性是internal,
        // 所以,我不能直接用它声明变量,只能使用object类型
        object config = null;

        if( useAppConfig )
            config = type.InvokeMember("GetAppConfig",
                BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic,
                null, null, null);
        else
            config = type.InvokeMember("GetConfig",
                BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic,
                null, null, new object[] { this.Context });


        HttpHandlersSection section = (HttpHandlersSection)type.InvokeMember("HttpHandlers",
             BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic,
             null, config, null);

        HttpHandlers = string.Format("总共 {0} 个配置项。<br />", section.Handlers.Count) +
            string.Join("<br />", (
                from h in section.Handlers.Cast<HttpHandlerAction>()
                let action = string.Format("path=\"{0}\" verb=\"{1}\" validate=\"{2}\" type=\"{3}\"",
                        h.Path, h.Verb, h.Validate, h.Type)
                select action).ToArray());
    }
}

我的示例网站中的web.config中的<httpHandlers>配置节定义如下:

<httpHandlers>
    <add path="Ajax*.*.aspx,Ajax*/*.aspx" verb="*" validate="true" 
                type="MySimpleServiceFramework.AjaxServiceHandler, MySimpleServiceFramework" />
    <add path="*.test" verb="*" validate="true" type="MyTestHandler" />
</httpHandlers>

那么,前面的示例代码的运行结果如下:

细说 HttpHandler 的映射过程_第2张图片

通过前面分析可知,代码中的HttpHandlers实际就是在运行时所能访问到的<httpHandlers>配置集合。 而且,我们在网站中所指定的配置会放在集合的前面,ASP.NET的默认配置会在靠后的位置。 接下来的FindMapping(requestType, path)的实现如下:

internal HttpHandlerAction FindMapping(string verb, VirtualPath path)
{
    this.ValidateHandlers();
    for (int i = 0; i < this.Handlers.Count; i++)
    {
        HttpHandlerAction action = this.Handlers[i];
        if (action.IsMatch(verb, path))
        {
            return action;
        }
    }
    return null;
}

从代码可以看出,其实所谓的【查找】过程,也就是【逐一匹配】每个在<httpHandlers>定义的配置项。

至于action.IsMatch()的匹配方式,我们可以从调用所传递的参数看出,其实只是判断【verb, path】这二个参数而已。 这个匹配过程的实现代码较长,我就不贴出它们的实现代码了,而是打算通过示例来说明它是如何匹配的。

action.IsMatch()的匹配方式也可以算是GetHandlerMapping()的核心过程。而这里的action的类型其实是HttpHandlerAction, 它与<httpHandlers>配置项一一对应。我们可以先来看一下MSDN是如何解释这个配置项的:

细说 HttpHandler 的映射过程_第3张图片

从MSDN的解释中可以看出:verb,path都可以支持【通配符(*)】,并且可以使用逗号(,)来分隔多个预期项。ASP.NET在实现通配符时,内部使用正则表达式的方式,具体过程可以自己去阅读相关代码。

下面,我将通过一个具体的配置项来分析这些配置参数:

<add path="Ajax*.*.aspx,Ajax*/*.aspx" verb="*" validate="true" 
        type="MySimpleServiceFramework.AjaxServiceHandler, MySimpleServiceFramework" />

1. path="Ajax*.*.aspx,Ajax*/*.aspx":表示可以接受【Ajax*.*.aspx】或者【Ajax*/*.aspx】这样的URL格式。 2. verb="*": 表示可以接受所有的HTTP调用,如:GET,POST 等等。 3. type="MySimpleServiceFramework.AjaxServiceHandler, MySimpleServiceFramework": 表示当某个请求匹配path,verb时, 交给MySimpleServiceFramework程序集中的MySimpleServiceFramework.AjaxServiceHandler来处理请求。 4. validate="true":表示在第一次读取<httpHandlers>时,验证type的设置是否有效。如果此项设为false,则表示延迟验证(创建时)。默认值:true

对于这4个属性,有2个比较重要: 1. type:  type中的字符串必须能在运行时找到一个可创建的类型,且该类型必须实现IHttpHandler或者IHttpHandlerFactory接口。 2. path:  在很多资料以及技术书籍中,一般都设置为某类文件扩展名,如:*.xxx ,事实上,我们完全可以设置为一个URL模式, 而且还可以设置多个URL模式。比如上面的配置示例中的path参数,具体在使用时,可以响应来自客户端的以下请求:

$(function(){
    $("#btnSubmit").click( function(){
        $.ajax({
                // 注意:下面的二个url地址都是可以使用的。
                url: "/AjaxServices/GetMd5.aspx",
                //url: "/AjaxServices.GetMd5.aspx",
                
                // 注意:下面的二种type的设置也都是可以使用的。
                type: "POST", 
                //type: "GET", 
                
                data: { str: $("#txtInput").val() },
                dataType: "text",
                success: function(responseText){
                    $("#md5Result").text(responseText);
                }
            });    
    });
});

注意:在path中直接使用某个扩展名的好处是不受配置项的顺序影响,而且容易理解,因为可读性较好。

这里要补充一点:MySimpleServiceFramework是我在博客【用Asp.net写自己的服务框架】中实现的那个框架。 在服务端,可以使用下面的代码来处理前面客户端发出的请求:

[MyService]
public class AjaxServices
{
    [MyServiceMethod]
    public string GetMd5(string str)
    {
        if( str == null )
            str = string.Empty;

        byte[] bb = (new MD5CryptoServiceProvider()).ComputeHash(Encoding.Default.GetBytes(str));
        return BitConverter.ToString(bb).Replace("-", "");
    }
}

前面的示例配置中:path="Ajax*.*.aspx,Ajax*/*.aspx" ,我使用了aspx这个扩展名。 我想大家都知道aspx这个扩展名是ASP.NET可处理的扩展名,为什么我还可以使用呢? 其实这个问题的答案在我前面的截图以及FindMapping的实现代码中:由于我们指定的配置要优先于ASP.NET的默认配置,所以先有机会参与匹配,并能匹配成功。

小结:在GetHandlerMapping()过程中,会根据请求的URL地址以及HTTP调用动作(GET,POST),返回一个在<httpHandlers>定义的配置项(HttpHandlerAction类型)。

GetFactory()

在GetHandlerMapping()返回一个HttpHandlerAction的配置项后,HttpApplication会调用this.GetFactory(mapping);获取一个IHttpHandlerFactory ,本小节将来分析这个过程。

首先,我们还是来看一下GetFactory的实现代码:

private Hashtable _handlerFactories = new Hashtable();

private IHttpHandlerFactory GetFactory(HttpHandlerAction mapping)
{
    // 先尝试从内部缓存中获取
    HandlerFactoryCache cache = (HandlerFactoryCache) this._handlerFactories[mapping.Type];
    if (cache == null)
    {
        // 创建一个缓存项并保存
        cache = new HandlerFactoryCache(mapping);
        this._handlerFactories[mapping.Type] = cache;
    }
    // 返回工厂对象
    return cache.Factory;
}

代码中做了哪些事情,我在注释中有说明。这个方法的最后一定会返回一个IHttpHandlerFactory的实例。

看到这里,您觉得奇怪吗? 可能我们仅仅只是实现了一个自定义的IHttpHanlder接口,并没有实现IHttpHandlerFactory,那么ASP.NET是如何处理的呢?

请注意 new HandlerFactoryCache(mapping) 这行代码,这可不是简单地创建一个缓存对象,具体实现代码如下:

internal class HandlerFactoryCache
{
    private IHttpHandlerFactory _factory;

    internal HandlerFactoryCache(HttpHandlerAction mapping)
    {
        // 根据HttpHandlerAction配置中的type字符串创建对应类型的对象
        object obj2 = mapping.Create();

        if( obj2 is IHttpHandler ) {
            // 如果我们的配置是一个IHttpHandler,
            // 那么ASP.NET会为我们包装成一个HandlerFactoryWrapper
            this._factory = new HandlerFactoryWrapper((IHttpHandler)obj2, this.GetHandlerType(mapping));
        }
        else {
            // 如果不是IHttpHandler以及IHttpHandlerFactory,就抛出异常
            if( !(obj2 is IHttpHandlerFactory) ) {
                throw new HttpException("Type_not_factory_or_handler");
            }
            this._factory = (IHttpHandlerFactory)obj2;
        }
    }

代码中,我加入的注释已经回答了前面提出的问题:如果是一个IHttpHandler对象,ASP.NET会再创建一个HandlerFactoryWrapper对象来包装IHttpHandler对象, 如果是IHttpHandlerFactory对象,则直接返回。 我们再来看一下HandlerFactoryWrapper的实现代码:

internal class HandlerFactoryWrapper : IHttpHandlerFactory
{
    private IHttpHandler _handler;
    private Type _handlerType;

    internal HandlerFactoryWrapper(IHttpHandler handler, Type handlerType)
    {
        this._handler = handler;
        this._handlerType = handlerType;
    }

    public IHttpHandler GetHandler(HttpContext context, 
                                string requestType, string url, string pathTranslated)
    {
        if( this._handler == null ) 
            this._handler = (IHttpHandler)HttpRuntime.CreateNonPublicInstance(this._handlerType);

        return this._handler;
    }

    public void ReleaseHandler(IHttpHandler handler)
    {
        // 一个HttpHandler是否能重用,这里就是一个典型的实现方式
        if( !this._handler.IsReusable ) 
            this._handler = null;
    }
}

小结:HttpApplication会根据web.config中的配置去查找一个匹配的IHttpHandlerFactory类型, 即使我们配置的是自定义的IHttpHandler类型,而不是IHttpHandlerFactory类型,调用过程依然如此。

GetHandler()

在ASP.NET中,定义了二个版本的HttpHandlerFactory接口,分别为:

public interface IHttpHandlerFactory
{
    IHttpHandler GetHandler(HttpContext context, 
                            string requestType, string url, string pathTranslated);
    void ReleaseHandler(IHttpHandler handler);
}

internal interface IHttpHandlerFactory2 : IHttpHandlerFactory
{
    IHttpHandler GetHandler(HttpContext context, 
                            string requestType, VirtualPath virtualPath, string physicalPath);
}

设计IHttpHandlerFactory接口的目的是为了在创建和重用IHttpHandler对象时,保留了足够的扩展机会, 而IHttpHandlerFactory2则是一个仅供微软使用的内部接口(因为VirtualPath类型的可见性也是internal)。

我们都知道aspx, ashx能直接处理HTTP请求,它们都实现了IHttpHandler接口。它们能处理HTTP请求也因为ASP.NET已经配置过它们。 以下是它们的默认配置:

<httpHandlers>
    <add path="*.aspx" verb="*" type="System.Web.UI.PageHandlerFactory" validate="true"/>
    <add path="*.ashx" verb="*" type="System.Web.UI.SimpleHandlerFactory" validate="true"/>
</httpHandlers>

有趣的是:PageHandlerFactory和SimpleHandlerFactory都实现了IHttpHandlerFactory2接口,因此,它们都可以根据要请求的路径创建一个IHttpHandler实例。

从ASP.NET的默认配置,我们也可以看到:type参数是可以设置为一个实现IHttpHandlerFactory接口的类型,而不一定要求是实现IHttpHandler接口的类型。

小结:HttpApplication在处理请求时,并不会直接创建一个IHttpHandler的实例,而是先获取一个IHttpHandlerFactory的对象, 再以接口的形式调用GetHandler()方法来获取一个IHttpHandler实例。

IHttpHandler.IsReusable

IHttpHandler接口有个IsReusable属性。MSDN对这个属性的说明也非常简单:

获取一个值,该值指示其他请求是否可以使用 IHttpHandler 实例。

按照这个说明,当我们直接在创建一个实现IHttpHandler的类型,并在web.config中注册到一个自定义的扩展名时,情况会如何呢? 我们再来看一下前面所提过的示例代码(MyTestHandler的):

public class MyTestHandler : IHttpHandler
{
    private Counter _counter = new Counter();

    public bool IsReusable
    {
        get { return true; }
    }

    public void ProcessRequest(HttpContext context)
    {
        _counter.ShowCountAndRequestInfo(context);
    }
}

web.config中的配置为:

<httpHandlers>
    <add path="*.test" verb="*" validate="true" type="MyTestHandler" />
</httpHandlers>

当我连续5次访问 http://localhost:51652/abc.test?id=1 时,会在浏览器中看到以下输出结果:

细说 HttpHandler 的映射过程_第4张图片

从这个截图来看,显然:MyTestHandler的实例被重用了。

 

我想很多人都创建过ashx文件,IDE会为我们创建一个实现了IHttpHandler接口的类型,在实现IsReusable属性时,一般都会这样:

public bool IsReusable {
    get {
        return false;
    }
}

有些人看到这个,会想:如果返回true,就可以重用IHttpHandler实例,达到优化性能的目的。 但事实上,即使你在ashx中返回true也是无意义的,因为您可以试着这样去实现这个属性:

public bool IsReusable
{
    get { throw new Exception("这里不起作用。"); }
}

如果您访问那个ashx,会发现:根本没有异常出现! 因此,我们可以得出一个结论:默认情况下,IsReusable不能决定一个ashx的实例是否能重用。

这个结果太奇怪了。为什么会这样呢?

 

前面我们看到*.ashx的请求交给SimpleHandlerFactory来创建相应的HttpHandler对象, 然而当ASP.NET调用SimpleHandlerFactory.GetHandler()方法时, 该方法会直接创建并返回我们实现的类型实例。 换句话说:SimpleHandlerFactory根本不使用IHttpHandler.IsReusable的属性,因此,这种情况下,想重用ashx的实例是不可能的事, 所以,即使我在实现IsReusable属性时,写上抛异常的语句,根本也不会被调用。

同样的事情还发在aspx页面的实例上,所以,在默认情况下,我们不可能重用aspx, ashx的实例。

至于aspx的实例不能重用,除了和PageHandlerFactory有关外,还与Page在实现IHttpHandler.IsReusable有关,以下是Page的实现方式:

public bool IsReusable
{
    get { return false; }
}

从代码可以看到微软在Page是否重用上的明确想法:就是不允许重用!

由于Page的IsReusable属性我们平时看不到,我想没人对它的重用性有产生过疑惑,但ashx就不同了, 它的IsReusable属性的代码是摆在我们面前的,任何人都可以看到它,试想一下:当有人发现把它设为true or false时都不起作用,会是个什么想法? 估计很多人会郁闷。

小结:IHttpHandler.IsReusable并不能决定是否重用HttpHanlder !

实现自己的HttpHandlerFactory

通过前面的示例,我们也看到了,虽然IHttpHandler定义了一个IsReusable属性,但它并不能决定此类型的实例是否能得到重用。 重不重用,其实是由HttpHandlerFactory来决定的。ashx的实例不能重用就是一个典型的例子。

下面我就来演示如何实现自己的HttpHandlerFactory来重用ashx的实例。示例代码如下(注意代码中的注释):

internal class ReusableAshxHandlerFactory : IHttpHandlerFactory
{
    private Dictionary<string, IHttpHandler> _cache 
        = new Dictionary<string, IHttpHandler>(200, StringComparer.OrdinalIgnoreCase);

    public IHttpHandler GetHandler(HttpContext context, 
                            string requestType, string virtualPath, string physicalPath)
    {
        string cacheKey = requestType + virtualPath;

        // 检查是否有缓存的实例(或者可理解为:被重用的实例)
        IHttpHandler handler = null;
        if( _cache.TryGetValue(cacheKey, out handler) == false ) {
            // 根据请求路径创建对应的实例
            Type handlerType = BuildManager.GetCompiledType(virtualPath);

            // 确保一定是IHttpHandler类型
            if( typeof(IHttpHandler).IsAssignableFrom(handlerType) == false )
                throw new HttpException("要访问的资源没有实现IHttpHandler接口。");

            // 创建实例,并保存到成员字段中
            handler = (IHttpHandler)Activator.CreateInstance(handlerType, true);

            // 如果handler要求重用,则保存它的引用。
            if( handler.IsReusable )
                _cache[cacheKey] = handler;
        }

        return handler;
    }

    public void ReleaseHandler(IHttpHandler handler)
    {
        // 不需要处理这个方法。
    }
}

为了能让HttpHandlerFactory能在ASP.NET中运行,还需要在web.config中注册:

<httpHandlers>
    <add path="*.ashx" verb="*" validate="false" type="ReusableAshxHandlerFactory"/>
</httpHandlers>

有了这个配置后,我们可以创建一个Handler2.ashx来测试效果:

<%@ WebHandler Language="C#" Class="Handler2" %>

using System;
using System.Web;

public class Handler2 : IHttpHandler {

    private Counter _counter = new Counter();

    public bool IsReusable
    {
        get { return true; }
    }

    public void ProcessRequest(HttpContext context)
    {
        _counter.ShowCountAndRequestInfo(context);
    }
}

在多次访问Handler2.ashx后,我们可以看到以下效果:

细说 HttpHandler 的映射过程_第5张图片

再来看看按照IDE默认生成的IsReusable会在运行时出现什么结果。示例代码:

<%@ WebHandler Language="C#" Class="Handler1" %>

using System;
using System.Web;

public class Handler1 : IHttpHandler {

    private Counter _counter = new Counter();

    public bool IsReusable
    {
        get {
            // 如果在配置文件中启用ReusableAshxHandlerFactory,那么这里将会被执行。
            // 可以尝试切换下面二行代码测试效果。

            //throw new Exception("这里不起作用。");
            return false;
        }
    }

    public void ProcessRequest(HttpContext context)
    {
        _counter.ShowCountAndRequestInfo(context);
    }
}

此时,无论我访问Handler1.ashx多少次,浏览器始终显示如下结果:

细说 HttpHandler 的映射过程_第6张图片

如果我启用代码行 throw new Exception("这里不起作用。"); 将会看到以下结果:

细说 HttpHandler 的映射过程_第7张图片

终于,我们期待的黄页出现了。 此时,如果我在web.config中将ReusableAshxHandlerFactory的注册配置注释起来,发现Handler1.ashx还是可以访问的。

回想一下前面我们看到的IHttpHandlerFactory接口,它还定义了一个ReleaseHandler方法,这个方法又是做什么的呢? 对于这个方法,MSDN也有一句简单的说明:

使工厂可以重用现有的处理程序实例。

对于这个说明,我认为并不恰当。如果按照HandlerFactoryWrapper的实现方式,那么这个解释是正确的。 但我前面的示例中,我在实现这个方法时,没有任何代码,但一样可以达到重用HttpHandler的目的。 因此,我认为重用的方式取决于具体的实现方式。

小结:IHttpHandler.IsReusable并不能完全决定HttpHandler的实例是否能重用,它只起到一个指示作用。HttpHandler如何重用,关键还是要由HttpHandlerFactory来实现。

是否需要IsReusable = true ?

经过前面文字讲解以及示例演示,有些人可能会想:我在实现IHttpHandler的IsReusable属性时, 要不要返回true呢?(千万别模仿我的示例代码抛异常哦。

如果返回true,则HttpHandler能得到重用,或许某些场合下,是可以达到性能优化的目的。 但是,它也可能会引发新的问题:HttpHandler实例的一些状态会影响后续的请求。 也正是由于这个原因,aspx, ashx 的实例在默认情况下,都是不重用的。

有些人还可能会担心:被重用的HttpHandler是否有线程安全问题? 理论上,在ASP.NET中,只要使用static的数据成员都会有这个问题。 不过,这里所说的被重用的单个HttpHandler实例在处理请求过程中,只会被一个线程所调用,因此,它的实例成员还是线程安全的。 但有一点需要注意:在HttpHandlerFactory中实现重用HttpHandler时,缓存HttpHandler的容器要保证是线程安全的。

如果您希望重用HttpHandler来提升程序性能,那么我建议应该考虑以下问题: HttpHandler的所有数据成员都能在处理请求前初始化。(通常会在后期维护时遗忘,尤其是多人维护时)

小结:在通常情况下,当实现IsReusable时返回false,虽然性能上不是最优,但却是最安全的做法。

HttpHandlerFactory的主要用途

前面示例演示了如何使用HttpHandlerFactory来重用HttpHandler,但设计HttpHandlerFactory并不是完全为了这个目的, 它的主要用途还是如何创建HttpHandler,而且定义IHttpHandlerFactory的主要目的是为了扩展性。

我想很多人也许使用过 Web Service ,它运行在ASP.NET平台上,自然也有对应的HttpHandler,我们来看看asmx这个扩展名是如何映射的。

<add path="*.asmx" verb="*"  validate="false"
     type="System.Web.Services.Protocols.WebServiceHandlerFactory, 
     System.Web.Services, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>

接着找WebServiceHandlerFactory,最后发现是这样创建的HttpHandler :

internal IHttpHandler CoreGetHandler(Type type, 
        HttpContext context, HttpRequest request, HttpResponse response)
{
    // ..... 已删除一些无关的代码

    bool isAsync = protocol.MethodInfo.IsAsync;
    bool enableSession = protocol.MethodAttribute.EnableSession;
    if( isAsync ) {
        if( enableSession ) {
            return new AsyncSessionHandler(protocol);
        }
        return new AsyncSessionlessHandler(protocol);
    }
    if( enableSession ) {
        return new SyncSessionHandler(protocol);
    }
    return new SyncSessionlessHandler(protocol);
}

这才是Factory嘛!

老实说,看到这几句话,我是眼前一亮:用HttpHandlerFactory来动态处理【是否支持Session】实在是太合适了。

这里有必要补充一下:

internal class SyncSessionHandler : SyncSessionlessHandler, IRequiresSessionState
{
}

小结:HttpHandlerFactory用途并非是为了专门处理HttpHandler的重用,它只是一个Factory,WebServiceHandlerFactory从另一个角度向我们展示了HttpHandlerFactory在扩展性方面所体现的重要作用。

你可能感兴趣的:(handler)