由做网站操作日志想到的HttpModule应用_.NET_编程开发_程序员俱乐部

中国优秀的程序员网站程序员频道CXYCLUB技术地图
热搜:
更多>>
 
您所在的位置: 程序员俱乐部 > 编程开发 > .NET > 由做网站操作日志想到的HttpModule应用

由做网站操作日志想到的HttpModule应用

 2013/12/22 18:08:58  冲动  博客园  我要评论(0)
  • 摘要:背景在以前的Web项目中,记录用户操作日志,总是在方法里,加一行代码,记录此时用户操作类型与相关信息。该记录日志的方法对原来的业务操作侵入性较强,也比较零散,不便于查看和管理。那么有没有更加通用点的方法呢。同事建议我,写个HttpModule,能够得到请求的Http报文,同时获取到输出的Http报文,这样大致上能够分析出请求的行为了。最初对HttpModule很陌生,一直也没机会用到,故开始对这个方法,有点抵触。后来利用了强大的搜索引擎,发现确实有人向这方面做出了努力。如
  • 标签:应用 网站 操作 HTTP

背景

  在以前的Web项目中,记录用户操作日志,总是在方法里,加一行代码,记录此时用户操作类型与相关信息。该记录日志的方法对原来的业务操作侵入性较强,也比较零散,不便于查看和管理。那么有没有更加通用点的方法呢。

  同事建议我,写个HttpModule,能够得到请求的Http报文,同时获取到输出的Http报文,这样大致上能够分析出请求的行为了。最初对HttpModule很陌生,一直也没机会用到,故开始对这个方法,有点抵触。后来利用了强大的搜索引擎,发现确实有人向这方面做出了努力。如《初涉电子商务系统开发随想--第二篇-基于asp.net Mvc的通用日志管理方法》。我也来写的试试吧。

HttpModule

  首先就必须先了解HttpModule,了解它的事件发生顺序,在什么时候又可以访问到Session,如何获取到Http输入流和输出流等。下面是参考的内容:

  选择HttpHandler还是HttpModule?

  ASP.NET中httpmodules与httphandlers全解析 

  最后我选择了 AcquireRequestState 这个事件,因为在其中可以访问到Session,以备不时之需。Module代码如下:

class="code_img_closed" src="/Upload/Images/2013122218/0015B68B3C38AA5B.gif" alt="" />logs_code_hide('60e104ca-a602-4e10-bc28-dfd3b977a1a0',event)" src="/Upload/Images/2013122218/2B1B950FA3DF188F.gif" alt="" />
    public class HttpFilterModule : IHttpModule, IRequiresSessionState
    {
        HttpFilterFactory filterHandler= new HttpFilterFactory();

        public void Init(HttpApplication context)
        {
            context.AcquireRequestState += new EventHandler((sender, e) =>
            {
                var app = sender as HttpApplication;
                app.Response.Filter = new HttpFileterStream(app, filterHandler);
            });
        }

        public void Dispose()
        {
        }
    }
HttpFilterModule

Http输入流和输出流

  读取Http的输入流很简单,相比如果之前有做Http接口的朋友,很清楚。直接在Request.InputStream 就可以读取到全部内容,那么输入内容是不是也这样方便呢?再次感谢强大的搜索引擎,关于读取输出流的内容,见 《Asp.net2.0 中自定义过滤器对Response内容进行处理》。摘抄部分如下:

在代码设计前分析了一下,前三个都很好解决,对于截获服务器返回的正文,准备用HttpResponse 对象中的Output 和 OutputStream 属性输出信息来解决。

可是在正式编码的过程中,发现Output和OutputStream 并不是想像中可以直接把数据转出取回,耗费了近两天的时间,想尽了一切办法可还是仅仅可以追加内容并无法读取。

在网上查阅到,对于HttpResponse 对象,仅仅可以使用过滤器来对其中将要输出的内容进行修改。
这个过滤器要继承自Stream 类,并要实现其中的虚方法。看来之前企图使用HttpWriter,TextWriter,Stream,HttpStream 这些类来转出数据完全是错误的。

  自定义HttpFileterStream 替换Response.Filter ,而网页在输出时,一定会调用 Write 方法,而Write方法里的参数,则是我们需要的东西了。

  这样输入流和输出流就能获取到了。

如何动态加载日志处理类型

  既然能够拿到每次请求的输入流和输出流,接下来则是将这些需要的信息交给工厂类来操作。目前所处的项目是 MVC3.0,前端采用extjs,控制器输出的则是各种Json。结合项目情况,我仅仅需要将 Controller和Action得到,然后交给指定的Log处理类,它来处理,流程基本上就走完了。

  但在实际的编码过程中,遇到一点问题,则是怎么更好的去处理这个映射关系。于是想到mvc,它最初得到的不也是url地址,只不过该url 包含了Controller与Action信息,那它是如何找到对应的Controller对象呢。这次强大的搜索引擎也没能解决我的问题,那就只能自己动手。这里有个插曲,是控制器的命名空间可以自己任意定义,mvc也能根据控制器的名字找到它。自己也想实现这样的效果,最后下载mvc的源代码单步调试,看到的结果吓了我一跳,想来也是在情理之中。

  mvc在初始化时,会加载所有的程序集,遍历程序集的所有类型,然后在做筛选,筛选后并将类型缓存起来。摘抄类如下:

namespace System.Web.Mvc {
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.IO;
    using System.Linq;
    using System.Reflection;

    internal static class TypeCacheUtil {

