【前言】
aop(aspect orient programming),我們一般稱為面向方面(切面)編程,作為面向對象的一種補充,用于處理系統中分布于各個模塊的橫切關注點,比如事務管理、日志、緩存等等。aop實現的關鍵在于aop框架自動創建的aop代理,aop代理主要分為靜態代理和動態代理,靜態代理的代表為aspectj;而動態代理則以spring aop為代表。
何為切面?
一個和業務沒有任何耦合相關的代碼段,諸如:調用日志,發送郵件,甚至路由分發。一切能為代碼所有且能和代碼充分解耦的代碼都可以作為一個業務代碼的切面。
我們為什么要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
2
3
4
5
6
7
|
[attributeusage(attributetargets.method | attributetargets. class , allowmultiple = false , inherited = true )] public class actionbaseattribute : attribute { public virtual void before( string @method, object [] parameters) { } public virtual object after( string @method, object result) { return result; } } |
1
2
3
4
5
6
7
8
|
[attributeusage(attributetargets. class , allowmultiple = false , inherited = true )] public class interceptorbaseattribute : attribute { public virtual object invoke( object @ object , string @method, object [] parameters) { return @ object .gettype().getmethod(@method).invoke(@ object , parameters); } } |
代理生成類采用emit的方式生成運行時il代碼。
先把代碼放在這里:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
|
public class dynamicproxy { public static tinterface createproxyofrealize<tinterface, timp>() where timp : class , new () where tinterface : class { return invoke<tinterface, timp>(); } public static tproxyclass createproxyofinherit<tproxyclass>() where tproxyclass : class , new () { return invoke<tproxyclass, tproxyclass>( true ); } private static tinterface invoke<tinterface, timp>( bool inheritmode = false ) where timp : class , new () where tinterface : class { var imptype = typeof (timp); string nameofassembly = imptype.name + "proxyassembly" ; string nameofmodule = imptype.name + "proxymodule" ; string nameoftype = imptype.name + "proxy" ; var assemblyname = new assemblyname(nameofassembly); var assembly = appdomain.currentdomain.definedynamicassembly(assemblyname, assemblybuilderaccess.run); var modulebuilder = assembly.definedynamicmodule(nameofmodule); //var assembly = appdomain.currentdomain.definedynamicassembly(assemblyname, assemblybuilderaccess.runandsave); //var modulebuilder = assembly.definedynamicmodule(nameofmodule, nameofassembly + ".dll"); typebuilder typebuilder; if (inheritmode) typebuilder = modulebuilder.definetype(nameoftype, typeattributes. public , imptype); else typebuilder = modulebuilder.definetype(nameoftype, typeattributes. public , null , new [] { typeof (tinterface) }); injectinterceptor<timp>(typebuilder, imptype.getcustomattribute( typeof (interceptorbaseattribute))?.gettype(), inheritmode); var t = typebuilder.createtype(); //assembly.save(nameofassembly + ".dll"); return activator.createinstance(t) as tinterface; } private static void injectinterceptor<timp>(typebuilder typebuilder, type interceptorattributetype, bool inheritmode = false ) { var imptype = typeof (timp); // ---- define fields ---- fieldbuilder fieldinterceptor = null ; if (interceptorattributetype != null ) { fieldinterceptor = typebuilder.definefield( "_interceptor" , interceptorattributetype, fieldattributes. private ); } // ---- define costructors ---- if (interceptorattributetype != null ) { var constructorbuilder = typebuilder.defineconstructor(methodattributes. public , callingconventions.standard, null ); var ilofctor = constructorbuilder.getilgenerator(); ilofctor.emit(opcodes.ldarg_0); ilofctor.emit(opcodes.newobj, interceptorattributetype.getconstructor( new type[0])); ilofctor.emit(opcodes.stfld, fieldinterceptor); ilofctor.emit(opcodes.ret); } // ---- define methods ---- var methodsoftype = imptype.getmethods(bindingflags. public | bindingflags.instance); string [] ignoremethodname = new [] { "gettype" , "tostring" , "gethashcode" , "equals" }; foreach (var method in methodsoftype) { //ignore method if (ignoremethodname.contains(method.name)) return ; var methodparametertypes = method.getparameters().select(p => p.parametertype).toarray(); methodattributes methodattributes; if (inheritmode) methodattributes = methodattributes. public | methodattributes. virtual ; else methodattributes = methodattributes. public | methodattributes.hidebysig | methodattributes.newslot | methodattributes. virtual | methodattributes.final; var methodbuilder = typebuilder.definemethod(method.name, methodattributes, callingconventions.standard, method.returntype, methodparametertypes); var ilmethod = methodbuilder.getilgenerator(); // set local field var impobj = ilmethod.declarelocal(imptype); //instance of imp object var methodname = ilmethod.declarelocal( typeof ( string )); //instance of method name var parameters = ilmethod.declarelocal( typeof ( object [])); //instance of parameters var result = ilmethod.declarelocal( typeof ( object )); //instance of result localbuilder actionattributeobj = null ; //attribute init type actionattributetype = null ; if (method.getcustomattribute( typeof (actionbaseattribute)) != null || imptype.getcustomattribute( typeof (actionbaseattribute)) != null ) { //method can override class attrubute if (method.getcustomattribute( typeof (actionbaseattribute)) != null ) { actionattributetype = method.getcustomattribute( typeof (actionbaseattribute)).gettype(); } else if (imptype.getcustomattribute( typeof (actionbaseattribute)) != null ) { actionattributetype = imptype.getcustomattribute( typeof (actionbaseattribute)).gettype(); } actionattributeobj = ilmethod.declarelocal(actionattributetype); ilmethod.emit(opcodes.newobj, actionattributetype.getconstructor( new type[0])); ilmethod.emit(opcodes.stloc, actionattributeobj); } //instance imp ilmethod.emit(opcodes.newobj, imptype.getconstructor( new type[0])); ilmethod.emit(opcodes.stloc, impobj); //if no attribute if (fieldinterceptor != null || actionattributeobj != null ) { ilmethod.emit(opcodes.ldstr, method.name); ilmethod.emit(opcodes.stloc, methodname); ilmethod.emit(opcodes.ldc_i4, methodparametertypes.length); ilmethod.emit(opcodes.newarr, typeof ( object )); ilmethod.emit(opcodes.stloc, parameters); // build the method parameters for (var j = 0; j < methodparametertypes.length; j++) { ilmethod.emit(opcodes.ldloc, parameters); ilmethod.emit(opcodes.ldc_i4, j); ilmethod.emit(opcodes.ldarg, j + 1); //box ilmethod.emit(opcodes.box, methodparametertypes[j]); ilmethod.emit(opcodes.stelem_ref); } } //dynamic proxy action before if (actionattributetype != null ) { //load arguments ilmethod.emit(opcodes.ldloc, actionattributeobj); ilmethod.emit(opcodes.ldloc, methodname); ilmethod.emit(opcodes.ldloc, parameters); ilmethod.emit(opcodes.call, actionattributetype.getmethod( "before" )); } if (interceptorattributetype != null ) { //load arguments ilmethod.emit(opcodes.ldarg_0); //this ilmethod.emit(opcodes.ldfld, fieldinterceptor); ilmethod.emit(opcodes.ldloc, impobj); ilmethod.emit(opcodes.ldloc, methodname); ilmethod.emit(opcodes.ldloc, parameters); // call invoke() method of interceptor ilmethod.emit(opcodes.callvirt, interceptorattributetype.getmethod( "invoke" )); } else { //direct call method if (method.returntype == typeof ( void ) && actionattributetype == null ) { ilmethod.emit(opcodes.ldnull); } ilmethod.emit(opcodes.ldloc, impobj); for (var j = 0; j < methodparametertypes.length; j++) { ilmethod.emit(opcodes.ldarg, j + 1); } ilmethod.emit(opcodes.callvirt, imptype.getmethod(method.name)); //box if (actionattributetype != null ) { if (method.returntype != typeof ( void )) ilmethod.emit(opcodes.box, method.returntype); else ilmethod.emit(opcodes.ldnull); } } //dynamic proxy action after if (actionattributetype != null ) { ilmethod.emit(opcodes.stloc, result); //load arguments ilmethod.emit(opcodes.ldloc, actionattributeobj); ilmethod.emit(opcodes.ldloc, methodname); ilmethod.emit(opcodes.ldloc, result); ilmethod.emit(opcodes.call, actionattributetype.getmethod( "after" )); } // pop the stack if return void if (method.returntype == typeof ( void )) { ilmethod.emit(opcodes.pop); } else { //unbox,if direct invoke,no box if (fieldinterceptor != null || actionattributeobj != null ) { if (method.returntype.isvaluetype) ilmethod.emit(opcodes.unbox_any, method.returntype); else ilmethod.emit(opcodes.castclass, method.returntype); } } // complete ilmethod.emit(opcodes.ret); } } } |
里面實現了兩種代理方式,一種是 面向接口實現 的方式,另一種是 繼承重寫 的方式。
但是繼承重寫的方式需要把業務類的所有方法寫成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
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對服務器之家的支持。
原文鏈接:https://www.cnblogs.com/7tiny/p/9657451.html