背景
在以前的Web項目中,記錄用戶操作日志,總是在方法里,加一行代碼,記錄此時用戶操作類型與相關信息。該記錄日志的方法對原來的業務操作侵入性較強,也比較零散,不便於查看和管理。那么有沒有更加通用點的方法呢。
同事建議我,寫個HttpModule,能夠得到請求的Http報文,同時獲取到輸出的Http報文,這樣大致上能夠分析出請求的行為了。最初對HttpModule很陌生,一直也沒機會用到,故開始對這個方法,有點抵觸。后來利用了強大的搜索引擎,發現確實有人向這方面做出了努力。如《初涉電子商務系統開發隨想--第二篇-基於asp.net Mvc的通用日志管理方法》。我也來寫的試試吧。
HttpModule
首先就必須先了解HttpModule,了解它的事件發生順序,在什么時候又可以訪問到Session,如何獲取到Http輸入流和輸出流等。下面是參考的內容:
ASP.NET中httpmodules與httphandlers全解析
最后我選擇了 AcquireRequestState 這個事件,因為在其中可以訪問到Session,以備不時之需。Module代碼如下:

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在初始化時,會加載所有的程序集,遍歷程序集的所有類型,然后在做篩選,篩選后並將類型緩存起來。摘抄類如下:

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就是日志應用了。