由做網站操作日志想到的HttpModule應用


背景

  在以前的Web項目中,記錄用戶操作日志,總是在方法里,加一行代碼,記錄此時用戶操作類型與相關信息。該記錄日志的方法對原來的業務操作侵入性較強,也比較零散,不便於查看和管理。那么有沒有更加通用點的方法呢。

  同事建議我,寫個HttpModule,能夠得到請求的Http報文,同時獲取到輸出的Http報文,這樣大致上能夠分析出請求的行為了。最初對HttpModule很陌生,一直也沒機會用到,故開始對這個方法,有點抵觸。后來利用了強大的搜索引擎,發現確實有人向這方面做出了努力。如《初涉電子商務系統開發隨想--第二篇-基於asp.net Mvc的通用日志管理方法》。我也來寫的試試吧。

HttpModule

  首先就必須先了解HttpModule,了解它的事件發生順序,在什么時候又可以訪問到Session,如何獲取到Http輸入流和輸出流等。下面是參考的內容:

  選擇HttpHandler還是HttpModule?

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

測試代碼下載


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM