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. tensorflow GPU的使用

    参考:https://blog.csdn.net/mzpmzk/article/details/78647711 import os # 默认情况,TF 会占用所有 GPU 的所有内存, 我们可以指定 ...

  2. js之数组乱序

    这是最近面试遇到的,不过忘记了,之前也有印象刷到过这道题,就再次记录一下加深印象吧,听到最多的答案是利用sort方法,不过也有说这种方法不好,利用了快排和插入排序,那就整理下吧 <!DOCTYP ...

  3. Java程序员普遍存在的面试问题以及应对之道(新书第一章节摘录)

    其实大多数Java开发确实能胜任日常的开发工作,但不少候选人却无法在面试中打动面试官.因为要在短时间的面试中全面展示自己的实力,这很需要技巧,而从当前大多数Java开发的面试现状来看,会面试的候选人不 ...

  4. 全国11省市出台区块链专项政策,Panda Global发现 "区块链+政务"被寄予厚望!

    2020年已经过半,回顾2020年的上半年,不难发现其实区块链的变化非常大,今天Panda Global就给大家回顾下上半年全国关于区块链政策的发布情况.今年上半年,全国已有11个省市出台区块链专项政 ...

  5. 你必须要知道的HTTP协议原理

    1 基本概念 HTTP协议:基于TCP协议之上实现的无状态.全文本的标准通信协议. 客户端:例如pc浏览器,移动应用端,第三方服务器等能发起http访问的设备. 服务器:能够接受HTTP协议请求,并且 ...

  6. Day11 python高级特性-- 迭代器 Iterator

    直接可以作用于for循环的数据类型有以下几种:   •  集合数据类型:        list.tuple.dict.set.str   •  Generator:        生成器 和 带 y ...

  7. jwt 简单基本使用加密解密

    import jwt # 加密 encode_jwt=jwt.encode({'uid':'123'},'密钥123',algorithm='HS256') print(encode_jwt) # 解 ...

  8. 四、testNG.xml 简单介绍

    TestNG定义了一套非常具体的术语描述测试. testng.xml testng.xml是一个以XML记录所有测试的文件.可以利用这个文件,跑同一个类或者多个不同类里面的测试用例. testng.x ...

  9. Android studio使用OKGO的POST请求访问http失败的解决方法

    前几天在旧手机(版本是Android7)上调试一个app,用OKGO的post请求连接服务器登录一直很正常.今天旧手机不在身边,用自己的手机调试,就出现网络请求失败的问题,弹onError()里自己写 ...

  10. ElasticSearch中head插件的简单用法

    1.首先在左侧打开Query栏. 2.Query下方的第一栏是分别输入es的地址.端口号.index.type. 3.Query下方的第二栏是输入将要执行的方式,旁边的下拉框是辅助选择执行的类型,如图 ...