动态注册HttpModule

通过前面的章节,我们知道HttpApplication在初始化的时候会初始化所有配置文件里注册的HttpModules,那么有一个疑问,能否初始化之前动态加载HttpModule,而不是只从Web.config里读取?

答案是肯定的, ASP.NET MVC3发布的时候提供了一个Microsoft.Web.Infrastructure.dll文件,这个文件就是提供了动态注册HttpModule的功能,那么它是如何以注册的呢?我们先去MVC3的源码看看该DLL的源代码。

注:该DLL位置在C:\Program Files\Microsoft ASP.NET\ASP.NET Web Pages\v1.0\Assemblies\下

我们发现了一个静态类DynamicModuleUtility,里面有个RegisterModule方法引起了我的注意:

// Call from PreAppStart to dynamically register an IHttpModule, just as if you had added it to the
// <modules> section in Web.config. 
[SecuritySafeCritical] 
public static void RegisterModule(Type moduleType) {
    if (DynamicModuleReflectionUtil.Fx45RegisterModuleDelegate != null) { 
        // The Fx45 helper exists, so just call it directly.
        DynamicModuleReflectionUtil.Fx45RegisterModuleDelegate(moduleType);
    }
    else { 
        // Use private reflection to perform the hookup.
        LegacyModuleRegistrar.RegisterModule(moduleType); 
    } 
}

通过代码和注释我们可以看到,这个方法就是让我们动态注册IHttpModule的,而且由于.Net4.5已经有helper类支持了,所以直接就可以用,其它版本使用LegacyModuleRegistrar.RegisterModule来动态注册IHttpModule 的。而这个方法里又分IIS6和IIS7集成或经典模式之分,代码大体上是一致的,我们这里就只分析IIS6版本的代码:

private static void AddModuleToClassicPipeline(Type moduleType) {
    // This works by essentially adding a new entry to the <httpModules> section defined 
    // in ~/Web.config. Need to set the collection to read+write while we do this. 

    // httpModulesSection = RuntimeConfig.GetAppConfig().HttpModules; 
    // httpModulesSection.Modules.bReadOnly = false;
    // httpModulesSection.Modules.Add(new HttpModuleAction(...));
    // httpModulesSection.Modules.bReadOnly = true;
 
    HttpModulesSection httpModulesSection = null;
    try { 
        object appConfig = _reflectionUtil.GetAppConfig(); 
        httpModulesSection = _reflectionUtil.GetHttpModulesFromAppConfig(appConfig);
        _reflectionUtil.SetConfigurationElementCollectionReadOnlyBit(httpModulesSection.Modules, false /* value */); 

        DynamicModuleRegistryEntry newEntry = CreateDynamicModuleRegistryEntry(moduleType);
        httpModulesSection.Modules.Add(new HttpModuleAction(newEntry.Name, newEntry.Type));
    } 
    finally {
        if (httpModulesSection != null) { 
            _reflectionUtil.SetConfigurationElementCollectionReadOnlyBit(httpModulesSection.Modules, true /* value */); 
        }
    } 
}

上面代码的注释非常重要,通过注释我们可以看到,该方法先从RuntimeConfig.GetAppConfig().HttpModules获取HttpModules集合,然后在集合里添加需要注册的新HttpModule,那就是说HttpApplication在初始化所有HttpModule之前必须将需要注册的HttpModule添加到这个集合里,那是在哪个周期呢?HttpApplication之前是HostingEnvironment,那是不是在这里可以注册呢?我们去该类查看一下相关的代码,在Initialize方法里突然发现一个貌似很熟悉的代码BuildManager.CallPreStartInitMethods(),代码如下:

// call AppInitialize, unless the flag says not to do it (e.g. CBM scenario).
// Also, don't call it if HostingInit failed (VSWhidbey 210495) 
if(!HttpRuntime.HostingInitFailed) { 
    try {
        BuildManager.CallPreStartInitMethods(); 
        if ((hostingFlags & HostingEnvironmentFlags.DontCallAppInitialize) == 0) {
            BuildManager.CallAppInitializeMethod();
        }
    } 
    catch (Exception e) {
        // could throw compilation errors in 'code' - report them with first http request 
        HttpRuntime.InitializationException = e; 

        if ((hostingFlags & HostingEnvironmentFlags.ThrowHostingInitErrors) != 0) { 
            throw;
        }
    }
}

