簡體   English   中英

在 try-finally 塊中嵌入方法的現有代碼 (2)

[英]Embed the existing code of a method in a try-finally block (2)

前段時間,我在將方法的現有代碼嵌入 try-finally 塊中詢問如何使用 ASM 將方法的主體包裝在 try-finally 塊中。 解決方案是在visitCode()方法主體的開頭訪問 try 塊的標簽,並在訪問visitInsn()帶有返回操作碼的指令時完成 try-finally 塊。 我知道如果一個方法沒有返回指令,則該解決方案將不起作用,如果該方法總是以異常方式離開則適用。

不過,我發現前一種解決方案有時也不適用於帶有返回指令的方法。 如果一個方法有多個返回指令,它將不起作用。 原因是它生成了無效的字節碼,因為在方法的開頭添加了一個 try-finally 塊,但完成了不止一個 try-finally 塊。

通常(但可能取決於 javac 編譯器),字節碼方法包含單個返回指令,並且所有返回路徑通過跳轉到該指令結束。 但是,使用 Eclipse 編譯以下代碼將導致帶有兩個返回指令的字節碼:

public boolean isEven(int x) {
  return x % 2 == 0;
}

用Eclipse編譯的字節碼:

   0: iload_1
   1: iconst_2
   2: irem
   3: ifne          8
   6: iconst_1
   7: ireturn       // javac compilation: goto 9
   8: iconst_0
   9: ireturn

因此,我想知道包裝方法代碼的整個代碼的正確方法是什么。

您必須回溯 Java 編譯器在編譯try … finally …時所做的事情try … finally …這意味着將您的finally操作復制到將保留受保護(源)代碼塊的每個點(即返回指令)並安裝多個受保護(結果字節碼)區域(因為它們不應該覆蓋您的finally操作)但它們可能都指向相同的異常處理程序。 或者,您可以轉換代碼,將所有返回指令替換為“之后”操作的一個實例的分支,然后是唯一的返回指令。

這不是小事。 因此,如果您不需要通常不支持向加載的類添加方法的熱代碼替換,那么避免這一切的最簡單方法是將原始方法重命名為不與其他方法沖突的名稱(您可以使用不允許的字符)在普通源代碼中)並使用舊名稱和簽名創建一個新方法,該方法包含一個簡單的try … finally …構造,其中包含對重命名方法的調用。

例如,將public void desired()更改為private void desired$instrumented()並添加一個新的

public void desired() {
    //some logging X

    try {
        desired$instrumented();
    }
    finally {
        //some logging Y
    }
}

請注意,由於調試信息保留在重命名的方法中,如果在重命名的方法中拋出異常,堆棧跟蹤將繼續報告正確的行號。 如果你只是通過添加一個不可見的字符來重命名它(請記住,你在字節碼級別有更多的自由),它會非常流暢。

感謝 Holger 的回答和 Antimony 的評論,我開發了以下滿足我需求的解決方案。 后來我發現在Using ASM framework to implement common bytecode transformation patterns , E. Kuleshov, AOSD.07, March 2007, Vancouver, Canada 中也描述了類似的方法。

此解決方案不適用於不包含非異常返回的方法(在每個執行路徑中拋出異常的方法,例如throw new NotSupportedOperationException(); )!

如果您還需要支持這些方法,您應該按照 Holger 的建議重命名原始方法,然后添加具有舊名稱的新方法。 將添加的方法中的委托調用添加到重命名的方法中,並將調用嵌入到 try-finally 塊中。


我使用一個簡單的MethodVisitor來訪問代碼。 visitCode()方法中,我添加了進入方法時要執行的指令。 然后,我通過訪問一個新的Label來添加 try 塊的開頭。 當我在visitInsn()訪問返回操作碼時,我將完成 try 塊並添加 finally 塊。 此外,我添加了一個新的Label來開始一個新的 try 塊,以防該方法包含進一步的返回指令。 (如果沒有退貨說明,則標簽訪問將沒有任何影響。)

簡化后的代碼如下:

public abstract class AbstractTryFinallyMethodVisitor extends MethodVisitor {

