動態方法攔截(AOP)的N種解決方案
- 2021 年 1 月 11 日
- 筆記
- [02] 編程技巧, AOP, DispatchProxy, Dynamic Method, IL Emit, Interception, RealProxy
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); } }