ASM給方法加try catch

期望通過註解的形式,給方法套上try catch。老樣子,先看一下try catch的字節碼。

CatchUtil

public class CatchUtil {
    private void handle() {
        try {
            Date date = new Date();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

找到編譯後的class文件,使用插件ASM Bytecode Viewer

 private handle()V
    TRYCATCHBLOCK L0 L1 L2 java/lang/Exception
   L0
    LINENUMBER 8 L0
    NEW java/util/Date
    DUP
    INVOKESPECIAL java/util/Date.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 12 L1
    GOTO L3
   L2
    LINENUMBER 9 L2
   FRAME SAME1 java/lang/Exception
    ASTORE 1
   L4
    LINENUMBER 10 L4
    ALOAD 1
    INVOKEVIRTUAL java/lang/Exception.printStackTrace ()V
   L3
    LINENUMBER 13 L3
   FRAME SAME
    RETURN

重點就是TRYCATCHBLOCK L0 L1 L2 java/lang/Exceptionlabel L0是try塊的起始點,label L1是終點。方法正常調用不出異常的情況下try塊走完GOTO到L3,return結束方法;發生異常到異常處理處L2,將異常加入局部變量表ASTORE,然後ALOAD異常,調用catch塊中printStackTrace()打印堆棧。

所以我們需要在方法開始時插入TRYCATCHBLOCK,異常處理標籤L2後插入printStackTrace()

先弄個註解Catch,只插入註解方法。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Catch {
}

CatchMethodVisitor在方法前插入TRYCATCHBLOCK。

object CatchMethodVisitor {

    operator fun invoke(
            mv: MethodVisitor,
            access: Int,
            name: String?,
            descriptor: String?,
    ): MethodVisitor {
        return object : AdviceAdapter(Opcodes.ASM9, mv, access, name, descriptor) {
            private var needTrack = false
            private val start = Label()
            private val end = Label()
            private val catch = Label()

            override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
                if (Type.getDescriptor(Catch::class.java) == descriptor) needTrack = true
                return super.visitAnnotation(descriptor, visible)
            }

            override fun onMethodEnter() {
                if (needTrack) {
                    visitLabel(start)
                    visitTryCatchBlock(start, end, catch, "java/lang/Exception")
                }
                super.onMethodEnter()
            }
        }
    }
}

重寫visitMaxs()方法,在此之前插入catch塊。

            override fun visitMaxs(maxStack: Int, maxLocals: Int) {
                if (needTrack) {
                    mv.visitLabel(end)
                    mv.visitLabel(catch)
                    mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, arrayOf<Any>("java/lang/Exception"))
                    val local = newLocal(Type.LONG_TYPE);
                    mv.visitVarInsn(ASTORE, local)
                    mv.visitVarInsn(ALOAD, local)
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Exception", "printStackTrace", "()V", false)
                }
                super.visitMaxs(maxStack, maxLocals)
            }

try塊結束標籤end,異常處理標籤catch,然後打印堆棧。

看起來很美好,跑個測試用例。

    @Catch
    private fun catchVoid() {
        val date = Date()
    }

    @Catch
    private fun catchObject() = Date()

找到transform中處理過的class文件,反編譯成Java。

   @Catch
   private final void catchVoid() {
      try {
         new Date();
      } catch (Exception var3) {
         var3.printStackTrace();
      }
   }

   @Catch
   private final Date catchObject() {
      try {
         return new Date();
      } catch (Exception var2) {
         var2.printStackTrace();
      }
   }

嗯哼~原方法中有返回值就有問題了,插入try catch後方法結尾沒有返回值報錯。這裏爲了方便在catch塊中將異常throw出去,當然也可以給方法返回一個默認值。

            override fun visitMaxs(maxStack: Int, maxLocals: Int) {
                if (needTrack) {
                    mv.visitLabel(end)
                    mv.visitLabel(catch)
                    mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, arrayOf<Any>("java/lang/Exception"))
                    val local = newLocal(Type.LONG_TYPE);
                    mv.visitVarInsn(ASTORE, local)
                    mv.visitVarInsn(ALOAD, local)
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Exception", "printStackTrace", "()V", false)
                    mv.visitVarInsn(ALOAD, local)
                    mv.visitInsn(ATHROW)
                }
                super.visitMaxs(maxStack, maxLocals)
            }

簡單的加個throw拋出異常,再跑一下測試用例。

   @Catch
   private final void catchVoid() {
      try {
         new Date();
      } catch (Exception var3) {
         var3.printStackTrace();
         throw var3;
      }
   }

   @Catch
   private final Date catchObject() {
      try {
         return new Date();
      } catch (Exception var2) {
         var2.printStackTrace();
         throw var2;
      }
   }

好起來了,個人感覺在catch塊中處理異常後繼續throw出去比較好,畢竟崩潰後定位問題也比較容易。返回默認值雖然能保證不崩潰,但業務邏輯可能產生其它奇奇怪怪的問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章