AOP的本质是方法拦截(将针对目标方法调用劫持下来,进而执行执行的操作),置于方法拦截的实现方案,不外乎两种代码注入类型,即编译时的静态注入和运行时的动态注入,本篇文章列出了几种常用的动态注入方案。这篇文章的目标并不是提供完整的AOP框架的解决方案,而是说明各种解决方案后面的原理,所以我们提供的实例代码会尽可能简单。为了确定拦截操作是否执行,我们定义了如下这个Indicator类型,我们的拦截操作会将其静态属性Injected属性设置为True,我们演示的代码最终通过这个属性来确定拦截是否成功。[源代码从这里下载]

public static class Indicator
{
public static bool Injected { get; set; }
}

一、IL Emit(接口)

IL Emit是实现AOP的首选方案。如果方法调用时针对接口完成,我们可以生成一个代理类型来封装对象,并且这个代理类型同时实现目标接口,那么只要我们能够将针对目标对象的方法调用转换成针对代理对象的调用,就能实现针对目标对象的方法拦截。举个简单的例子,Foobar实现了IFoobar接口,如果我们需要拦截接口方法Invoke,我们可以生成一个FoobarProxy类型。如代码片段所示,FoobarProxy封装了一个IFoobar对象,并实现了IFoobar接口。在实现的Invoke方法中,它在调用封装对象的同名方法之前率先执行了拦截操作。

public interface IFoobar
{
int Invoke();
} public class Foobar : IFoobar
{
public int Invoke() => 1;
} public class FoobarProxy : IFoobar
{
private readonly IFoobar _target;
public FoobarProxy(IFoobar target)=>_target = target
    public int Invoke()
{
Indicator.Injected = true;
return _target.Invoke();
}
}

上述的这个FoobarProxy类型就可以按照如下的方式利用GenerateProxyClass方法来生成。在Main方法中,我们创建一个Foobar对象,让据此创建这个动态生成的FoobarProxy,当该对象的Invoke方法执行的时候,我们期望的拦截操作自然会自动执行。

class Program
{
static void Main(string[] args)
{
var foobar = new Foobar();
var proxy = (IFoobar)Activator.CreateInstance(GenerateProxyClass(), foobar); Debug.Assert(Indicator.Injected == false);
Debug.Assert(proxy.Invoke() == 1);
Debug.Assert(Indicator.Injected == true);
} static Type GenerateProxyClass()
{
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("Proxy"), AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("Proxy.dll");
var typeBuilder = moduleBuilder.DefineType("FoobarProxy", TypeAttributes.Public, null, new Type[] { typeof(IFoobar) });
var targetField = typeBuilder.DefineField("_target", typeof(IFoobar), FieldAttributes.Private | FieldAttributes.InitOnly); var constructor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof(IFoobar) });
var il = constructor.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Stfld, targetField);
il.Emit(OpCodes.Ret); var attributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual | MethodAttributes.Final;
var invokeMethod = typeBuilder.DefineMethod("Invoke", attributes, typeof(int), null);
il = invokeMethod.GetILGenerator();
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Call, typeof(Indicator).GetProperty("Injected").SetMethod);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, targetField);
il.Emit(OpCodes.Callvirt, typeof(IFoobar).GetMethod("Invoke"));
il.Emit(OpCodes.Ret); return typeBuilder.CreateType();
}
}

二、IL Emit(虚方法)

如果待拦截的并非接口方法,而是一个虚方法,我们可以利用IL Emit的方式动态生成一个派生类,并重写这个虚方法的方式来完成拦截。以下面的代码片段为例,我们需要拦截定义在Foobar中的虚方法Invoke,我们可以生成如下这个派生与Foobar的Foobar的FoobarProxy类型,在重写的Invoke方法中,我们在调用基类同名方法之前,率先执行拦截操作。

public class Foobar
{
public virtual int Invoke() => 1;
}
public class FoobarProxy : Foobar { public override int Invoke()
{
Indicator.Injected = true;
return base.Invoke();
}
}

