关于反射:如何在C#中拦截方法调用?

How do I intercept a method call in C#?

对于给定的类,我希望具有跟踪功能,即,我希望记录每个方法调用(方法签名和实际参数值)和每个方法出口(仅方法签名)。

我如何做到这一点,假设:

  • 我不想使用任何第三方用于c_的AOP库,
  • 我不想向所有我想跟踪的方法添加重复的代码,
  • 我不想更改类的公共API——类的用户应该能够以完全相同的方式调用所有方法。

为了使问题更具体,我们假设有三类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 public class Caller
 {
     public static void Call()
     {
         Traced traced = new Traced();
         traced.Method1();
         traced.Method2();
     }
 }

 public class Traced
 {
     public void Method1(String name, Int32 value) { }

     public void Method2(Object object) { }
 }

 public class Logger
 {
     public static void LogStart(MethodInfo method, Object[] parameterValues);

     public static void LogEnd(MethodInfo method);
 }

如何在不修改caller.call方法和不将调用显式添加到tracked.method1和tracked.method2的情况下,为每次调用method1和method2调用logger.logstart和logger.logend?

编辑:如果允许我稍微更改调用方法,解决方案是什么?


C不是面向AOP的语言。它有一些AOP特性,您可以模仿其他一些特性,但是用C_创建AOP是很痛苦的。

我想办法做你想做的事,但我却找不到一个简单的办法。

据我所知,这就是你想要做的:

1
2
[Log()]
public void Method1(String name, Int32 value);