  private Label m_currentBeginLabel;
  private boolean m_isInOriginalCode = true;

  protected void execBeforeMethodCode() {
    // Code at the beginning of the method and not in a try block
  }

  protected void execVisitTryBlockBegin() {
    // Code at the beginning of each try block
  }

  protected void execVisitFinallyBlock() {
    // Code in each finally block
  }

  @Override
  public void visitCode() {
    try {
      m_isInOriginalCode = false;
      execBeforeMethodCode();
      beginTryFinallyBlock();
    }
    finally {
      m_isInOriginalCode = true;
    }
  }

  protected void beginTryFinallyBlock() {
    m_currentBeginLabel = new Label();
    visitLabel(m_currentBeginLabel);
    execVisitTryBlockBegin();
  }

  @Override
  public void visitInsn(int opcode) {
    if (m_inOriginalCode && isReturnOpcode(opcode) {
      try {
        m_isInOriginalCode = false;
        completeTryFinallyBlock();

        super.visitInsn(opcode);

        beginTryBlock();
      }
      finally {
        m_isInOriginalCode = true;
      }
    }
    else {
      super.visitInsn(opcode);
    }
  }

  protected void completeTryFinallyBlock() {
    Label l1 = new Label();
    visitTryCatchBlock(m_currentBeginLabel, l1, l1, null);
    Label l2 = new Label();
    visitJumpInsn(GOTO, l2);
    visitLabel(l1);
    // visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] { "java/lang/Throwable" });
    visitVarInsn(ASTORE, 1);

    execVisitFinallyBlock();

    visitVarInsn(ALOAD, 1);
    super.visitInsn(ATHROW);
    visitLabel(l2);
    // visitFrame(Opcodes.F_SAME, 0, null, 0, null);

    execVisitFinallyBlock();
  }

   protected static boolean isReturnOpcode(int opcode) {
     return opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN;
   }
}

筆記:

  • 如果使用COMPUTE_FRAMES標志實例化ClassWriter ,則COMPUTE_FRAMES調用visitFrame
  • 也可以(並且可能更可取)使用AdviceAdapter並在其onMethodEnter()onMethodExit()方法中執行字節碼操作。
  • 如前所述,僅當字節碼包含至少一個返回指令時才會添加 try-finally 塊。

問題的isEven()方法的轉換字節碼輸出將是:

public boolean isEven(int);
Code:
 0: ldc           #22                 //isEven(int)
 2: invokestatic  #28                 //log/Logger.push:(Ljava/lang/String;)V
 5: iload_1                  *1*
 6: iconst_2                 *1*  
 7: irem                     *1*
 8: ifne          25         *1*
11: iconst_1                 *1*
12: goto          21         *1*
15: astore_1
16: invokestatic  #31                 //log/Logger.pop:()V
19: aload_1            
20: athrow
21: invokestatic  #31                 //log/Logger.pop:()V
24: ireturn
25: iconst_0                 *2*
26: goto          35         *2*
29: astore_1
30: invokestatic  #31                 //log/Logger.pop:()V
33: aload_1
34: athrow
35: invokestatic  #31                 //log/Logger.pop:()V
38: ireturn

Exception table:
 from    to  target type
     5    15    15   any     *1*
    25    29    29   any     *2*
}

不可能將整個構造函數包裝到try-finally 塊中,因為try 塊不能跨越對超級構造函數的調用。 雖然我在規范中找不到這個限制,但我可以找到兩張討論它的票: JDK-8172282asm #317583

如果您不關心構造函數,您可以將方法包裝到其他方法中,這些方法可以捕獲 Holger 編寫的異常 這是一個簡單的解決方案,在許多情況下可能都很好。 但是,此答案描述了不需要生成第二種方法的替代解決方案。


解決方案大致基於JVM規范中的“最終編譯” 該解決方案使用JSR指令。 從語言級別 7 開始不支持該指令。因此,我們使用JSRInlinerAdapter之后替換指令。

我們將從創建我們自己的MethodVisitor 請注意,我們擴展MethodNode而不是MethodVisitor 我們這樣做是為了在將信息傳遞給下一個訪問者之前收集整個方法。 稍后再談。

