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...");
}
需要做什麼
針對我們的期望結果我們需要做以下三點
- 增加一個doBcelPlan方法
- 在doCoding方法中呼叫doBcelPlan方法
- 在常量池中加入方法的宣告,常量等其它使用到的變數和方法。
工程先引入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型別
方法返回型別描述參考
3. 在常量池中增加一個String型別的Filed
因為有System.out.println("doBcelPlan")語句
doBcelPlan中的System.out 變數和println方法再doCoding中已經使用所有已經在常量池中了
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 結果
其中17,25都是常量池的引用參見下圖,將原先的Programmer類編譯後使用javap -versobse XXX.class 可以檢視常量池資訊。
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");
可以看到經過編輯後的Class檔案輸出結果同我們預期的是一樣的
Done!
from: https://yq.aliyun.com/articles/7243?spm=5176.100239.blogcont7241.37.db8GKF