背景

  在以前的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就是日志应用了。

测试代码下载

由做网站操作日志想到的HttpModule应用的更多相关文章

  1. Struts2拦截器记录系统操作日志

    前言 最近开发了一个项目,由于项目在整个开发过程中处于赶时间状态(每个项目都差不多如此)所以项目在收尾阶段发现缺少记录系统日志功能,以前系统都是直接写在每个模块的代码中,然后存入表单,在页面可以查看部 ...

  2. springAOP实现操作日志记录,并记录请求参数与编辑前后字段的具体改变

    本文为博主原创,未经允许不得转载: 在项目开发已经完成多半的情况下,需要开发进行操作日志功能的开发,由于操作的重要性,需要记录下操作前的参数和请求时的参数, 在网上找了很多,没找到可行的方法.由于操作 ...

  3. .NetCore中使用ExceptionLess 添加操作日志

    上一篇文章已经扩展了日志,下面我们在结合下处理操作日志 通常我们想到操作日志 可能想到的参数可能有 模块 方法 参数内容 操作人 操作时间 操作 Ip 下面我们就来结合这些信息添加操作日志 如果要在代 ...

  4. 我使用Spring AOP实现了用户操作日志功能

    我使用Spring AOP实现了用户操作日志功能 今天答辩完了,复盘了一下系统,发现还是有一些东西值得拿出来和大家分享一下. 需求分析 系统需要对用户的操作进行记录,方便未来溯源 首先想到的就是在每个 ...

  5. Appfuse:记录操作日志

    appfuse的数据维护操作都发生在***form页面,与之对应的是***FormController,在Controller中处理数据的操作是onSubmit方法,既然所有的操作都通过onSubmi ...

  6. C#操作日志

    首先引用NLog的dll文件 using System.IO; using NLog; -------------------------------------------------------- ...

  7. 【开源】OSharp3.0框架解说系列(6.2):操作日志与数据日志

    OSharp是什么? OSharp是个快速开发框架,但不是一个大而全的包罗万象的框架,严格的说,OSharp中什么都没有实现.与其他大而全的框架最大的不同点,就是OSharp只做抽象封装,不做实现.依 ...

  8. SSH基于Hibernate eventListener 事件侦听器的操作日志自动保存到数据库

    在spring xml配置文件中添加配置,包含:model.listener 在model中增加需要写入数据库对应表的model 在auditLog.xml配置文件中配置自己项目中,需要进行日志记录的 ...

  9. springmvc+log4j操作日志记录,详细配置

    没有接触过的,先了解一下:log4j教程 部分内容来:log4j教程 感谢! 需要导入包: log包:log4j-12.17.jar 第一步:web.xml配置 <!-- log4j配置,文件路 ...

随机推荐

  1. MOS管学习笔记

    最近在做一个小的电路设计项目,其中遇到了MOS管,经过查询资料,多年遗忘的数电.模电渐渐又浮现在我的脑海,在百度文库找到一篇比较不错的文章,把它截图使用出来,如原稿作者看到感觉侵权,请及时联系我,以便 ...

  2. Android 6.0 动态申请 音频+拍照+相册 权限

    1.音频的权限(包括录音和播放) 1.1.首先要在清单中加上两个权限 <uses-permission android:name="android.permission.WRITE_E ...

  3. PHP.26-TP框架商城应用实例-后台3-商品修改、删除

    商品修改{修改页一般与添加页有百分之九十的相似度} create($_POST,Model::MODEL_UPDATE):系统内置的数据操作包括Model::MODEL_INSERT(或者1)和Mod ...

  4. P2985 [USACO10FEB]吃巧克力Chocolate Eating

    P2985 [USACO10FEB]吃巧克力Chocolate Eating 题目描述 Bessie has received N (1 <= N <= 50,000) chocolate ...

  5. gradle编译很慢解决方法

    1.升级内存,内存最好在8g以上. 我的12g,编译运行,2s22ms,不到3s. 2.设置Android staido 不要 打开instant run

  6. 剑指Offer - 九度1369 - 字符串的排列

    剑指Offer - 九度1369 - 字符串的排列2014-02-05 21:12 题目描述: 输入一个字符串,按字典序打印出该字符串中字符的所有排列.例如输入字符串abc,则打印出由字符a,b,c所 ...

  7. oracle 隔离级别、事务怎么开始的以及如何查看数据库采用字符集

    把一下语句全部粘贴至控制台运行后可以查看oracle 隔离级别 declare trans_id ); begin trans_id := dbms_transaction.local_transac ...

  8. Day1 Toast/Menu/Intent传递数据

    ** --------------->未经允许,禁止转载<----------------** 今天是我读<第二行代码>的第一天,也是我第一次开始写CSDN博客,之前的笔记都在 ...

  9. vue实现数据的增删改查

    在管理员的一些后台页面里,个人中心里的数据列表里,都会有对这些数据进行增删改查的操作.比如在管理员后台的用户列表里,我们可以录入新用户的信息,也可以对既有的用户信息进行修改.在vue中,我们更应该专注 ...

  10. sharePreference的几个重点

    一.  SharePreferences是用来存储一些简单配置信息的一种机制,使用Map数据结构来存储数据,以键值对的方式存储,采用了XML格式将数据存储到设备中,文件存放在/data/data/&l ...