为了做到这一点,你有两个主要的选择

  • 从MarshalByRefObject或ContextBoundObject继承类,并定义从IMessageSink继承的属性。本文有一个很好的例子。你必须考虑到使用MarshalByRefObject时性能会下降到地狱般的地步,我的意思是,我说的是性能会下降10倍,所以在尝试之前要仔细考虑。

  • 另一种选择是直接注入代码。在运行时,这意味着您必须使用反射来"读取"每个类,获取其属性并注入适当的调用(为此,我认为您不能使用反射.emit方法,正如我认为的反射.emit不允许您在已经存在的方法中插入新代码)。在设计时,这意味着要为clr编译器创建一个扩展,而我真的不知道它是如何完成的。

  • 最后一个选择是使用国际奥委会框架。也许这并不是一个完美的解决方案,因为大多数国际奥委会框架都是通过定义入口点来工作的,这些入口点允许方法被钩住,但这取决于你想要实现什么,这可能是一个公平的限制。


    实现这一点的最简单方法可能是使用Postharp。它根据应用于方法的属性在方法内部注入代码。它允许你做你想做的事情。

    另一种选择是使用分析API在方法内部注入代码,但这确实是硬内核。


    如果编写实现IDisposable接口的类(称为跟踪),则可以将所有方法体包装在

    1
    Using( Tracing tracing = new Tracing() ){ ... method body ...}

    在跟踪类中,可以在跟踪类中分别处理constructor/dispose方法中跟踪的逻辑,以跟踪方法的进入和退出。这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
        public class Traced
        {
            public void Method1(String name, Int32 value) {
                using(Tracing tracer = new Tracing())
                {
                    [... method body ...]
                }
            }

            public void Method2(Object object) {
                using(Tracing tracer = new Tracing())
                {
                    [... method body ...]
                }
            }
        }


    你可以通过一个DI容器的拦截功能来实现它,比如温莎城堡。实际上,可以以这样的方式配置容器,即截取具有由特定属性修饰的方法的每个类。

    关于点3,OP要求提供一个没有AOP框架的解决方案。我在下面的回答中假设应该避免的是方面、接合点、切入点等。根据Castlewindsor的拦截文档,这些都不需要完成所要求的。

    根据存在的属性配置拦截器的常规注册:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class RequireInterception : IContributeComponentModelConstruction
    {
        public void ProcessModel(IKernel kernel, ComponentModel model)
        {
            if (HasAMethodDecoratedByLoggingAttribute(model.Implementation))
            {
                model.Interceptors.Add(new InterceptorReference(typeof(ConsoleLoggingInterceptor)));
                model.Interceptors.Add(new InterceptorReference(typeof(NLogInterceptor)));
            }
        }

        private bool HasAMethodDecoratedByLoggingAttribute(Type implementation)
        {
            foreach (var memberInfo in implementation.GetMembers())
            {
                var attribute = memberInfo.GetCustomAttributes(typeof(LogAttribute)).FirstOrDefault() as LogAttribute;
                if (attribute != null)
                {
                    return true;
                }
            }

            return false;
        }
    }

    将创建的IContributeComponentModelConstruction添加到容器中

    1
    container.Kernel.ComponentModelBuilder.AddContributor(new RequireInterception());

    你可以在拦截器中做任何你想做的事情

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class ConsoleLoggingInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            Console.Writeline("Log before executing");
            invocation.Proceed();
            Console.Writeline("Log after executing");
        }
    }

    将logging属性添加到要记录的方法中

    1
    2
    3
    4
    5
    6
    7
    8
     public class Traced
     {
         [Log]
         public void Method1(String name, Int32 value) { }

         [Log]
         public void Method2(Object object) { }
     }

    注意,如果只需要截取类的某个方法,则需要对属性进行一些处理。默认情况下,将截取所有公共方法。


    如果你想无限制地跟踪你的方法(没有代码适应,没有AOP框架,没有重复的代码),让我告诉你,你需要一些魔法……

    说真的,我决定在运行时实现一个AOP框架。

    您可以在这里找到:ncocern.net AOP框架

    我决定创建这个AOP框架来响应这种需求。它是一个简单的库,非常轻。您可以在主页中看到记录器的示例。

    如果不想使用第三方程序集,可以浏览代码源(开放源代码),并复制两个文件aspect.directory.cs和aspect.directory.entry.cs以根据需要进行调整。这些类允许在运行时替换方法。我只要求你尊重执照。

    我希望你能找到你需要的,或者说服你最终使用AOP框架。


    首先,必须修改类以实现接口(而不是实现MarshalByRefObject)。

    1
    2
    3
    4
    5
    interface ITraced {
        void Method1();
        void Method2()
    }
    class Traced: ITraced { .... }

    接下来,您需要一个基于realproxy的通用包装器对象来修饰任何接口,以允许拦截对修饰对象的任何调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class MethodLogInterceptor: RealProxy
    {
         public MethodLogInterceptor(Type interfaceType, object decorated)
             : base(interfaceType)
         {
              _decorated = decorated;
         }

        public override IMessage Invoke(IMessage msg)
        {
            var methodCall = msg as IMethodCallMessage;
            var methodInfo = methodCall.MethodBase;
            Console.WriteLine("Precall" + methodInfo.Name);
            var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
            Console.WriteLine("Postcall" + methodInfo.Name);

            return new ReturnMessage(result, null, 0,
                methodCall.LogicalCallContext, methodCall);
        }
    }

    现在我们准备拦截对itraced的method1和method2的调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
     public class Caller
     {
         public static void Call()
         {
             ITraced traced = (ITraced)new MethodLogInterceptor(typeof(ITraced), new Traced()).GetTransparentProxy();
             traced.Method1();
             traced.Method2();
         }
     }


    我找到了另一种可能更容易的方法…

    声明方法InvokeMethod

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    [WebMethod]
        public object InvokeMethod(string methodName, Dictionary<string, object> methodArguments)
        {
            try
            {
                string lowerMethodName = '_' + methodName.ToLowerInvariant();
                List<object> tempParams = new List<object>();
                foreach (MethodInfo methodInfo in serviceMethods.Where(methodInfo => methodInfo.Name.ToLowerInvariant() == lowerMethodName))
                {
                    ParameterInfo[] parameters = methodInfo.GetParameters();
                    if (parameters.Length != methodArguments.Count()) continue;
                    else foreach (ParameterInfo parameter in parameters)
                        {
                            object argument = null;
                            if (methodArguments.TryGetValue(parameter.Name, out argument))
                            {
                                if (parameter.ParameterType.IsValueType)
                                {
                                    System.ComponentModel.TypeConverter tc = System.ComponentModel.TypeDescriptor.GetConverter(parameter.ParameterType);
                                    argument = tc.ConvertFrom(argument);

                                }
                                tempParams.Insert(parameter.Position, argument);

                            }
                            else goto ContinueLoop;
                        }

                    foreach (object attribute in methodInfo.GetCustomAttributes(true))
                    {
                        if (attribute is YourAttributeClass)
                        {
                            RequiresPermissionAttribute attrib = attribute as YourAttributeClass;
                            YourAttributeClass.YourMethod();//Mine throws an ex
                        }
                    }

                    return methodInfo.Invoke(this, tempParams.ToArray());
                ContinueLoop:
                    continue;
                }
                return null;
            }
            catch
            {
                throw;
            }
        }

    然后我这样定义我的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    [WebMethod]
        public void BroadcastMessage(string Message)
        {
            //MessageBus.GetInstance().SendAll("<span class='system'>Web Service Broadcast:" + Message +"</span>");
            //return;
            InvokeMethod("BroadcastMessage", new Dictionary<string, object>() { {"Message", Message} });
        }

        [RequiresPermission("editUser")]
        void _BroadcastMessage(string Message)
        {
            MessageBus.GetInstance().SendAll("<span class='system'>Web Service Broadcast:" + Message +"</span>");
            return;
        }

    现在我可以在运行时进行检查,而不需要依赖注入…

    站点中没有gotchas:)

    希望您同意这比AOP框架轻,或者从MarshalByRefObject派生,或者使用远程处理或代理类。


    看看这个-相当重的东西……http://msdn.microsoft.com/en-us/magazine/cc164165.aspx

    必要的.NET-DonBox有一章介绍了你需要什么叫做拦截。我在这里刮了一些(对字体颜色很抱歉-当时我有一个黑色主题…)http://madcoderspeak.blogspot.com/2005/09/essential-interception-using-contexts.html


    您可以在codeplex上使用开源框架cinject。您可以编写最少的代码来创建一个注入器,并让它用cinject快速拦截任何代码。另外,由于这是开源的,所以您也可以扩展它。

    或者,您可以按照本文中提到的使用IL拦截方法调用的步骤,并使用Reflection.Emit类创建自己的拦截器。


    AOP对于干净的代码实现是必须的,但是如果您想用C包围一个块,一般方法的使用相对容易一些。(使用intelli-sense和强类型代码)当然,它不能作为AOP的替代方案。

    虽然Postsharp没有什么问题(我对在生产中使用没有信心),但它是一个很好的东西。

    通用包装类,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public class Wrapper
    {
        public static Exception TryCatch(Action actionToWrap, Action<Exception> exceptionHandler = null)
        {
            Exception retval = null;
            try
            {
                actionToWrap();
            }
            catch (Exception exception)
            {
                retval = exception;
                if (exceptionHandler != null)
                {
                    exceptionHandler(retval);
                }
            }
            return retval;
        }

        public static Exception LogOnError(Action actionToWrap, string errorMessage ="", Action<Exception> afterExceptionHandled = null)
        {
            return Wrapper.TryCatch(actionToWrap, (e) =>
            {
                if (afterExceptionHandled != null)
                {
                    afterExceptionHandled(e);
                }
            });
        }
    }

    用法可以是这样的(当然是有智能感的)

    1
    2
    3
    4
    5
    var exception = Wrapper.LogOnError(() =>
    {
      MessageBox.Show("test");
      throw new Exception("test");
    },"Hata");


    你需要让艾尔德回答他是怎么做的:http://ayende.com/blog/archive/2009/11/19/can-you-hack-this-out.aspx


    您可以潜在地使用GOF装饰器模式,并"装饰"所有需要跟踪的类。

    这可能只适用于IOC容器(但正如前面指出的那样,如果要沿着IOC路径前进,您可能需要考虑方法拦截)。


    我不知道解决方案,但我的方法如下。

    用自定义属性修饰类(或其方法)。在程序的其他地方,让初始化函数反映所有类型,读取用属性修饰的方法,并向该方法中注入一些IL代码。实际上,用一个称为LogStart、实际方法然后称为LogEnd的存根来替换该方法可能更为实际。另外,我不知道您是否可以使用反射来更改方法,因此替换整个类型可能更为实际。


  • 编写自己的AOP库。
  • 使用反射在实例上生成日志代理(不确定是否可以在不更改现有代码的某些部分的情况下执行此操作)。
  • 重写程序集并注入日志代码(基本上与1相同)。
  • 在这个级别上托管clr并添加日志记录(我认为这是最难实现的解决方案,但不确定您是否在clr中拥有所需的挂钩)。

  • 在C 6发布'nameof'之前,最好使用slow stacktrace和linq表达式。

    例如,对于这种方法

    1
    2
    3
    4
    5
    6
        public void MyMethod(int age, string name)
        {
            log.DebugTrace(() => age, () => name);

            //do your stuff
        }

    这样的行可以在日志文件中生成

    1
    Method 'MyMethod' parameters age: 20 name: Mike

    实现方法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
        //TODO: replace with 'nameof' in C# 6
        public static void DebugTrace(this ILog log, params Expression<Func<object>>[] args)
        {
            #if DEBUG

            var method = (new StackTrace()).GetFrame(1).GetMethod();

            var parameters = new List<string>();

            foreach(var arg in args)
            {
                MemberExpression memberExpression = null;
                if (arg.Body is MemberExpression)
                    memberExpression = (MemberExpression)arg.Body;

                if (arg.Body is UnaryExpression && ((UnaryExpression)arg.Body).Operand is MemberExpression)
                    memberExpression = (MemberExpression)((UnaryExpression)arg.Body).Operand;

                parameters.Add(memberExpression == null ?"NA" : memberExpression.Member.Name +":" + arg.Compile().DynamicInvoke().ToString());
            }

            log.Debug(string.Format("Method '{0}' parameters {1}", method.Name, string.Join("", parameters)));

            #endif
        }