AOP從靜態代理到動態代理 Emit實現
【前言】
AOP為Aspect Oriented Programming的縮寫,意思是面向切面程式設計的技術。
何為切面?
一個和業務沒有任何耦合相關的程式碼段,諸如:呼叫日誌,傳送郵件,甚至路由分發。一切能為程式碼所有且能和程式碼充分解耦的程式碼都可以作為一個業務程式碼的切面。
我們為什麼要AOP?
那我們從一個場景舉例說起:
如果想要採集使用者操作行為,我們需要掌握使用者呼叫的每一個介面的資訊。這時候的我們要怎麼做?
如果不採用AOP技術,也是最簡單的,所有方法體第一句話先呼叫一個日誌介面將方法資訊傳遞記錄。
有何問題?
實現業務沒有任何問題,但是隨之而來的是程式碼臃腫不堪,難以調整維護的諸多問題(可自行腦補)。
如果我們採用了AOP技術,我們就可以在系統啟動的地方將所有將要採集日誌的類注入,每一次呼叫方法前,AOP框架會自動呼叫我們的日誌程式碼。
是不是省去了很多重複無用的勞動?程式碼也將變得非常好維護(有朝一日不需要了,只需將切面程式碼註釋掉即可)
接下來我們看看AOP框架的工作原理以及實過程。
【實現思路】
AOP框架呢,一般通過靜態代理和動態代理兩種實現方式。
何為靜態代理?
靜態代理,又叫編譯時代理,就是在編譯的時候,已經存在代理類,執行時直接呼叫的方式。說的通俗一點,就是自己手動寫程式碼實現代理類的方式。
我們通過一個例子來展現一下靜態代理的實現過程:
我們這裡有一個業務類,裡面有方法Test(),我們要在Test呼叫前和呼叫後分別輸出日誌。
我們既然要將Log當作一個切面,我們肯定不能去動原有的業務程式碼,那樣也違反了面向物件設計之開閉原則。
那麼我們要怎麼做呢?我們定義一個新類 BusinessProxy 去包裝一下這個類。為了便於在多個方法的時候區分和辨認,方法也叫 Test()
這樣,我們如果要在所有的Business類中的方法都新增Log,我們就在BusinessProxy代理類中新增對應的方法去包裝。既不破壞原有邏輯,又可以實現前後日誌的功能。
當然,我們可以有更優雅的實現方式:
我們可以定義代理類,繼承自業務類。將業務類中的方法定義為虛方法。那麼我們可以重寫父類的方法並且在加入日誌以後再呼叫父類的原方法。
當然,我們還有更加優雅的實現方式:
我們可以使用發射的技術,寫一個通用的Invoke方法,所有的方法都可以通過該方法呼叫。
我們這樣便實現了一個靜態代理。
那我們既然有了靜態代理,為什麼又要有動態代理呢?
我們仔細回顧靜態代理的實現過程。我們要在所有的方法中新增切面,我們就要在代理類中重寫所有的業務方法。更有甚者,我們有N個業務類,就要定義N個代理類。這是很龐大的工作量。
這就是動態代理出現的背景,相比都可以猜得到,動態代理就是將這一系列繁瑣的步驟自動化,讓程式自動為我們生成代理類。
何為動態代理?
動態代理,又成為執行時代理。在程式執行的過程中,呼叫了生成代理類的程式碼,將自動生成業務類的代理類。不需要我們手共編寫,極高的提高了工作效率和 調整了程式設計師的心態 。
原理不必多說,就是動態生成靜態代理的程式碼。我們要做的,就是選用一種生成程式碼的方式去生成。
今天我分享一個簡單的AOP框架,程式碼使用Emit生成。當然,Emit 程式碼的寫法不是今天要講的主要內容,需要提前去學習。
先說效果:
定義一個Action特性類 ActionAttribute 繼承自 ActionBaseAttribute ,裡面在Before和After方法中輸出兩條日誌;
定義一個Action特性類 InterceptorAttribute 繼承自 InterceptorBaseAttribute ,裡面捕獲了方法呼叫異常,以及執行前後分別輸出日誌;
然後定義一個業務類 BusinessClass 實現了 IBusinessClass 介面,定義了各種型別的方法
多餘的方法不貼圖了。
我們把上面定義的方法呼叫切面標籤放在業務類上,表示該類下所有的方法都執行異常過濾;
我們把Action特性放在Test方法上,表明要在 Test() 方法的 Before 和 After 呼叫時記錄日誌;
我們定義測試類:
呼叫一下試試:
可見,全類方法標籤 Interceptor 在 Test 和 GetInt 方法呼叫前後都打出了對應的日誌;
Action方法標籤只在 Test 方法上做了標記,那麼 Test 方法 Before 和 After 執行時打出了日誌;
【實現過程】
實現的思路在上面已經有詳細的講解,可以參考靜態代理的實現思路。
我們定義一個動態代理生成類 DynamicProxy ,用於原業務程式碼的掃描和代理類程式碼的生成;
定義兩個過濾器標籤, ActionBaseAttribute ,提供 Before 和 After 切面方法; InterceptorBaseAttribute ,提供 Invoke “全呼叫”包裝的切面方法;
Before可以獲取到當前呼叫的方法和引數列表,After可以獲取到當前方法呼叫以後的結果。
Invoke 可以拿到當前呼叫的物件和方法名,引數列表。在這裡進行反射動態呼叫。
1 [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] 2public class ActionBaseAttribute : Attribute 3{ 4public virtual void Before(string @method, object[] parameters) { } 5 6public virtual object After(string @method, object result) { return result; } 7}
1 [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] 2public class InterceptorBaseAttribute : Attribute 3{ 4public virtual object Invoke(object @object, string @method, object[] parameters) 5{ 6return @object.GetType().GetMethod(@method).Invoke(@object, parameters); 7} 8}
代理生成類採用Emit的方式生成執行時IL程式碼。
先把程式碼放在這裡:
1 public class DynamicProxy 2{ 3public static TInterface CreateProxyOfRealize<TInterface, TImp>() where TImp : class, new() where TInterface : class 4{ 5return Invoke<TInterface, TImp>(); 6} 7 8public static TProxyClass CreateProxyOfInherit<TProxyClass>() where TProxyClass : class, new() 9{ 10return Invoke<TProxyClass, TProxyClass>(true); 11} 12 13private static TInterface Invoke<TInterface, TImp>(bool inheritMode = false) where TImp : class, new() where TInterface : class 14{ 15var impType = typeof(TImp); 16 17string nameOfAssembly = impType.Name + "ProxyAssembly"; 18string nameOfModule = impType.Name + "ProxyModule"; 19string nameOfType = impType.Name + "Proxy"; 20 21var assemblyName = new AssemblyName(nameOfAssembly); 22 23var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); 24var moduleBuilder = assembly.DefineDynamicModule(nameOfModule); 25 26//var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave); 27//var moduleBuilder = assembly.DefineDynamicModule(nameOfModule, nameOfAssembly + ".dll"); 28 29TypeBuilder typeBuilder; 30if (inheritMode) 31typeBuilder = moduleBuilder.DefineType(nameOfType, TypeAttributes.Public, impType); 32else 33typeBuilder = moduleBuilder.DefineType(nameOfType, TypeAttributes.Public, null, new[] { typeof(TInterface) }); 34 35InjectInterceptor<TImp>(typeBuilder, impType.GetCustomAttribute(typeof(InterceptorBaseAttribute))?.GetType(), inheritMode); 36 37var t = typeBuilder.CreateType(); 38 39//assembly.Save(nameOfAssembly + ".dll"); 40 41return Activator.CreateInstance(t) as TInterface; 42} 43 44private static void InjectInterceptor<TImp>(TypeBuilder typeBuilder, Type interceptorAttributeType, bool inheritMode = false) 45{ 46var impType = typeof(TImp); 47// ---- define fields ---- 48FieldBuilder fieldInterceptor = null; 49if (interceptorAttributeType != null) 50{ 51fieldInterceptor = typeBuilder.DefineField("_interceptor", interceptorAttributeType, FieldAttributes.Private); 52} 53// ---- define costructors ---- 54if (interceptorAttributeType != null) 55{ 56var constructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, null); 57var ilOfCtor = constructorBuilder.GetILGenerator(); 58 59ilOfCtor.Emit(OpCodes.Ldarg_0); 60ilOfCtor.Emit(OpCodes.Newobj, interceptorAttributeType.GetConstructor(new Type[0])); 61ilOfCtor.Emit(OpCodes.Stfld, fieldInterceptor); 62ilOfCtor.Emit(OpCodes.Ret); 63} 64 65// ---- define methods ---- 66 67var methodsOfType = impType.GetMethods(BindingFlags.Public | BindingFlags.Instance); 68 69string[] ignoreMethodName = new[] { "GetType", "ToString", "GetHashCode", "Equals" }; 70 71foreach (var method in methodsOfType) 72{ 73//ignore method 74if (ignoreMethodName.Contains(method.Name)) 75return; 76 77var methodParameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray(); 78 79MethodAttributes methodAttributes; 80 81if (inheritMode) 82methodAttributes = MethodAttributes.Public | MethodAttributes.Virtual; 83else 84methodAttributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual | MethodAttributes.Final; 85 86var methodBuilder = typeBuilder.DefineMethod(method.Name, methodAttributes, CallingConventions.Standard, method.ReturnType, methodParameterTypes); 87 88var ilMethod = methodBuilder.GetILGenerator(); 89 90// set local field 91var impObj = ilMethod.DeclareLocal(impType);//instance of imp object 92var methodName = ilMethod.DeclareLocal(typeof(string));//instance of method name 93var parameters = ilMethod.DeclareLocal(typeof(object[]));//instance of parameters 94var result = ilMethod.DeclareLocal(typeof(object));//instance of result 95LocalBuilder actionAttributeObj = null; 96 97//attribute init 98Type actionAttributeType = null; 99if (method.GetCustomAttribute(typeof(ActionBaseAttribute)) != null || impType.GetCustomAttribute(typeof(ActionBaseAttribute)) != null) 100{ 101//method can override class attrubute 102if (method.GetCustomAttribute(typeof(ActionBaseAttribute)) != null) 103{ 104actionAttributeType = method.GetCustomAttribute(typeof(ActionBaseAttribute)).GetType(); 105} 106else if (impType.GetCustomAttribute(typeof(ActionBaseAttribute)) != null) 107{ 108actionAttributeType = impType.GetCustomAttribute(typeof(ActionBaseAttribute)).GetType(); 109} 110 111actionAttributeObj = ilMethod.DeclareLocal(actionAttributeType); 112ilMethod.Emit(OpCodes.Newobj, actionAttributeType.GetConstructor(new Type[0])); 113ilMethod.Emit(OpCodes.Stloc, actionAttributeObj); 114} 115 116//instance imp 117ilMethod.Emit(OpCodes.Newobj, impType.GetConstructor(new Type[0])); 118ilMethod.Emit(OpCodes.Stloc, impObj); 119 120//if no attribute 121if (fieldInterceptor != null || actionAttributeObj != null) 122{ 123ilMethod.Emit(OpCodes.Ldstr, method.Name); 124ilMethod.Emit(OpCodes.Stloc, methodName); 125 126ilMethod.Emit(OpCodes.Ldc_I4, methodParameterTypes.Length); 127ilMethod.Emit(OpCodes.Newarr, typeof(object)); 128ilMethod.Emit(OpCodes.Stloc, parameters); 129 130// build the method parameters 131for (var j = 0; j < methodParameterTypes.Length; j++) 132{ 133ilMethod.Emit(OpCodes.Ldloc, parameters); 134ilMethod.Emit(OpCodes.Ldc_I4, j); 135ilMethod.Emit(OpCodes.Ldarg, j + 1); 136//box 137ilMethod.Emit(OpCodes.Box, methodParameterTypes[j]); 138ilMethod.Emit(OpCodes.Stelem_Ref); 139} 140} 141 142//dynamic proxy action before 143if (actionAttributeType != null) 144{ 145//load arguments 146ilMethod.Emit(OpCodes.Ldloc, actionAttributeObj); 147ilMethod.Emit(OpCodes.Ldloc, methodName); 148ilMethod.Emit(OpCodes.Ldloc, parameters); 149ilMethod.Emit(OpCodes.Call, actionAttributeType.GetMethod("Before")); 150} 151 152if (interceptorAttributeType != null) 153{ 154//load arguments 155ilMethod.Emit(OpCodes.Ldarg_0);//this 156ilMethod.Emit(OpCodes.Ldfld, fieldInterceptor); 157ilMethod.Emit(OpCodes.Ldloc, impObj); 158ilMethod.Emit(OpCodes.Ldloc, methodName); 159ilMethod.Emit(OpCodes.Ldloc, parameters); 160// call Invoke() method of Interceptor 161ilMethod.Emit(OpCodes.Callvirt, interceptorAttributeType.GetMethod("Invoke")); 162} 163else 164{ 165//direct call method 166if (method.ReturnType == typeof(void) && actionAttributeType == null) 167{ 168ilMethod.Emit(OpCodes.Ldnull); 169} 170 171ilMethod.Emit(OpCodes.Ldloc, impObj); 172for (var j = 0; j < methodParameterTypes.Length; j++) 173{ 174ilMethod.Emit(OpCodes.Ldarg, j + 1); 175} 176ilMethod.Emit(OpCodes.Callvirt, impType.GetMethod(method.Name)); 177//box 178if (actionAttributeType != null) 179{ 180if (method.ReturnType != typeof(void)) 181ilMethod.Emit(OpCodes.Box, method.ReturnType); 182else 183ilMethod.Emit(OpCodes.Ldnull); 184} 185} 186 187//dynamic proxy action after 188if (actionAttributeType != null) 189{ 190ilMethod.Emit(OpCodes.Stloc, result); 191//load arguments 192ilMethod.Emit(OpCodes.Ldloc, actionAttributeObj); 193ilMethod.Emit(OpCodes.Ldloc, methodName); 194ilMethod.Emit(OpCodes.Ldloc, result); 195ilMethod.Emit(OpCodes.Call, actionAttributeType.GetMethod("After")); 196} 197 198// pop the stack if return void 199if (method.ReturnType == typeof(void)) 200{ 201ilMethod.Emit(OpCodes.Pop); 202} 203else 204{ 205//unbox,if direct invoke,no box 206if (fieldInterceptor != null || actionAttributeObj != null) 207{ 208if (method.ReturnType.IsValueType) 209ilMethod.Emit(OpCodes.Unbox_Any, method.ReturnType); 210else 211ilMethod.Emit(OpCodes.Castclass, method.ReturnType); 212} 213} 214// complete 215ilMethod.Emit(OpCodes.Ret); 216} 217} 218} DynamicProxy
裡面實現了兩種代理方式,一種是 面向介面實現 的方式,另一種是 繼承重寫 的方式。
但是繼承重寫的方式需要把業務類的所有方法寫成virtual虛方法,動態類會重寫該方法。
我們從上一節的Demo中獲取到執行時生成的代理類dll,用ILSpy反編譯檢視原始碼:
可以看到,我們的代理類分別呼叫了我們特性標籤中的各項方法。
核心程式碼分析(原始碼在上面摺疊部位已經貼出):
解釋:如果該方法存在Action標籤,那麼載入 action 標籤例項化物件,載入引數,執行Before方法;如果該方法存在Interceptor標籤,那麼使用類欄位this._interceptor呼叫該標籤的Invoke方法。
解釋:如果面的Interceptor特性標籤不存在,那麼會載入當前掃描的方法對應的引數,直接呼叫方法;如果Action標籤存在,則將剛才呼叫的結果包裝成object物件傳遞到After方法中。
這裡如果目標引數是object型別,而實際引數是直接呼叫返回的明確的值型別,需要進行裝箱操作,否則執行時報呼叫記憶體錯誤異常。
解釋:如果返回值是void型別,則直接結束並返回結果;如果返回值是值型別,則需要手動拆箱操作,如果是引用型別,那麼需要型別轉換操作。
IL實現的細節,這裡不做重點討論。
【系統測試】
1.介面實現方式,Api測試(各種標籤使用方式對應的不同型別的方法呼叫):
結論:對於上述窮舉的型別,各種標籤使用方式皆成功打出了日誌;
2.繼承方式,Api測試(各種標籤使用方式對應的不同型別的方法呼叫):
結論:繼承方式和介面實現方式的效果是一樣的,只是方法上需要不同的實現調整;
3.直接呼叫三個方法百萬次效能結果:
結論:直接呼叫三個方法百萬次呼叫耗時 58ms
4.使用實現介面方式三個方法百萬次呼叫結果
結論:結果見上圖,需要注意是三個方法百萬次呼叫,也就是300w次的方法呼叫
5.使用繼承方式三個方法百萬次呼叫結果
結論:結果見上圖,需要注意是三個方法百萬次呼叫,也就是300w次的方法呼叫
事實證明,IL Emit的實現方式效能還是很高的。
綜合分析:
通過各種的呼叫分析,可以看出使用代理以後和原生方法呼叫相比效能損耗在哪裡。效能差距最大的,也是耗時最多的實現方式就是添加了全類方法代理而且是使用Invoke進行全方法切面方式。該方式耗時的原因是使用了反射Invoke的方法。
直接新增Action代理類實現 Before和After的方式和原生差距不大,主要損耗在After觸發時的拆裝箱上。
綜上分析,我們使用的時候,儘量針對性地對某一個方法進行AOP注入,而儘量不要全類方法進行AOP注入。
【總結】
通過自己實現一個AOP的動態注入框架,對Emit有了更加深入的瞭解,最重要的是,對CLR IL程式碼的執行過程有了一定的認知,受益匪淺。
該方法在使用的過程中也發現了問題,比如有ref和out型別的引數時,會出現問題,需要後續繼續改進
本文的原始碼已託管在GitHub上,又需要可以自行拿取(順手Star哦~): https://github.com/sevenTiny/CodeArts
該程式碼的位置在 CodeArts.CSharp 分割槽下
VS開啟後,可以在 EmitDynamicProxy 分割槽下找到;本部落格所有的測試專案都在專案中可以找到。
再次放上原始碼地址,供一起學習的朋友參考,希望能幫助到你: https://github.com/sevenTiny/CodeArts