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