方法攔截中擷取相關資料的實現
如果你看不懂我的上一篇文章,請你退回去一步一步根據例子運行起來,在你啟動並執行過程中你會加深對
程式的理解。
本篇文章主要是利用ASM來在攔截方法時擷取方法調用時的相關資料,如參數列表,本地變數列表,方法
調用棧以及操作狀態代碼等重要資料,以及方法執行時間。結合當前系統的記憶體狀態,CPU佔用率等系統資訊
可以為商務邏輯出現異常時提供最可靠的分析依據。本篇要求對ByteCode有一定瞭解,即使你因為某些原
因不得不照抄這裡的實現,我也希望你能在先使用這些實現範例後再弄懂它。能看懂這些內容並能正確使用
這些實現的,你本來就有能力弄懂它。
一個比較繞人的地方,我們來比較一下用ASM動態產生class和利用代理代理來注入代碼的不同:
在利用代理來注入代碼時,其實我們是利用反射來invoke一個Method對象。所以要注入的代碼直接插入
在invoke的前後:
before();<br />invoke(obj,m,args);<br />after();
這樣的實現非常好理解,說白了就是如果你要執行方法m,那麼請你交給我來協助你執行,這樣我就可以向別
人炫耀一下:我要執行方法m了(before),我已經執行完方法m了(after)。
而在ASM動態產生class時,MyAdviceAdaptor的onMethodEnter和onMethodEixt方法是在封裝某個方法
調用的,而不是運行某個方法時調用的。如果我們在onMethodEnter方法中調用
System.out.println("Hello,world");
那麼是在visitMethod時調用了和要產生的新方法無關的一個System.out.println("Hello");的調用。
產生後的這個方法沒有被注入任何指令,所以在執行的時候不會調用System.out.println("Hello");
正確的做法是在onMethodEnter中調用ASM操作來向要封裝的方法插入指令:
visitFieldInsn(GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");<br />visitLdcInsn("Hello world!");<br />visitMethodInsn(INVOKEVIRTUAL,"java/io/PrintStream","println",<br />"(Ljava/lang/String;)V");
這樣的才能將System.out.println("Hello,world");插入到新產生的方法的最前端。
比如原來的方法是
void test{<br /> new Thread().start();<br />}
那麼,利用ASM產生這個方法應該是
visitTypeInsn(NEW, "java/lang/Thread");<br />visitInsn(DUP);<br />visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "()V");<br />visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "start", "()V");<br />visitInsn(RETURN);
而現在因為MyAdviceAdaptor的onMethodEnterr的方法中有插入指令的代碼,所以這些代碼會在MethoeVisitor
開始原始方法的產生之前最先插入,即產生新方法的指令變成:
visitFieldInsn(GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");<br />visitLdcInsn("Hello world!");<br />visitMethodInsn(INVOKEVIRTUAL,"java/io/PrintStream","println",<br />"(Ljava/lang/String;)V");<br />//這裡如果棧沒有平衡應該加上pop。使你插入的代碼在執行完成後,原始方法代碼開始之前棧還原成原始狀態。<br />visitTypeInsn(NEW, "java/lang/Thread");<br />visitInsn(DUP); //還記得我去年寫的為什麼對象new操作後一定會DUP?<br />//http://blog.csdn.net/axman/archive/2008/05/05/2393621.aspx<br />visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "()V");<br />visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "start", "()V");<br />visitInsn(RETURN);
這樣原來的方法就被重建為:
void test{<br /> System.out.println("Hello,world");<br /> new Thread().start();<br />}
同樣,必須在onMethodEixt中使用ASM插入指令的方法才能將代碼注入到產生後的方法中。
好了,弄明白上面的理論。我們來實現相關資料的擷取。
首先,在一個方法調用時,利用ASM我們能夠擷取到方法的參數列表和本地變數列表。以及方法最後操作狀態代碼
利用注入在原方法體前後的代碼的時間差可以計算出原方法代碼的執行時間。
因為onMethodEnter方法中插入的代碼會在原始方法的所有操作包括實參傳遞之前插入,所以擷取參數表,
本地變數表,操作狀態代碼都在onMethodEixt方法擷取:
擷取本地變數列表:
void onMethodExit(int opcode){<br /> int localVarCnt = 0;<br /> if(nextLocal > firstLocal) localVarCnt = nextLocal - firstLocal;<br /> //這兩個變數是AdviceAdapter的超類實現的,因為一個MethodVisitor在visitMethod可以從原始<br /> //的class中的某方法中分析出本地變數表。注意我說本地變數表是指靜態表。而本地變數列表是說<br /> //方法調用時實際的記憶體中的方法棧中的實際變數的列表。<br /> loadVarArray(localVarCnt);<br />}</p><p>private void loadVarArray(int varCnt){<br /> Type OBJECT_TYPE = Type.getObjectType("java/lang/Object");<br /> push(varCnt);<br /> newArray(OBJECT_TYPE);<br /> for (int i = 0; i < varCnt; i++)<br /> {<br /> int index = super.firstLocal + i;<br /> dup();<br /> push(i);<br /> loadLocal(index);<br /> box(getLocalType(index));<br /> arrayStore(OBJECT_TYPE);<br /> }<br /> //這時已經將本地變數全部放在一個Object[]中,這就是本地變數列表,如何傳出來,下面再說。<br />}</p><p>
擷取方法參數列表:
void onMethodExit(int opcode){<br /> loadArgArray();<br /> //這個方法也是AdviceAdapter的超類已經實現的。其實和本地變數列表擷取差不多,它的具體實現:<br /> //push(argumentTypes.length);<br /> //newArray(OBJECT_TYPE);<br /> //for (int i = 0; i < argumentTypes.length; i++) {<br /> // dup();<br /> // push(i);<br /> // loadArg(i);<br /> // box(argumentTypes[i]);<br /> // arrayStore(OBJECT_TYPE);<br /> //}<br /> //上面的loadVarArray其實就是參考這段代碼,然後計算出本地變數實際的index來實現的。<br />}
好了,假如我們現在要傳出這樣幾個資訊:
被攔截的方法所在的類,方法名稱,本地變數列表(數組),參數列表(數組),操作狀態代碼。那麼:
void onMethodExit(int opcode){<br /> visitLdcInsn(className);<br /> visitLdcInsn(methodName); //前一篇文章我們只用了方法的sortName,真正實現時應該用FullName,因為<br /> //方法有重載,只憑sortName不能限定到某一個方法。<br /> visitLdcInsn(String.valueOf(opcode));<br /> int localVarCnt = 0;<br /> if(nextLocal > firstLocal) {<br /> localVarCnt = nextLocal - firstLocal;<br /> loadVarArray(localVarCnt);<br /> }else{<br /> visitInsn(ACONST_NULL); //為了佔用一個棧位置。<br /> }<br /> int argsCnt = 0;<br /> argCnt = xxx;<br /> //從方法簽名可以分析出參數個數。<br /> if(argsCnt > 0){<br /> loadArgArray();<br /> }<br /> else{<br /> visitInsn(ACONST_NULL); //為了佔用一個棧位置。<br /> }<br />}
OK,現在棧頂向下的五個運算元分別是args,vars,opcode,mthodName,className,我們當然可以同樣用ASM指令
產生代碼將棧中的五個運算元傳遞到指定的目的地,但這樣的操作太複雜,我們只要調用一個有五個參數的方法,然後在
這個方法內用普通JAVA代碼自由地實現向目的地傳送,而不是在被攔截的方法中用ASM代碼來實現向目的地傳送。
所以我們只要在外部定義好一個方法形如:
class org.axman.test.Sender{<br /> public static void send(String className,String method,String opCode,Object[] localVar,Object[] args){<br /> //列印 或者 儲存到檔案/資料庫或傳給其它服務處理。<br /> }<br />}</p><p>那麼在onMethodExit中:<br />void onMethodExit(int opcode){<br /> visitLdcInsn(className);<br /> visitLdcInsn(methodName); //前一篇文章我們只用了方法的sortName,真正實現時應該用FullName,因為<br /> //方法有重載,只憑sortName不能限定到某一個方法。<br /> visitLdcInsn(String.valueOf(opcode));<br /> int localVarCnt = 0;<br /> if(nextLocal > firstLocal) {<br /> localVarCnt = nextLocal - firstLocal;<br /> loadVarArray(localVarCnt);<br /> }else{<br /> visitInsn(ACONST_NULL); //為了佔用一個棧位置。<br /> }<br /> int argsCnt = 0;<br /> argCnt = xxx;<br /> //從方法簽名可以分析出參數個數。<br /> if(argsCnt > 0){<br /> loadArgArray();<br /> }<br /> else{<br /> visitInsn(ACONST_NULL); //為了佔用一個棧位置。<br /> }<br /> visitMethodInsn(Opcodes.INVOKESTATIC, "org.axman.test.Sender","send",<br /> "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;[Ljava/lang/Object;)V");</p><p>}<br />
就可以將被攔截的方法的實參列表,本地變數列表,操作狀態代碼等資訊傳遞到外部來。
擷取調用棧
利用sun.reflect.Reflection.getCallerClass(x);我們只能擷取調用者所在的類,如果是同一類中不同方法的
之間的調用測無法明確地查看方法調用鏈。
使用Throwable的getStackTrace()則可以擷取完整的方法調用棧。我們只要構造一個Throwable對象就可以通過
它來擷取調用這個方法的所有調用者:
public static String getInvokeStack(){<br /> Throwable t = new Throwable();<br /> StackTraceElement ste[] = t.getStackTrace();<br /> StringBuilder sb = new StringBuilder();<br /> for (int i = 1; i < ste.length; i++)<br /> {<br /> sb.append(ste[i].toString()).append(";");<br /> }<br /> return sb.toString();<br /> }
這裡僅僅是調用StackTraceElement的toString()方法,你可以根據需要擷取它的詳細資料。
由於set[0]是t對象所在的方法本身,我們不需要這個調用者資訊。所以迴圈直接從1開始。
如果Test.main()方法中調用了這個方法,那麼第一個調用者直接是Test.main();
本來我們需要在onMethodEixt方法中利用ASM指令來讓被攔截的方法調用這個方法,並將方法傳回值壓棧作為一個參數
傳給send方法。但是因為被攔截的方法一定會調用send方法,所以我們直接在send中調用getInvokeStack(),那麼被攔截的
方法就是第二調用者。所以我們只要將迴圈從2開始,然後在send方法中調用getInvokeStack();就可以擷取到被攔截的方法
的所有調用鏈。
實際上,我們在OnMethidEnter方法中同樣會將className,methodName利用類似的方法傳遞出來。同時在輸入要攔截的方法時還會傳入
一個隨機串給MyAdviceAdaptor的構造方法,然後在OnMethidEnter和OnMethidEixt方法中同時會調用ASM指令把這個隨機串都傳給形如
Sender.send()的方法,這樣可以對方法進入和退出的send資料進行配對。並可以用兩個配對的send的時間差計算方法執行時間而不是把
計算原方法代碼執行時間的代碼注入到原方法。
其它的細節問題。以後再作交待。(整個測試專案的壓縮文檔需要聯絡擷取,以前在blog上提供MMS項目的壓縮檔,有很多兄弟竟然直接拿項目中My Phone號進行開發測試,弄得我收到大量的測試多媒體訊息)