由做网站操作日志想到的HttpModule应用
背景
在以前的Web项目中,记录用户操作日志,总是在方法里,加一行代码,记录此时用户操作类型与相关信息。该记录日志的方法对原来的业务操作侵入性较强,也比较零散,不便于查看和管理。那么有没有更加通用点的方法呢。
同事建议我,写个HttpModule,能够得到请求的Http报文,同时获取到输出的Http报文,这样大致上能够分析出请求的行为了。最初对HttpModule很陌生,一直也没机会用到,故开始对这个方法,有点抵触。后来利用了强大的搜索引擎,发现确实有人向这方面做出了努力。如《初涉电子商务系统开发随想--第二篇-基于asp.net Mvc的通用日志管理方法》。我也来写的试试吧。
HttpModule
首先就必须先了解HttpModule,了解它的事件发生顺序,在什么时候又可以访问到Session,如何获取到Http输入流和输出流等。下面是参考的内容:
ASP.NET中httpmodules与httphandlers全解析
最后我选择了 AcquireRequestState 这个事件,因为在其中可以访问到Session,以备不时之需。Module代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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在初始化时,会加载所有的程序集,遍历程序集的所有类型,然后在做筛选,筛选后并将类型缓存起来。摘抄类如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
不仅控制器的类型是这样加载的,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} ", 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就是日志应用了。