上面这个FoobarProxy类型就可以通过如下这个GenerateProxyClass生成出来。

class Program
{
static void Main(string[] args)
{
var proxy = (Foobar)Activator.CreateInstance(GenerateProxyClass()); Debug.Assert(Indicator.Injected == false);
Debug.Assert(proxy.Invoke() == 1);
Debug.Assert(Indicator.Injected == true);
} static Type GenerateProxyClass()
{
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("Proxy"), AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("Proxy.dll");
var typeBuilder = moduleBuilder.DefineType("FoobarProxy", TypeAttributes.Public, typeof(Foobar)); var attributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual | MethodAttributes.Final;
var invokeMethod = typeBuilder.DefineMethod("Invoke", attributes, typeof(int), null);
var il = invokeMethod.GetILGenerator();
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Call, typeof(Indicator).GetProperty("Injected").SetMethod);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, typeof(Foobar).GetMethod("Invoke"));
il.Emit(OpCodes.Ret); return typeBuilder.CreateType();
}
}

三、方法替换(跳转)

上面两种方案都具有一个局限性:需要将针对目标对象的方法调用转换成针对代理对象的调用。如果我们能够直接将目标方法替换成另一个包含拦截操作的方案(或者说从原来的方法调转到具有拦截操作的方法),那么即使我们不改变方法的调用方式,方法依旧能够拦截。Harmony框架就是采用这样的方案实现的,我们可以通过下面这个简单的实例来模拟其实现原理(下面演示的程序引用了HarmonyLib包)。

class Program
{
static void Main(string[] args)
{
HarmonyLib.Memory.DetourMethod(typeof(Foobar).GetMethod("Invoke"), GenerateNewMethod());
Debug.Assert(Indicator.Injected == false);
Debug.Assert(new Foobar().Invoke() == 1);
Debug.Assert(Indicator.Injected == true);
} static MethodBase GenerateNewMethod()
{
var dynamicMethod = new DynamicMethodDefinition(typeof(Foobar).GetMethod("Invoke"));
var il = dynamicMethod.GetILProcessor();
var ldTrue = il.Create(OpCodes.Ldc_I4_1);
var setIndicator = il.Create(OpCodes.Call, dynamicMethod.Module.ImportReference(typeof(Indicator).GetProperty("Injected").SetMethod));
il.InsertBefore(dynamicMethod.Definition.Body.Instructions.First(), setIndicator);
il.InsertBefore(setIndicator, ldTrue);
return dynamicMethod.Generate();
}
}
public class Foobar
{
public virtual int Invoke() => 1;
}

如上面的代码片段所示,为了拦截Foobar的Invoke方法,我们在GenerateNewMethod方法中根据这个方法创建了一个DynamicMethodDefinition对象(定义在MonoMod.Common包中),并在方法体的前面添加了两个IL指令将Indicator的Injected属性设置为True,该方法最终返回通过这个DynamicMethodDefinition对象生成的MethodBase对象。在Main方法中,我们利用HarmonyLib.Memory的静态方法DetourMethod将原始的Invoke方法“转移”到生成的方法上。即使我们调用的依然是Foobar对象的Invoke方法,但是拦截操作依然会被执行。

四、RealProxy/TransparentProxy

RealProxy/TransparentProxy是.NET Framework时代一种常用的方法拦截方案。如果目标类型实现了某个接口或者派生于MarshalByRefObject类型,我们就可以采用这种拦截方案。如果需要拦截某个类型的方法,我们可以定义如下这么一个FoobarProxy<T>类型,泛型参数T代表目标类型或者接口。和第一种方案一样,我们的代理对象依旧是封装目标对象,在实现的Invoke方案中,我们利用作为参数的IMessage 方法得到代表目标方法的MethodBase对象,进而利用它实现针对目标方法的调用。在目标方法调用之前,我们可以执行拦截操作。

