C#動態(tài)方法攔截(AOP)的5種解決方案!
轉(zhuǎn)自:Artech cnblogs.com/artech/archive
前言
AOP的本質(zhì)是方法攔截(將針對目標(biāo)方法調(diào)用劫持下來,進(jìn)而執(zhí)行執(zhí)行的操作),置于方法攔截的實(shí)現(xiàn)方案,不外乎兩種代碼注入類型,即編譯時的靜態(tài)注入和運(yùn)行時的動態(tài)注入,本篇文章列出了幾種常用的動態(tài)注入方案。
這篇文章的目標(biāo)并不是提供完整的AOP框架的解決方案,而是說明各種解決方案后面的原理,所以我們提供的實(shí)例代碼會盡可能簡單。
為了確定攔截操作是否執(zhí)行,我們定義了如下這個Indicator類型,我們的攔截操作會將其靜態(tài)屬性Injected屬性設(shè)置為True,我們演示的代碼最終通過這個屬性來確定攔截是否成功。源代碼下載:https://files.cnblogs.com/files/artech/Interception.7z
public static class Indicator
{
public static bool Injected { get; set; }
}
一、IL Emit(接口)
IL Emit是實(shí)現(xiàn)AOP的首選方案。如果方法調(diào)用時針對接口完成,我們可以生成一個代理類型來封裝對象,并且這個代理類型同時實(shí)現(xiàn)目標(biāo)接口,那么只要我們能夠?qū)⑨槍δ繕?biāo)對象的方法調(diào)用轉(zhuǎn)換成針對代理對象的調(diào)用,就能實(shí)現(xiàn)針對目標(biāo)對象的方法攔截。
舉個簡單的例子,F(xiàn)oobar實(shí)現(xiàn)了IFoobar接口,如果我們需要攔截接口方法Invoke,我們可以生成一個FoobarProxy類型。
如代碼片段所示,F(xiàn)oobarProxy封裝了一個IFoobar對象,并實(shí)現(xiàn)了IFoobar接口。在實(shí)現(xiàn)的Invoke方法中,它在調(diào)用封裝對象的同名方法之前率先執(zhí)行了攔截操作。
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方法中,我們創(chuàng)建一個Foobar對象,讓據(jù)此創(chuàng)建這個動態(tài)生成的FoobarProxy,當(dāng)該對象的Invoke方法執(zhí)行的時候,我們期望的攔截操作自然會自動執(zhí)行。
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的方式動態(tài)生成一個派生類,并重寫這個虛方法的方式來完成攔截。以下面的代碼片段為例,我們需要攔截定義在Foobar中的虛方法Invoke,我們可以生成如下這個派生與Foobar的Foobar的FoobarProxy類型,在重寫的Invoke方法中,我們在調(diào)用基類同名方法之前,率先執(zhí)行攔截操作。
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();
}
}
三、方法替換(跳轉(zhuǎn))
上面兩種方案都具有一個局限性:需要將針對目標(biāo)對象的方法調(diào)用轉(zhuǎn)換成針對代理對象的調(diào)用。如果我們能夠直接將目標(biāo)方法替換成另一個包含攔截操作的方案(或者說從原來的方法調(diào)轉(zhuǎn)到具有攔截操作的方法),那么即使我們不改變方法的調(diào)用方式,方法依舊能夠攔截。Harmony框架就是采用這樣的方案實(shí)現(xiàn)的,我們可以通過下面這個簡單的實(shí)例來模擬其實(shí)現(xiàn)原理(下面演示的程序引用了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方法中根據(jù)這個方法創(chuàng)建了一個DynamicMethodDefinition對象(定義在MonoMod.Common包中),并在方法體的前面添加了兩個IL指令將Indicator的Injected屬性設(shè)置為True,該方法最終返回通過這個DynamicMethodDefinition對象生成的MethodBase對象。
在Main方法中,我們利用HarmonyLib.Memory的靜態(tài)方法DetourMethod將原始的Invoke方法“轉(zhuǎn)移”到生成的方法上。即使我們調(diào)用的依然是Foobar對象的Invoke方法,但是攔截操作依然會被執(zhí)行。
四、RealProxy/TransparentProxy
RealProxy/TransparentProxy是.NET Framework時代一種常用的方法攔截方案。如果目標(biāo)類型實(shí)現(xiàn)了某個接口或者派生于MarshalByRefObject類型,我們就可以采用這種攔截方案。
如果需要攔截某個類型的方法,我們可以定義如下這么一個FoobarProxy
和第一種方案一樣,我們的代理對象依舊是封裝目標(biāo)對象,在實(shí)現(xiàn)的Invoke方案中,我們利用作為參數(shù)的IMessage 方法得到代表目標(biāo)方法的MethodBase對象,進(jìn)而利用它實(shí)現(xiàn)針對目標(biāo)方法的調(diào)用。在目標(biāo)方法調(diào)用之前,我們可以執(zhí)行攔截操作。
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方法中,我們創(chuàng)建目標(biāo)Foobar對象,然后將其封裝成一個FoobarProxy
當(dāng)我們調(diào)用這個透明對象的任何一個方法的時候,定義在FoobarProxy
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項目中實(shí)現(xiàn),在.NET Core中它具有一個替代類型,那就是DispatchProxy。
我們可以采用如下的方式利用DispatchProxy實(shí)現(xiàn)我們所需的攔截功能。
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);
}
}
- EOF -
終于可以DIY個人微信紅包封面了!還免費(fèi)!
臥槽:微信可以這樣換個字體了!
點(diǎn)贊和在看就是最大的支持??