public class MyMethodVisitor extends MethodNode {

訪問者需要三個標簽。 第一個標簽指定原始內容的開始和try 塊的開始。 第二個標簽指定原始內容的結尾和try 塊的結尾。 它還指定異常處理程序的開始。 最后一個標簽指定代表finally 塊的子程序。

  private final Label originalContentBegin = new Label();
  private final Label originalContentEnd = new Label();
  private final Label finallySubroutine = new Label();

構造函數重用MethodVisitor的字段mv MethodNode不使用它。 我們也可以創建自己的領域。 構造函數還創建了JSRInlinerAdapter來替換上面提到的JSR指令。

  public MyMethodVisitor(
      MethodVisitor methodVisitor,
      int access, String name, String descriptor,
      String signature, String[] exceptions)
  {
    super(Opcodes.ASM8, access, name, descriptor, signature, exceptions);
    mv = new JSRInlinerAdapter(methodVisitor, access, name, descriptor, signature, exceptions);
  }

接下來,我們聲明生成字節碼的方法,這些方法將在執行原始代碼之前和之后執行。

  protected void generateBefore() { /* Generate your code here */ }
  protected void generateAfter() { /* Generate your code here */ }

根據MethodVisitor的 JavadocASM調用

  • 在訪問方法的內容之前訪問visitCode() ,以及
  • visitMaxs(int,int)方法的內容被訪問后。

ASM訪問方法的內容之前,我們要注入我們自己的字節碼並訪問我們的標簽,它指定了原始內容的開頭。

  @Override
  public void visitCode() {
    super.visitCode();
    generateBefore();
    super.visitLabel(originalContentBegin);
  }

每當原始方法返回時,我們都希望調用 finally 塊的代碼。

  @Override
  public void visitInsn(int opcode) {
    if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
      super.visitJumpInsn(Opcodes.JSR, finallySubroutine);
    }
    super.visitInsn(opcode);
  }

在該方法的末尾,我們為try 塊和包含finally 塊的子例程注入異常處理程序。

  @Override
  public void visitMaxs(int maxStack, int maxLocals) {
    super.visitLabel(originalContentEnd);
    super.visitJumpInsn(Opcodes.JSR, finallySubroutine);
    super.visitInsn(Opcodes.ATHROW);

    super.visitLabel(finallySubroutine);
    super.visitVarInsn(Opcodes.ASTORE, 0);
    generateAfter();
    super.visitVarInsn(Opcodes.RET, 0);

    super.visitMaxs(maxStack, maxLocals);
  }

最后,我們必須創建try-catch 塊並將方法轉發給下一個方法訪問者。 由於visitTryCatchBlock(…)調用的順序不利,我們無法更早地使用訪問者模式創建try-catch 塊(請參閱問題 #317617 )。 這就是我們擴展MethodNode而不是MethodVisitor

  @Override
  public void visitEnd() {
    super.visitEnd();
    tryCatchBlocks.add(new TryCatchBlockNode(
        getLabelNode(originalContentBegin),
        getLabelNode(originalContentEnd),
        getLabelNode(originalContentEnd),
        null));
    accept(mv);
  }
}

由於轉換不適用於構造函數,我們的方法訪問者可以像這樣在ClassVisitor

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
  if (name.equals("<init>")) {
    return super.visitMethod(access, name, descriptor, signature, exceptions);
  }
  else {
    return new MyMethodVisitor(
        super.visitMethod(access, name, descriptor, signature, exceptions),
        access, name, descriptor, signature, exceptions);
  }
}

還有一些改進的空間。

  • 您可以避免使用JSR指令並刪除JSRInlinerAdapter 這也可能提供一些機會來減少生成的代碼的大小,因為JSRInlinerAdapter可能會JSRInlinerAdapter復制finally 塊的代碼。

  • 即使您無法捕獲超級構造函數的異常,您也可以為在調用超級構造函數之前和之后處理異常的構造函數添加有限的支持。

無論如何,這種更改也可能會使代碼變得更加復雜。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM