动态方法拦截(AOP)的N种解决方案(拦截器和动态代理)

createh52周前 (02-04)技术教程14

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代表目标类型或者接口。和第一种方案一样,我们的代理对象依旧是封装目标对象,在实现的Invoke方案中,我们利用作为参数的IMessage 方法得到代表目标方法的MethodBase对象,进而利用它实现针对目标方法的调用。在目标方法调用之前,我们可以执行拦截操作。

public?interface?IFoobar
{
????int?Invoke();
}

public?class?Foobar?:?IFoobar
{
????public?int?Invoke()?=>?1;
}

public?class?FoobarProxy?:?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对象。我们最终调用GetTransparentProxy方法创建出透明代理,并将其转换成IFoobar类型。当我们调用这个透明对象的任何一个方法的时候,定义在FoobarProxy中的Invoke方法均会执行。

class?Program
{
????static?void?Main(string[]?args)
????{
????????var?proxy?=?(IFoobar)(new?FoobarProxy(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>();
????????((FoobarProxy)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?:?DispatchProxy
{
????public?T?Target?{?get;?set;?}

????protected?override?object?Invoke(MethodInfo?targetMethod,?object[]?args)
????{
????????Indicator.Injected?=?true;
????????return?targetMethod.Invoke(Target,?args);
????}
}

相关文章

JAVA和.NET谁更优秀?你选择谁?(java和.net哪个简单)

Java和.NET都是现代流行的编程语言,它们在许多方面都有各自的优缺点和优势。下面我们将详细分析Java和.NET谁更优秀,并给出具体理由。性能:Java和.NET在性能上没有明显的区别。它们都是基...

.NET Core/.NET5/.NET6 开源项目:工作流组件

前言开源项目是众多组织与个人分享的组件或项目,作者付出的心血我们是无法体会的,所以首先大家要心存感激、尊重。请严格遵守每个项目的开源协议后再使用。尊重知识产权,共建和谐开源社区。ELSAElsa Co...

谁说.NET没有GC调优,只改一行代码就让程序不再占用内存

经常看到有群友调侃“为什么搞Java的总在学习JVM调优?那是因为Java烂!我们.NET就不需要搞这些!”真的是这样吗?今天我就用一个案例来分析一下。昨天,一位学生问了我一个问题:他建了一个默认的A...

Avalonia:可信创.NET 跨平台UI,让JAVA失业者转.NET信创开发!

亲爱的读者们,今天我想与大家分享一个令人兴奋的主题 —— Avalonia,这个强大的.NET跨平台UI框架。作为一名曾经的JAVA开发者,我深知转换技术栈的挑战。然而,在当前快速变化的IT行业中,适...

net与java开发哪个好(.net和java哪个简单)

.NET和Java都是非常流行的开发平台,它们各自具有一些优点和缺点,选择哪个平台取决于项目的具体需求和开发团队的技能。下面是一些分析:.NET的优点:易于使用:.NET平台提供了一个易于使用的开发环...

通过接口实现JAVA和.NET互调用-JNInterface

使用C#编程多年,也十分感激微软在语言架构、语法糖、编辑器等方面给自己带来的便利。但因为最近工作中有接触到JAVA,渐渐地发现的确像大家说的那样,JAVA的生态很好,要找点什么几乎都有现成的,于是自然...