        private static IEnumerable<Type> FilterTypesInAssemblies(IBuildManager buildManager, Predicate<Type> predicate) {
            // Go through all assemblies referenced by the application and search for types matching a predicate
            IEnumerable<Type> typesSoFar = Type.EmptyTypes;

            ICollection assemblies = buildManager.GetReferencedAssemblies();
            foreach (Assembly assembly in assemblies) {
                Type[] typesInAsm;
                try {
                    typesInAsm = assembly.GetTypes();
                }
                catch (ReflectionTypeLoadException ex) {
                    typesInAsm = ex.Types;
                }
                typesSoFar = typesSoFar.Concat(typesInAsm);
            }
            return typesSoFar.Where(type => TypeIsPublicClass(type) && predicate(type));
        }

        public static List<Type> GetFilteredTypesFromAssemblies(string cacheName, Predicate<Type> predicate, IBuildManager buildManager) {
            TypeCacheSerializer serializer = new TypeCacheSerializer();

            // first, try reading from the cache on disk
            List<Type> matchingTypes = ReadTypesFromCache(cacheName, predicate, buildManager, serializer);
            if (matchingTypes != null) {
                return matchingTypes;
            }

            // if reading from the cache failed, enumerate over every assembly looking for a matching type
            matchingTypes = FilterTypesInAssemblies(buildManager, predicate).ToList();

            // finally, save the cache back to disk
            SaveTypesToCache(cacheName, matchingTypes, buildManager, serializer);

            return matchingTypes;
        }

        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Cache failures are not fatal, and the code should continue executing normally.")]
        internal static List<Type> ReadTypesFromCache(string cacheName, Predicate<Type> predicate, IBuildManager buildManager, TypeCacheSerializer serializer) {
            try {
                Stream stream = buildManager.ReadCachedFile(cacheName);
                if (stream != null) {
                    using (StreamReader reader = new StreamReader(stream)) {
                        List<Type> deserializedTypes = serializer.DeserializeTypes(reader);
                        if (deserializedTypes != null && deserializedTypes.All(type => TypeIsPublicClass(type) && predicate(type))) {
                            // If all read types still match the predicate, success!
                            return deserializedTypes;
                        }
                    }
                }
            }
            catch {
            }

            return null;
        }

        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Cache failures are not fatal, and the code should continue executing normally.")]
        internal static void SaveTypesToCache(string cacheName, IList<Type> matchingTypes, IBuildManager buildManager, TypeCacheSerializer serializer) {
            try {
                Stream stream = buildManager.CreateCachedFile(cacheName);
                if (stream != null) {
                    using (StreamWriter writer = new StreamWriter(stream)) {
                        serializer.SerializeTypes(matchingTypes, writer);
                    }
                }
            }
            catch {
            }
        }

        private static bool TypeIsPublicClass(Type type) {
            return (type != null && type.IsPublic && type.IsClass && !type.IsAbstract);
        }

    }
}
TypeCacheUtil

  不仅控制器的类型是这样加载的,Area等好多类型都是这样加载的,mvc内部大量使用静态变量缓存数据。

  既然这样,我也想采用类似的机制,在所有程序集中搜索继承IFilter的类型。

Attribute的应用

  由于我最后设想的是,将每次请求的行为,映射到一个或多个日志处理上。所以最后在方法上定义了属性进行识别。看看FilterAttribute的定义:

    [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
    public class FilterMethodAttribute : Attribute
    {
        public FilterMethodAttribute(string controllerName, params string[] actionName)
        {
            this.controllerName = controllerName;
            this.actionName = actionName;
        }
        //需要过滤的控制器名称
        public string controllerName { get; set; }
        public string[] actionName { get; set; }
    }

  这样就可以监控一个Controller上的多个Action,由于设置了该属性可以重复,故可以监控多个控制器上的多个Action。

日志处理写法

  除了方法必须包含一个FilterContext参数,并且在方法上指定FilterMethod属性,此外没有任何约束。看看典型的实例:

    public class Log1 : IFilter
    {
        [FilterMethod("Home", "Index")]
        public void Log_Login(FilterContext context)
        {
            var path = HttpContext.Current.Server.MapPath("~/log1.log");
            var content = string.Format("登陆ID:{0} 登陆人:{1}\r\n", context.IP, context.UserName);
            File.AppendAllText(path, content);
        }
    }

回顾

  现在,整个流程则是,程序在初始化时,会在所有的程序集中搜索继承IFilter的类型,然后在该类型中找到定义了FilterMethod属性的方法,判断它要监控哪些控制器,之后将该方法生成一个强类型的委托,缓存到Dictionary中,便于工厂查找。

  最初是为了实现日志应用,后来发现越做越不像日志处理,就类似一个通用的模块,可以得到Http的输入流和输出流,其中若需要更多,可根据情况修改代码。而拿到这些数据,我们可以做很多应用,而日志处理只是一种应用。

其他

  1.若日志等应用处理,无需更改输出内容,则可以将处理放入线程池中执行。

  2.若需要做安全检测、权限验证,则可以直接使用MVC提供的Filter,虽然不能读取输出流,但是可以改写和追加。

  3.Module需要被加载,必须在WebConfig中 system.webServer 节点下进行配置,若有多个HttpModule,加载顺序则是根据配置的顺序来加载的。

  4.源码中AssemblyType文件夹存放的是MVC源代码中的类型搜索相关类,可以直接拿出来使用。HttpFilter文件夹存放的是实现该模块的相关类。而LogFilter就是日志应用了。

测试代码下载

发表评论
用户名: 匿名