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

createh55个月前 (02-04)技术教程50

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

相关文章

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

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

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

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

java中的url 编码与解码(java urldecode解码)

在开始讨论编码解码之前,首先来明确一下问题。什么是application/x-www-form-urlencoded字符串?答:它是一种编码类型。当URL地址里包含非西欧字符的字符串时,系统会将这些字...

工作日报 2021.10.20 OkHttp3错误异常:unexpected end of stream

2021.10.20问题澄清:Plugtest IDMS对接版本准备;蓝牙手咪适配问题配合定位;Mcdata http彩信上传,接口方案DT联调;河北联创项目联调;OkHttp3错误异常: java....

初识.Net Core Mvc(session 登录+查询)

ASP.NET Core MVC简单介绍:Controllers:控制器文件夹Views:视图文件夹wwwroot:静态文件文件夹,包括css,js,img等Program.cs:应用程序入口文件,包...

信创来袭,我们应该何去何从(信创还有机会吗)

最近,随着我国自主可控产业的快速发展,"信创"(信通技术和人工智能)已经成为一个热门话题。越来越多的人开始讨论,在信创大潮下,苦逼的程序员们应该采取什么样的应对措施。首先,有许多人担心.NET平台会被...