1. 程式人生 > >JVM Class位元組碼之三-使用BCEL改變類屬性

JVM Class位元組碼之三-使用BCEL改變類屬性

使用BCEL動態改變Class內容

之前對Class檔案中的常量池,Method的位元組碼指令進行了說明。
JVM Class詳解之一
JVM Class詳解之二 Method位元組碼指令
現在我們開始實際動手,使用BCEL改變位元組碼指令,對Class檔案進行功能擴充。

先介紹下BCEL全程Apache Byte Code Engineering Library,BCEL 每項內容操作在JVM組合語言的級別

HelloWorld搞起

這個case我們需要給Programmer類做功能擴充套件,Programmer 職責進行了變化,除了要Coding以外,在每次Coding之前需要先做Plan,所以需要在do Coding資訊輸出之前輸出 "doBcelPlan..." 資訊。
Demo

public class Programmer implements Person {

    @Override
    public void doCoding() {
        System.out.println("do Coding...");
    }

}

期望效果

    @Override
    public void doCoding() {
         doPlan();
         System.out.println("do Coding...");
    }

    private void doPlan() {
         System.out
.println("do Plan..."); }

需要做什麼

針對我們的期望結果我們需要做以下三點

  1. 增加一個doBcelPlan方法
  2. 在doCoding方法中呼叫doBcelPlan方法
  3. 在常量池中加入方法的宣告,常量等其它使用到的變數和方法。

工程先引入BCEL的依賴Pom中追加即可

        <dependency>
            <groupId>asm</groupId>
            <artifactId>asm</artifactId>
            <version>
3.1</version> </dependency> <dependency> <groupId>asm</groupId> <artifactId>asm-tree</artifactId> <version>3.1</version> </dependency>

1. 先使用BCEL 載入需要編輯的Class

        JavaClass clazz = Repository.lookupClass(Programmer.class);
        ClassGen classGen = new ClassGen(clazz);
        ConstantPoolGen cPoolGen = classGen.getConstantPool(); // 常量池資訊

2. 在常量池中增加一個MethodRef doBcelPlan

    int methodIndex = cPoolGen.addMethodref("byteCode.decorator.Programmer", "doBcelPlan", "()V");    // 在常量池中增加一個方法的宣告返回methodIndex為宣告在常量池中的位置索引

第一個引數的去路徑類名
第二個引數是方法名稱
第三個方法返回型別 ()V 是void型別
方法返回型別描述參考
screenshot

3. 在常量池中增加一個String型別的Filed

因為有System.out.println("doBcelPlan")語句 
doBcelPlan中的System.out 變數和println方法再doCoding中已經使用所有已經在常量池中了
screenshot

    int stringIndex = cPoolGen.addString("doBcelPlan...");// 在常量池中增加一個Field的宣告返回stringIndex為宣告在常量池中的位置索引

注意這裡需要記錄追加方法和Filed的index後面需要使用。

4. 然後建立doBcelPlan方法的實體的位元組碼指令

呼叫System.out變數和println方法 具體的位元組碼指令引數 上一節內容有說明 參考上一節文件 JVM Class詳解之二 Method位元組碼指令

InstructionList instructionDoPlan = new InstructionList();  // 位元組碼指令資訊 
instructionDoPlan.append(new GETSTATIC(17));  // 獲取System.out常量
instructionDoPlan.append(new LDC(stringIndex));  // 獲取String Field資訊
instructionDoPlan.append(new INVOKEVIRTUAL(25)); // 呼叫Println方法
instructionDoPlan.append(new RETURN());    // return 結果

screenshot
其中17,25都是常量池的引用參見下圖,將原先的Programmer類編譯後使用javap -versobse XXX.class 可以檢視常量池資訊。
screenshot

stringIndex 是引用第三步追加常量池String Field soBcelPlan

5. 生成doBcelPlan方法

MethodGen doPlanMethodGen = new MethodGen(1, Type.VOID, Type.NO_ARGS, null, "doBcelPlan",
classGen.getClassName(), instructionDoPlan, cPoolGen);
classGen.addMethod(doPlanMethodGen.getMethod());

方法的宣告並追加到classGen中。
這樣doBcelPlan方法就追加成功了。接下來我們需要找到doCoding方法,在方法中追加doBcelPlan的呼叫。

6. 找到並修正doCoding方法

        Method[] methods = classGen.getMethods();
        for (Method method : methods) {
            String methodName = method.getName();
            if ("doCoding".equals(methodName)) {
                MethodGen methodGen = new MethodGen(method, clazz.getClassName(), cPoolGen);
                InstructionList instructionList = methodGen.getInstructionList();
                InstructionHandle[] handles = instructionList.getInstructionHandles();
                InstructionHandle from = handles[0];
                InstructionHandle aload = instructionList.append(from, new ALOAD(0));
                instructionList.append(aload, new INVOKESPECIAL(methodIndex));
                classGen.replaceMethod(method, methodGen.getMethod());
            }
        }

InstructionList 是當前方法中的位元組碼指令,我們append了兩個指令ALOAD和INVOKESPECIAL。實現doBcelPlan的呼叫。

7. 將編輯後的Class輸出

        JavaClass target = classGen.getJavaClass();
        target.dump("D:\\AliDrive\\bytecode\\bcel\\Programmer.class");

將修改後的位元組碼輸出來看下,使用JD開啟OK
screenshot

可以看到經過編輯後的Class檔案輸出結果同我們預期的是一樣的
Done!

from: https://yq.aliyun.com/articles/7243?spm=5176.100239.blogcont7241.37.db8GKF