public interface IFoobar
{
int Invoke();
} public class Foobar : IFoobar
{
public int Invoke() => 1;
} public class FoobarProxy<T> : RealProxy
{
public T _target;
public FoobarProxy(T target):base(typeof(T))
=> _target = target;
public override IMessage Invoke(IMessage msg)
{
Indicator.Injected = true;
IMethodCallMessage methodCall = (IMethodCallMessage)msg;
IMethodReturnMessage methodReturn = null;
object[] copiedArgs = Array.CreateInstance(typeof(object), methodCall.Args.Length) as object[];
methodCall.Args.CopyTo(copiedArgs, 0);
try
{
object returnValue = methodCall.MethodBase.Invoke(_target, copiedArgs);
methodReturn = new ReturnMessage(returnValue, copiedArgs, copiedArgs.Length, methodCall.LogicalCallContext, methodCall);
}
catch (Exception ex)
{
methodReturn = new ReturnMessage(ex, methodCall);
}
return methodReturn;
}
}

在Main方法中,我们创建目标Foobar对象,然后将其封装成一个FoobarProxy<IFoobar>对象。我们最终调用GetTransparentProxy方法创建出透明代理,并将其转换成IFoobar类型。当我们调用这个透明对象的任何一个方法的时候,定义在FoobarProxy<T>中的Invoke方法均会执行。

class Program
{
static void Main(string[] args)
{
var proxy = (IFoobar)(new FoobarProxy<IFoobar>(new Foobar()).GetTransparentProxy());
Debug.Assert(Indicator.Injected == false);
Debug.Assert(proxy.Invoke() == 1);
Debug.Assert(Indicator.Injected == true);
}
}

五、DispatchProxy

RealProxy/TransparentProxy仅限于.NET Framework项目中实现,在.NET Core中它具有一个替代类型,那就是DispatchProxy。我们可以采用如下的方式利用DispatchProxy实现我们所需的拦截功能。

class Program
{
static void Main(string[] args)
{
var proxy = DispatchProxy.Create<IFoobar, FoobarProxy<IFoobar>>();
((FoobarProxy<IFoobar>)proxy).Target = new Foobar(); Debug.Assert(Indicator.Injected == false);
Debug.Assert(proxy.Invoke() == 1);
Debug.Assert(Indicator.Injected == true);
}
} public interface IFoobar
{
int Invoke();
} public class Foobar : IFoobar
{
public int Invoke() => 1;
} public class FoobarProxy<T> : DispatchProxy
{
public T Target { get; set; } protected override object Invoke(MethodInfo targetMethod, object[] args)
{
Indicator.Injected = true;
return targetMethod.Invoke(Target, args);
}
}

