查看原文
其他

动态方法拦截(AOP)的N种解决方案

DotNet 2021-09-23

(给DotNet加星标,提升.Net技能

转自:Artech
cnblogs.com/artech/archive

前言


AOP的本质是方法拦截(将针对目标方法调用劫持下来,进而执行执行的操作),置于方法拦截的实现方案,不外乎两种代码注入类型,即编译时的静态注入和运行时的动态注入,本篇文章列出了几种常用的动态注入方案。


这篇文章的目标并不是提供完整的AOP框架的解决方案,而是说明各种解决方案后面的原理,所以我们提供的实例代码会尽可能简单。


为了确定拦截操作是否执行,我们定义了如下这个Indicator类型,我们的拦截操作会将其静态属性Injected属性设置为True,我们演示的代码最终通过这个属性来确定拦截是否成功。源代码下载:https://files.cnblogs.com/files/artech/Interception.7z


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);
}
}


- EOF -


推荐阅读  点击标题可跳转

当.NET 5遇上OpenTelemetry,会碰撞出怎样的火花?

深入研究 .NET 5 的开放式遥测

C# 9.0:Records


看完本文有收获?请转发分享给更多人

推荐关注「DotNet」,提升.Net技能 

点赞和在看就是最大的支持❤️

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存