ASM字節(jié)碼編程 | 用字節(jié)碼增強技術(shù)給所有方法加上TryCatch捕獲異常并輸出
沉淀、分享、成長,讓自己和他人都能有所收獲
一、前言
你開發(fā)的系統(tǒng)是裸奔的嗎?深夜被老板 Diss
一套系統(tǒng)是否穩(wěn)定運行,取決于它的運行健康度,而這包括;調(diào)用量、可用率、響應時長以及服務器性能等各項指標的一個綜合值。并且在系統(tǒng)出現(xiàn)異常問題時,可以抓取整個業(yè)務方法執(zhí)行鏈路并輸出;當時的入?yún)ⅰ⒊鰠ⅰ惓P畔⒌鹊?。當然還包括一些JVM、Redis、Mysql的各項性能指標,以用于快速定位并解決問題。
那么要做到這樣的事情有什么監(jiān)控方案呢,這里面的做法比較多。比如;
最簡單粗暴的可能就是硬編碼在方法中,收取執(zhí)行耗時以及出入?yún)⒑彤惓P畔ⅰ5@樣的成本實在太大,而且有一些不可預估的風險。
可以選擇切面方式做一套統(tǒng)一監(jiān)控的組件,相對來說還是好一些的。但也需要硬編碼,同時維護成本不低。
市面上對于這樣的監(jiān)控其實是有整套的非入侵監(jiān)控方案的,比如;Google Dapper、Zipkin等都可以實現(xiàn),他們都是基于探針技術(shù)非入侵的采用字節(jié)碼增強的方式進行監(jiān)控。
好,那么這樣非入侵的探針方式是怎么實現(xiàn)的呢?如何去做方法的字節(jié)碼增強?
在字節(jié)碼增強方面有三個框架;ASM、Javassist、ByteCode,各有優(yōu)缺點按需選擇。這在我們之前的字節(jié)碼編程文章里也有所提到。
本文主要講解關(guān)于 ASM 方式的字節(jié)碼增強,接下來的案例會逐步講解一個給方法添加 TryCatch 塊,用于采集異常信息以及正常的出參結(jié)果的流程。
一步步向你展示通過指令碼來改寫你的方法!
二、系統(tǒng)環(huán)境
jdk1.8.0
asm-commons 6.2.1
三、技術(shù)目標
通過 ASM 字節(jié)碼增強技術(shù),使用指令碼將方法修改為我們想要的效果。這部分原本需要使用 JavaAgent 技術(shù),在工程啟動加載時候進行修改字節(jié)碼。這里為了將關(guān)于字節(jié)碼核心內(nèi)容展示出來,通過加載類名稱獲取字節(jié)碼進行修改。
這是修改之前的方法
public Integer strToNumber(String str) { return Integer.parseInt(str); }
1
2
3
這是修改之后的方法
public Integer strToNumber(String str) { try { Integer var2 = Integer.parseInt(str); MethodTest.point("org.itstack.test.MethodTest$Test.strToNumber", var2); return var2; } catch (Exception var3) { MethodTest.point("org.itstack.test.MethodTest$Test.strToNumber", var3); throw var3; } }
1
2
3
4
5
6
7
8
9
10
從修改前到修改后,可以看到。有如下幾點修改;
返回值賦值給新的參數(shù),并做了輸出
把方法包裹在一個 TryCatch 中,并將異常也做了輸出
好!如果你有很敏銳的嗅覺,或者很多小問號。那么你是否會想到如果使用到你自己的業(yè)務中,是不是就可以做一套非入侵的監(jiān)控系統(tǒng)了?
之后升職加薪
四、實現(xiàn)過程
字節(jié)碼增強的過程乍一看還是比較麻煩的,如果你沒有閱讀過JVM虛擬機規(guī)范等相關(guān)書籍,確實很不好理解。但是也就是這部分不那么容易理解的知識,才是你后續(xù)價值的體現(xiàn)。
接下來我會一步步的帶著你通過字節(jié)碼增強的方式,來實現(xiàn)我們的監(jiān)控需求。最終的完整的代碼,可以通過關(guān)注公眾號:bugstack蟲洞棧 回復源碼獲取(ASM字節(jié)碼編程)。
1. 搭建字節(jié)碼框架
/** * 字節(jié)碼增強獲取新的字節(jié)碼 */ private byte[] getBytes(String className) throws IOException { ClassReader cr = new ClassReader(className); ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS); cr.accept(new ClassVisitor(ASM5, cw) { public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { // 方法過濾 if (!"strToNumber".equals(name)) return super.visitMethod(access, name, descriptor, signature, exceptions); MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); return new AdviceAdapter(ASM5, mv, access, name, descriptor) { // 方法進入時修改字節(jié)碼 protected void onMethodEnter() {} // 訪問局部變量和操作數(shù)棧 public void visitMaxs(int maxStack, int maxLocals) {} // 方法退出時修改字節(jié)碼 protected void onMethodExit(int opcode) {} }; } }, ClassReader.EXPAND_FRAMES); return cw.toByteArray(); }
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
以上這段代碼就是 ASM 用于處理字節(jié)碼增強的模版代碼塊。首先他會分別創(chuàng)建 ClassReader、ClassWriter,用于對類的加載和寫入,這里的加載方式在構(gòu)造方法中也提供的比較豐富??梢酝ㄟ^類名、字節(jié)碼或者流的方式進行處理。
接下來是對方法的訪問 MethodVisitor ,基本所有使用 ASM 技術(shù)的監(jiān)控系統(tǒng),都會在這里來實現(xiàn)字節(jié)碼的注入。這里面目前用到了三個方法的,如下;
onMethodEnter 方法進入時設置一些基本內(nèi)容,比如當前納秒用于后續(xù)監(jiān)控方法的執(zhí)行耗時。還有就是一些 Try 塊的開始。
visitMaxs 這個是在方法結(jié)束前,用于添加 Catch 塊。到這也就可以將整個方法進行包裹起來了。
onMethodExit 最后是這個方法退出時,用于 RETURN 之前,可以注入結(jié)尾的字節(jié)碼加強,比如調(diào)用外部方法輸出監(jiān)控信息。
基本上所有的 ASM 字節(jié)碼增強操作,都離不開這三個方法。下面我就一步步來用指令將方法改造。
2. 獲取方法返回值
這是一個被測試的方法;
public Integer strToNumber(String str) { return Integer.parseInt(str); }
1
2
3
編寫指令
這個 onMethodExit 方法就是我們上面提到的字節(jié)碼編寫框架中的內(nèi)容,在里面添加具體的字節(jié)碼指令。
@Override protected void onMethodExit(int opcode) { if ((IRETURN <= opcode && opcode <= RETURN) || opcode == ATHROW) { int nextLocal = this.nextLocal; mv.visitVarInsn(ASTORE, nextLocal); // 將棧頂引用類型值保存到局部變量indexbyte中。 mv.visitVarInsn(ALOAD, nextLocal); // 從局部變量indexbyte中裝載引用類型值入棧。 } }
1
2
3
4
5
6
7
8
this.nextLocal,獲取局部變量的索引值。這個值就讓局部變量最后的值,也就是存放 ARETURN 的值(ARETURN,是返回對象類型,如果是返回 int 則需要使用 IRETURN)。
ASTORE,將棧頂引用類型值保存到局部變量indexbyte中。這里就是把返回的結(jié)果,保存到局部變量。在你頭腦中可以想象這有兩塊區(qū)域,一個是局部變量、一個是操作數(shù)棧。他們不斷的進行壓棧和操作。
ALOAD,從局部變量indexbyte中裝載引用類型值入?!,F(xiàn)在再將這個值放到操作數(shù)棧用,用于一會輸出使用。
被初次增強后的方法;
public Integer strToNumber(String str) { Integer var2 = Integer.parseInt(str); return var2; }
1
2
3
4
首先可以看到,原本的返回值被賦值到一個參數(shù)上,之后再由 return 將參數(shù)返回。這樣也就可以讓我們拿到了方法出參 var2 進行輸出操作。
3. 輸出方法返回值
在上面我們已經(jīng)將返回內(nèi)容賦值給參數(shù),那么在 return 之前,我們就可以在添加一個方法來輸出方法信息和出參了。
定義輸出結(jié)果方法;
public static void point(String methodName, Object response) { System.out.println("系統(tǒng)監(jiān)控 :: [方法名稱:" + methodName + " 輸出信息:" + JSON.toJSONString(response) + "]\r\n"); }
1
2
3
接下來我們使用字節(jié)碼增強的方式來調(diào)用這個靜態(tài)方法。
@Override protected void onMethodExit(int opcode) { if ((IRETURN <= opcode && opcode <= RETURN) || opcode == ATHROW) { ... mv.visitLdcInsn(className + "." + name); // 類名.方法名 mv.visitVarInsn(ALOAD, nextLocal); mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(MethodTest.class), "point", "(Ljava/lang/String;Ljava/lang/Object;)V", false); } }
1
2
3
4
5
6
7
8
9
10
mv.visitLdcInsn(className + “.” + name);,常量池中的常量值(int, float, string reference, object reference)入棧。也就是我們把類名和方法名,寫到常量池中。
mv.visitVarInsn(ALOAD, nextLocal);,將上面我們提到的返回值加載到操作數(shù)棧。
mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(MethodTest.class), “point”, “(Ljava/lang/String;Ljava/lang/Object;)V”, false);,調(diào)用靜態(tài)方法。INVOKESTATIC 是調(diào)用指令,后面是方法的地址、方法名、方法描述。
(Ljava/lang/String;Ljava/lang/Object;)V,表示 String 和 Object 類型的入?yún)ⅲ琕 是返回空。整體看也就是我們的方法;void point(String methodName, Object response)
再次被增強后的方法;
public Integer strToNumber(String str) { Integer var2 = Integer.parseInt(str); point("org.itstack.test.MethodTest.strToNumber", var2); return var2; }
1
2
3
4
5
在字節(jié)碼增強后的方法,每次調(diào)用這個方法都會輸出方法的名稱和出參結(jié)果??赡苓€有一個問題就是,如果拋異常了,那么就監(jiān)控不到了!
4. 給方法加上TryCatch
如果需要抓住方法的異常信息并輸出,那么就需要給原有的方法包上一層 TryCatch 捕獲異常。接下來我們開始完成這樣的指令碼操作。
添加 TryCatch 開始
private Label from = new Label(), to = new Label(), target = new Label(); @Override protected void onMethodEnter() { //標志:try塊開始位置 visitLabel(from); visitTryCatchBlock(from, to, target, "java/lang/Exception"); }
1
2
3
4
5
6
7
8
9
10
11
12
13
在 onMethodEnter() 中,加入 TryCatch 開始塊,在部分在 ASM 中固定的模式,按照需求添加即可。
添加 TryCatch 結(jié)尾
@Override public void visitMaxs(int maxStack, int maxLocals) { //標志:try塊結(jié)束 mv.visitLabel(to); //標志:catch塊開始位置 mv.visitLabel(target); mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Exception"}); // 異常信息保存到局部變量 int local = newLocal(Type.LONG_TYPE); mv.visitVarInsn(ASTORE, local); // 拋出異常 mv.visitVarInsn(ALOAD, local); mv.visitInsn(ATHROW); super.visitMaxs(maxStack, maxLocals); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在 visitMaxs 方法中完成 TryCatch 的結(jié)尾,包住異常請拋出。
mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Exception"});,在指定方法操作數(shù)棧中將 TryCatch 處理完成。這里面的幾個參數(shù)也可以動態(tài)拼裝;局部變量、參數(shù)、棧、異常。
ASTORE,將異常信息保存到局部變量,并使用指定 ALOAD 放到操作數(shù)棧,用于拋出。
ATHROW,最后是拋出異常的指令,也就是 throw var;
這次增強后的方法;
public Integer strToNumber(String str) { try { Integer var2 = Integer.parseInt(str); point("org.itstack.test.MethodTest.strToNumber", var2); return var2; } catch (Exception var3) { throw var3; } }
1
2
3
4
5
6
7
8
9
這時離我們要的內(nèi)容越來越近了,整個方法被包裝到一個 TryCatch 中,并按照需要輸出我們的信息。接下來就需要將異常信息,打印出來。
5. 輸出異常信息
在我們使用 ASM 字節(jié)碼增強后,已經(jīng)可以將方法拓展的非常的適合于監(jiān)控了。接下來我們定義一個靜態(tài)方法,用于輸出異常信息;
定義輸出異常方法;
public static void point(String methodName, Throwable throwable) { System.out.println("系統(tǒng)監(jiān)控 :: [方法名稱:" + methodName + " 異常信息:" + throwable.getMessage() + "]\r\n"); }
1
2
3
接下來的事情就很簡單了,只需要在拋出異常的指令中,把調(diào)用外部方法的內(nèi)容集成進去就可以了。
@Override public void visitMaxs(int maxStack, int maxLocals) { ... // 輸出信息 mv.visitLdcInsn(className + "." + name); // 類名.方法名 mv.visitVarInsn(ALOAD, local); mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(MethodTest.class), "point", "(Ljava/lang/String;Ljava/lang/Throwable;)V", false); ... }
1
2
3
4
5
6
7
8
9
10
這一部分主要體現(xiàn)將異常信息進行輸出,通過字節(jié)碼指令來實現(xiàn)調(diào)用外部方法。
mv.visitLdcInsn,加載常量。也就是類名和方法名。
ALOAD,將異常信息加載到操作數(shù)棧用,用于輸出。
INVOKESTATIC,調(diào)用靜態(tài)方法。調(diào)用方法除了這個指令外還有;invokespecial、invokevirtual、invokeinterface。
現(xiàn)在再看字節(jié)碼增強后的方法;
public Integer strToNumber(String str) { try { Integer var2 = Integer.parseInt(str); point("org.itstack.test.MethodTest.strToNumber", (Object)var2); return var2; } catch (Exception var3) { point("org.itstack.test.MethodTest.strToNumber", (Throwable)var3); throw var3; } }
1
2
3
4
5
6
7
8
9
10
好!到這我們已經(jīng)將這個方法徹底的通過字節(jié)碼改造完成,可以非常方便的監(jiān)控異常信息。對用外部輸出的方法,后續(xù)可以通過 MQ 等機制推送出去,用于圖表展示監(jiān)控信息。
五、測試驗證
這是一個字符串轉(zhuǎn)換成數(shù)字類型的方法,我們通過調(diào)用傳輸不同的參數(shù)進行驗證。比如;數(shù)字類型字符串和非數(shù)字類型字符串。
另外這里是我們通過字節(jié)碼增強的方式進行改造方法,改造后這個方法反饋給我們的仍然是字節(jié)碼,所以需要使用到 ClassLoader 進行加載到執(zhí)行。
測試方法;
public static void main(String[] args) throws Exception { // 方法字節(jié)碼增強 byte[] bytes = new MethodTest().getBytes(MethodTest.class.getName()); // 輸出方法新字節(jié)碼 outputClazz(bytes, MethodTest.class.getSimpleName()); // 測試方法 Class> clazz = new MethodTest().defineClass("org.itstack.test.MethodTest", bytes, 0, bytes.length); Method queryUserInfo = clazz.getMethod("strToNumber", String.class); // 正確入?yún)?;測試驗證結(jié)果輸出 Object obj01 = queryUserInfo.invoke(clazz.newInstance(), "123"); System.out.println("01 測試結(jié)果:" + obj01); // 異常入?yún)?;測試驗證打印異常信息 Object obj02 = queryUserInfo.invoke(clazz.newInstance(), "abc"); System.out.println("02 測試結(jié)果:" + obj02); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
輸出結(jié)果;
ASM字節(jié)碼增強后類輸出路徑:/User/itstack/git/github.com/WormholePistachio/SQM/target/test-classes/MethodTestSQM.class 系統(tǒng)監(jiān)控 :: [方法名稱:org.itstack.test.MethodTest.strToNumber 輸出信息:123] 01 測試結(jié)果:123 系統(tǒng)監(jiān)控 :: [方法名稱:org.itstack.test.MethodTest.strToNumber 異常信息:For input string: "abc"] Process finished with exit code 1
1
2
3
4
5
6
7
8
六、總結(jié)
通過字節(jié)碼指令控制代碼的編寫注入,是不是很酷?完成功能的同時,逐步也解了 JVM虛擬機 。至少不向以前那樣只是去硬背一些理論,而是徹底的實踐了。不要感覺這很難,嗯!
在逐步的了解字節(jié)碼編程后,你會在很多的場景領(lǐng)域中建設出高級的玩法。甚至去翻看源碼也能更加容易閱讀理解,并把這技巧復用給自己其他系統(tǒng)。
比如我們常用的非入侵的監(jiān)控系統(tǒng),全鏈路監(jiān)控,以及一些反射框架中,其實都用到了 ASM,只是還沒有注意到而已。最終多學習一些延申拓展的知識,關(guān)于這些技巧可以閱讀 JVM虛擬機規(guī)范,也可以閱讀ASM文檔;asm.itstack.org
七、彩蛋
最近將個人原創(chuàng)代碼庫資源整理出一份 wiki 文檔,同時逐步將各類案例匯總集中,方便獲取。
鏈接:https://github.com/fuzhengwei/CodeGuide/wiki
JVM
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。