通过去BuildManager类去查看该方法的详情,最终发现了如下这个方法:

internal static ICollection<MethodInfo> GetPreStartInitMethodsFromAssemblyCollection(IEnumerable<Assembly> assemblies) {
    List<MethodInfo> methods = new List<MethodInfo>(); 
    foreach (Assembly assembly in assemblies) {
        PreApplicationStartMethodAttribute[] attributes = null;
        try {
            attributes = (PreApplicationStartMethodAttribute[])assembly.GetCustomAttributes(typeof(PreApplicationStartMethodAttribute), inherit: true); 
        }
        catch { 
            // GetCustomAttributes invokes the constructors of the attributes, so it is possible that they might throw unexpected exceptions. 
            // (Dev10 bug 831981)
        } 

        if (attributes != null && attributes.Length != 0) {
            Debug.Assert(attributes.Length == 1);
            PreApplicationStartMethodAttribute attribute = attributes[0]; 
            Debug.Assert(attribute != null);
 
            MethodInfo method = null; 
            // Ensure the Type on the attribute is in the same assembly as the attribute itself
            if (attribute.Type != null && !String.IsNullOrEmpty(attribute.MethodName) && attribute.Type.Assembly == assembly) { 
                method = FindPreStartInitMethod(attribute.Type, attribute.MethodName);
            }

            if (method != null) { 
                methods.Add(method);
            } 
            else { 
                throw new HttpException(SR.GetString(SR.Invalid_PreApplicationStartMethodAttribute_value,
                    assembly.FullName, 
                    (attribute.Type != null ? attribute.Type.FullName : String.Empty),
                    attribute.MethodName));
            }
        } 
    }
    return methods; 
}

发现了该方法会查找应用程序下所有的程序集,判断如果程序集标记为PreApplicationStartMethodAttribute特性,就会执行这个特性里指定的方法(静态方法),再检查该类的代码:

[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] 
public sealed class PreApplicationStartMethodAttribute : Attribute {
     private readonly Type _type;
    private readonly string _methodName;
    public PreApplicationStartMethodAttribute(Type type, string methodName) { 
        _type = type;
        _methodName = methodName; 
    } 

    public Type Type {   get { return _type; }   } 
    public string MethodName {  get {  return _methodName; }   }
}

这时候,心里应该有数了吧,我们可以在这里指定一个静态方法名称,然后在该方法去通过如下代码去注册一个自定义的HttpModule(注意我们只能使用一次):

DynamicModuleUtility.RegisterModule(typeof(CustomModule));

我们来做个试验试试我们分析的结果是不是正确,首先创建一个自定义HttpModule,代码如下:

public class CustomModule : IHttpModule
{
    public void Init(HttpApplication context)
    {
        context.BeginRequest += new EventHandler(context_BeginRequest);
            
    }

    void context_BeginRequest(object sender, EventArgs e)
    {
        HttpApplication ap = sender as HttpApplication;
        if (ap != null)
        {
            ap.Response.Write("汤姆大叔测试PreApplicationStartMethod通过!<br/>");
        }
    }

    public void Dispose()
    {
        //nothing to do here
    }
}

然后在创建一个用于注册这个HttpModule的类并且带有静态方法:

public class PreApplicationStartCode
{
    private static bool hasLoaded;

    public static void PreStart()
    {
        if (!hasLoaded)
        {
            hasLoaded = true;
            //注意这里的动态注册,此静态方法在Microsoft.Web.Infrastructure.DynamicModuleHelper
            DynamicModuleUtility.RegisterModule(typeof(CustomModule));
        }
    }
}

接着要安装要求对该程序添加一个特性,代码如下:

[assembly: PreApplicationStartMethod(typeof(WebApplication1.Test.PreApplicationStartCode), "PreStart")]

最后,编译运行,会发现所有的页面顶部都会出现我们指定的文字(汤姆大叔测试PreApplicationStartMethod通过!),截图如下:

这就证实了我们的分析结果是正确的,怎么样,这个功能不错吧,不需要每次都在web.config里定义你的HttpModule咯,而且我们以后封装自己的类库就方便多了,不需要在网站程序集里指定代码去启动自己封装好的单独类库了,因为我们可以在自己的类库里直接使用这种方式实现自动注册的功能。下个章节,我们将介绍一个利用此功能开发的超强类库。

注:同一个程序集只能使用一次这个特性来调用一个静态方法。