ASM(三) 利用Method組件動態生成方法的字節碼

 一、概述

      ASMCoreApi中還提供了對class中方法的生成和解析的組件。前面兩篇着重介紹了ClassVisitor組件的應用場景。ClassVisitor Api中的visitMethod(int access, String name, String desc, String signature, String[] exceptions)方法返回了一個MethodVisitor對象,MethodVisitor類提供了對於字節碼文件中方法的字節碼進行解析。同ClassVisitor一樣,MethodVisitor的實例方法也需要按照一定的順序調用。

visitAnnotationDefault?

( visitAnnotation |visitParameterAnnotation|visitAttribute)*

( visitCode

( visitTryCatchBlock |visitLabel|visitFrame|visitXxxInsn|

visitLocalVariable|visitLineNumber)*

visitMaxs)?

visitEnd

     對於動態生成方法的實現,ASM提供的主要接口就是MethodVisitorMethods組件主要涉及到一下三個類:

     1ClassReader類負責解析字節碼中的method部分,並且順序調用MethodVisitor的方法。其中MethodVisitor是通過ClassVisitorvisitMethod返回的對象。而ClassVisitor是通過accept方法的參數形式轉遞給ClassReaderaccept方法在前面章介紹ASMcore apiClassVisitor部分已經介紹過了。

    2ClassWritervisitMethod方法返回了繼承自MethodVisitor的實例。並且以二進制數組的形式構建了編譯後的方法。

    3MethodVisitor類似一個事件過濾器,可以接受另一個MethodVisitor實例,代理調用所有這個MethodVisitor實例的方法。這個也是類似於前面兩章介紹的ClassVisitor

  下面就開始介紹下用ASMMethodVisitor如何生成和變更、轉移methods字節碼。

二、生成字節碼methods

 

     在瞭解ASMmethods api之前,必須要清楚一些字節碼的指令和JVM執行引擎在運行時數據區是運用字節碼指令解釋執行的。這裏可以參看JVM字節碼執行模型及字節碼指令集 以及JVM 字節碼指令對於棧幀數據操作舉例。這裏我們結合JVM 字節碼指令對於棧幀數據操作舉例中的例子。生成如下方法:

 public void addEspresso(int espresso) {    
	   if (espresso > 1) {    
	            this.espresso = espresso;    
	        } else {    
	            throw new IllegalArgumentException();    
	        }    
	    }  

   爲了方便查看字節碼生成效果,我們通過ClassWriter來輸出字節數組,並寫到文件中,方便查看class文件。對於ClassWriter Api參考ASM(二) 利用Core API 變更類成員。Java代碼如下:

    

package asm.core.asm.core.method;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * methods api 動態生成字節碼 Created by yunshen.ljy on 2015/6/24.
 */
public class GenerateClasses {

    public static void main(String[] args) throws IOException {

        ClassWriter cw = new ClassWriter(0);
        cw.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "bytecode/MethodGenClass", null, "java/lang/Object", null);
        cw.visitField(Opcodes.ACC_PRIVATE, "espresso", "I", null, null).visitEnd();
        MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "addEspresso", "(I)V", null, null);
        // 方法訪問開始
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ILOAD, 1);
        // label 代表跳轉的字節碼位置。
        Label label = new Label();
        mv.visitJumpInsn(Opcodes.IFLT, label);
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitVarInsn(Opcodes.ILOAD, 1);
        mv.visitFieldInsn(Opcodes.PUTFIELD, "bytecode/MethodGenClass", "espresso", "I");
        Label end = new Label();
        mv.visitJumpInsn(Opcodes.GOTO, end);
        mv.visitLabel(label);
        mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
        // 創建Exception對象指令
        mv.visitTypeInsn(Opcodes.NEW, "java/lang/IllegalArgumentException");
        mv.visitInsn(Opcodes.DUP);
        // 調用方法指令
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/IllegalArgumentException", "<init>", "()V", false);
        mv.visitInsn(Opcodes.ATHROW);
        mv.visitLabel(end);
        mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(2, 2);
        // 方法訪問結束
        mv.visitEnd();
        cw.visitEnd();

        byte[] b = cw.toByteArray();

        File file = new File("MethodGenClass.class");
        FileOutputStream fout = new FileOutputStream(file);
        fout.write(b);
        fout.close();
    }

}

 

      這裏先說明一下棧圖(Stack Map Table)的概念。詳見:JVM StackMapTable 屬性的作用及理解  。

     Java7版本之後,需要強制實現棧圖結構,不過還好ASM框架給我們處理了生成字節碼棧圖的細節。構建棧圖在ASM框架中,可以通過在無條件跳轉語句後調用mv.visitFrame();方法或者調整ClassWriter()構造器的參數方式來實現,ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);,構造器中的COMPUTE_FRAMES參數會爲我們計算Stack Map Table中的frame。本例中,我們採用“手動擋”。visitFrame(type, nLocal, local, nStack, stack)的第一個參數是stack map frame的操作類型,nLocalnStack是局部變量和操作數棧的sizeLocalstack是包含相關類型的數組。本例中,只有兩個frame,且記錄的狀態中操作數棧都是空的,所以參數是(Opcodes.F_SAME, 0, null, 0, null)

      這裏再額外說明一下ClassWriter構造器的另外一種參數。如果我們使用了ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);那麼COMPUTE_MAXS就是替我們計算局部變量表和操作數棧的大小,這時候我們還是需要調用  mv.visitMaxs(2, 2);方法,但是可以傳遞任意參數,因爲COMPUTE_MAXS的方式會幫助我們重新計算局部變量和操作數的size。還需要注意的是COMPUTE_MAXS不會爲我們計算StackMapFrame,而COMPUTE_FRAMES既會計算棧size,也會計算StackMapFrame,當然使用COMPUTE_MAXS會讓ClassWriter10%,而使用COMPUTE_FRAMES會慢20%(數據來自ASM官方說明文檔,這裏就不再測試)。當然,我覺得,如果使用起來的話,我還是寧可用自動擋。這樣可以適當增強代碼可讀性,減少了代碼維護成本。如果有很好的算法(如果你確定你比ASM框架寫出了更好的實現算法)來計算這些參數,當然手動擋也是一種樂趣。

      上述實現中我們看到了Label label = new Label()這個語句中,label的作用是爲了條件跳轉,其實也可以理解成字節碼指令的參數。所以label必須對應一條字節碼指令,通過visitLabel(label)來調用,並且visitLabel的調用必須緊跟隨着label對象指定的指令。如例子中,第一個label指向goto後,所以順序必須是:mv.visitJumpInsn(Opcodes.GOTO, end);

        mv.visitLabel(label);,第二個label也是同理。但是多個字節碼指令可能指向同一個label,比如同時跳轉到某一個指令執行的情況,並且label是方法獨有的,label對象不能跨方法調用。

      下面我們再看一下,例中方法生成的字節碼信息。

     

   
     好了,我好餓,先寫到這裏,後續會繼續介紹Method Api 的另外一種用法,其實猜猜也知道,同ClassVisitor一樣,不僅可以生成字節碼,當然也可以動態改變、添加、刪除方法的字節碼。

 

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