动态方法拦截(AOP)的N种解决方案的更多相关文章

  1. 01_5_Struts_ActionMethod_DMI_动态方法调用

    01_5_Struts_ActionMethod_DMI_动态方法调用 1. ActionMethod_DMI_动态方法调用 Action执行的时候并不一定要执行execute()方法 可以在配置文件 ...

  2. 动态代理的两种方式,以及区别(静态代理、JDK与CGLIB动态代理、AOP+IoC)

    Spring学习总结(二)——静态代理.JDK与CGLIB动态代理.AOP+IoC   目录 一.为什么需要代理模式 二.静态代理 三.动态代理,使用JDK内置的Proxy实现 四.动态代理,使用cg ...

  3. c# AOP编程:Context与方法拦截

    之前做AgentBooking时候,遇到两个问题比较棘手,一个是异常的传递与捕获:如何可以合理地在层层代码调用中统一传递并统一捕获异常.因为如果有一个做法,可以地方统一处理异常,可以使代码减少很多tr ...

  4. AOP的几种实现方法

    C# 实现AOP 的几种常见方式 原文出处:http://www.cnblogs.com/zuowj/p/7501896.html AOP为Aspect Oriented Programming的缩写 ...

  5. JAVA高级架构师基础功:Spring中AOP的两种代理方式:动态代理和CGLIB详解

    在spring框架中使用了两种代理方式: 1.JDK自带的动态代理. 2.Spring框架自己提供的CGLIB的方式. 这两种也是Spring框架核心AOP的基础. 在详细讲解上述提到的动态代理和CG ...

  6. js 动态加载事件的几种方法总结

    本篇文章主要是对js 动态加载事件的几种方法进行了详细的总结介绍,需要的朋友可以过来参考下,希望对大家有所帮助   有些时候需要动态加载javascript事件的一些方法往往我们需要在 JS 中动态添 ...

  7. Spring中AOP的两种代理方式(Java动态代理和CGLIB代理)

    第一种代理即Java的动态代理方式上一篇已经分析,在这里不再介绍,现在我们先来了解下GCLIB代理是什么?它又是怎样实现的?和Java动态代理有什么区别? cglib(Code Generation ...

  8. bugfree如何修改Bug7种解决方案的标注方法

    Bug有7种解决方案的标注方法 By Design- 就是这么设计的,无效的Bug Duplicate - 这个问题别人已经发现了,重复的Bug External - 是个外部因素(比如浏览器.操作系 ...

  9. WPF编程,通过Double Animation动态旋转控件的一种方法。

    原文:WPF编程,通过Double Animation动态旋转控件的一种方法. 版权声明:我不生产代码,我只是代码的搬运工. https://blog.csdn.net/qq_43307934/art ...

随机推荐

  1. 阿里云服务器搭建Docker版AWVS

    本文严重参考该文章:https://www.sqlsec.com/2020/04/awvs.html 阿里云服务器搭建Docker版AWVS,因为之前有使用Docker的经验,所以本文只是简述一下安装 ...

  2. 转:解释lsh

    Locality sensitive hashing - LSH explained The problem of finding duplicate documents in a list may ...

  3. 题解-CF1389F Bicolored Segments

    题面 CF1389F Bicolored Segments 给 \(n\) 条线段 \([l_i,r_i]\),每条有个颜色 \(t_i\in\{0,1\}\),求最多选出多少条线段,使没有不同颜色的 ...

  4. rsync 参数说明及使用参数笔记好文摘抄

    一.前言 最近发现rsync挺好用的--不过参数有点多,所以这儿写一篇给自己以后要用的时候做个参考. 二.参数说明 这儿全是我翻资料连蒙带猜(有些实在是不好解释)翻译出来的,请各位转载的留个名啊,虽然 ...

  5. STL——容器(deque)deque 的插入 insert()

    deque.insert(pos,elem); //在pos位置插入一个elem元素的拷贝,返回新数据的位置. 1 #include <iostream> 2 #include <d ...

  6. [水题日常]UVA11181 条件概率(Probability|Given)

    话说好久没写blog了 好好学概率论的第一天,这题一开始完全不会写,列出个条件概率的公式就傻了,后来看着lrj老师的书附带的代码学着写的- 因为我比较弱智 一些比较简单的东西也顺便写具体点或者是按照书 ...

  7. Spark-1-调优基本原则

    1基本概念和原则 每一台host上面可以并行N个worker,每一个worker下面可以并行M个executor,task们会被分配到executor上面去执行.Stage指的是一组并行运行的task ...

  8. maven继承父工程统一版本号

    一.建立一个maven工程 pom类型 统一管理依赖以及版本号 子工程不会使用所有的定义的依赖 子工程使用依赖时无需指定版本号 pom.xml <project xmlns="http ...

  9. 阿里云Centos7.6中部署nginx1.16+uwsgi2.0.18+Django2.0.4

    当你购买了阿里云的ecs,涉及ecs的有两个密码,一定要搞清楚,一个密码是远程链接密码,也就是通过浏览器连接服务器的密码,另外一个是实例密码,这个密码就是ecs的root密码,一般情况下,我们经常用到 ...

  10. VC++安装window8.1系统

    下载VC++软件 解压安装(这个过程一般不会出现问题) 安装完成后,运行VC++会出现不兼容信息,照着一下方法就可以解决了. 将MSDEV.EXE重命名为MSDEV1.EXE.(路径:Common/M ...