ASM字節(jié)碼編程 | 用字節(jié)碼增強技術(shù)給所有方法加上TryCatch捕獲異常并輸出

      網(wǎng)友投稿 976 2025-04-02

      沉淀、分享、成長,讓自己和他人都能有所收獲

      一、前言

      你開發(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)。

      ASM字節(jié)碼編程 | 用字節(jié)碼增強技術(shù)給所有方法加上TryCatch捕獲異常并輸出

      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)容。

      上一篇:excel中制作圖表方法
      下一篇:雙贏的談判技巧
      相關(guān)文章
      亚洲最大中文字幕| 亚洲一卡2卡三卡4卡有限公司| 亚洲国产精品综合久久2007| 亚洲成AV人片在线观看无| 国产亚洲精品久久久久秋霞| 国产美女亚洲精品久久久综合| 亚洲AV成人精品一区二区三区| 亚洲午夜成人精品无码色欲| 中国亚洲呦女专区| 亚洲va久久久久| 亚洲色欲色欲www| 国产午夜亚洲精品| 亚洲粉嫩美白在线| 亚洲午夜精品久久久久久app| 亚洲国产精品无码久久九九大片| 亚洲中文字幕一二三四区苍井空| 亚洲国产日韩精品| 亚洲Av无码国产一区二区| 无码色偷偷亚洲国内自拍| 亚洲国产成人影院播放| 亚洲日韩在线第一页| 亚洲精品午夜国产VA久久成人| 亚洲欧洲日产国码av系列天堂| 久久精品国产亚洲沈樵| 亚洲国产精品第一区二区 | 中文字幕亚洲免费无线观看日本| 亚洲人成依人成综合网| 亚洲特级aaaaaa毛片| 亚洲AV一二三区成人影片| 亚洲色一区二区三区四区| 国产综合成人亚洲区| 久久亚洲AV无码西西人体| 亚洲精品成人片在线观看精品字幕| 亚洲精品乱码久久久久久自慰| 久久精品亚洲中文字幕无码网站| 亚洲永久永久永久永久永久精品| 亚洲日韩中文字幕天堂不卡| 亚洲中文无码卡通动漫野外| 亚洲av成人一区二区三区在线观看| 亚洲一级片内射网站在线观看| 亚洲高清国产AV拍精品青青草原|