在上一篇文章中,我們介紹了通過生成新方法來調用原方法的方式實現字節碼增強。本文將進一步探討如何直接在原方法上插入指令,這種方式更加靈活,也更貼近實際 AOP 開發場景。
我們以一個簡單的 StringBuilder 類為例,展示修改前後的對比:
public class StringBuilder {
private String buildString(int length) {
String result = "";
for (int i = 0; i < length; i++) {
result += (char)(i%26 + 'a');
}
System.out.println(result);
return result;
}
// ... main 方法省略
}
public class StringBuilder {
private String buildString(int length) {
System.out.println("start start start");
long starttime = System.currentTimeMillis();
String result = "";
for (int i = 0; i < length; i++) {
result += (char)(i%26 + 'a');
}
System.out.println(result);
System.out.println("Call to buildString$impl took " +
(System.currentTimeMillis()-starttime) + " ms.");
System.out.println("end end end end");
return result;
}
// ... main 方法省略
}
核心需求是在方法入口插入計時開始代碼,在返回前插入計時結束並輸出耗時的代碼。
以下是完整的 BCELTiming2 類,演示如何操作字節碼:
import java.io.FileOutputStream;
import java.util.Iterator;
import org.apache.bcel.Constants;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.LineNumberTable;
import org.apache.bcel.classfile.LocalVariableTable;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.ClassGen;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.InstructionConstants;
import org.apache.bcel.generic.InstructionFactory;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.InvokeInstruction;
import org.apache.bcel.generic.LocalVariableGen;
import org.apache.bcel.generic.MethodGen;
import org.apache.bcel.generic.ObjectType;
import org.apache.bcel.generic.PUSH;
import org.apache.bcel.generic.Type;
public class BCELTiming2 {
private static void modifyWrapper(ClassGen cgen, Method method) {
InstructionFactory ifact = new InstructionFactory(cgen);
ConstantPoolGen pgen = cgen.getConstantPool();
String cname = cgen.getClassName();
// 將原方法包裝為 MethodGen 以便操作
MethodGen wrapgen = new MethodGen(method, cname, pgen);
// 先移除原方法,稍後重新添加修改後的版本
cgen.removeMethod(method);
// 獲取指令列表
InstructionList ilist = wrapgen.getInstructionList();
// 計算局部變量表槽位,非靜態方法 slot 0 存 this
Type returnType = wrapgen.getReturnType();
Type[] types = wrapgen.getArgumentTypes();
int slot = wrapgen.isStatic() ? 0 : 1;
for (int i = 0; i < types.length; i++) {
slot += types[i].getSize(); // long/double 佔 2 個槽位
}
// 自動分配新局部變量索引(用於存儲 starttime)
LocalVariableGen lvg = wrapgen.addLocalVariable("starttime", Type.LONG, null, null);
// ========== 1. 在方法入口插入計時開始代碼 ==========
InstructionList printlnList = ifact.createPrintln("start test start test start test");
InvokeInstruction invokestatic = ifact.createInvoke(
"java.lang.System", "currentTimeMillis", Type.LONG, Type.NO_ARGS, Constants.INVOKESTATIC);
InstructionHandle firstHandle = printlnList.append(invokestatic);
// 將當前時間存入新分配的局部變量
printlnList.append(InstructionFactory.createStore(Type.LONG, lvg.getIndex()));
// 插入到方法最開始
ilist.insert(printlnList);
// ========== 2. 在方法返回前插入計時結束代碼 ==========
InstructionHandle insertPosition = null;
if (returnType.getType() != Type.VOID.getType()) {
// 有返回值:插在最後一條 return 指令之前
insertPosition = ilist.getEnd().getPrev();
} else {
// void 方法:直接插在最後
insertPosition = ilist.getEnd();
}
InstructionList tempList = new InstructionList();
// 獲取 System.out
tempList.append(ifact.createFieldAccess("java.lang.System", "out",
new ObjectType("java.io.PrintStream"), Constants.GETSTATIC));
tempList.append(InstructionFactory.DUP);
tempList.append(InstructionFactory.DUP);
// 輸出 "Call to method xxx took "
String text = "Call to method " + wrapgen.getName() + " took ";
tempList.append(new PUSH(pgen, text));
tempList.append(ifact.createInvoke("java.io.PrintStream", "print",
Type.VOID, new Type[]{Type.STRING}, Constants.INVOKEVIRTUAL));
// 獲取當前時間
tempList.append(ifact.createInvoke("java.lang.System", "currentTimeMillis",
Type.LONG, Type.NO_ARGS, Constants.INVOKESTATIC));
// 獲取之前存儲的 starttime
tempList.append(InstructionFactory.createLoad(Type.LONG, lvg.getIndex()));
// 相減得到耗時
tempList.append(InstructionConstants.LSUB);
// 輸出耗時
tempList.append(ifact.createInvoke("java.io.PrintStream", "print",
Type.VOID, new Type[]{Type.LONG}, Constants.INVOKEVIRTUAL));
// 輸出 " ms."
tempList.append(new PUSH(pgen, " ms."));
tempList.append(ifact.createInvoke("java.io.PrintStream", "println",
Type.VOID, new Type[]{Type.STRING}, Constants.INVOKEVIRTUAL));
// 最後輸出結束標記
tempList.append(ifact.createPrintln("end end end end end end end end end end"));
// 插入到返回指令之前
ilist.insert(insertPosition, tempList);
// ========== 3. 收尾工作 ==========
wrapgen.stripAttributes(false);
wrapgen.setMaxStack();
wrapgen.setMaxLocals();
cgen.addMethod(wrapgen.getMethod());
ilist.dispose();
}
public static void main(String[] args) {
// 測試入口:指定 class 文件路徑和方法名
args = new String[]{
"D:\\java to eclipse\\javaeclipsestudy\\workspace\\BCELTest\\bin\\StringBuilder.class",
"buildString"
};
String targetClassfile = "StringBuilder.class";
if (args.length == 2 && args[0].endsWith(".class")) {
try {
JavaClass jclas = new ClassParser(args[0]).parse();
ClassGen cgen = new ClassGen(jclas);
Method[] methods = jclas.getMethods();
int index;
for (index = 0; index < methods.length; index++) {
if (methods[index].getName().equals(args[1])) break;
}
if (index < methods.length) {
modifyWrapper(cgen, methods[index]);
FileOutputStream fos = new FileOutputStream(targetClassfile);
cgen.getJavaClass().dump(fos);
fos.close();
System.out.println("修改完成,已輸出至 " + targetClassfile);
} else {
System.err.println("Method " + args[1] + " not found in " + args[0]);
}
} catch (Exception e) {
e.printStackTrace();
}
} else {
System.out.println("usage: class-file method-name");
}
}
}
非靜態方法的第 0 個槽位存放 this,參數按順序排列,long/double 佔 2 個槽位。BCEL 的 addLocalVariable 會自動分配可用索引,避免衝突。
ilist.insert(printlnList) 插在第一條指令前。return,需插在其前一條;void 方法則插在末尾。插入指令後必須調用 setMaxStack() 和 setMaxLocals() 重新計算最大棧深和局部變量表大小,否則驗證器會報錯。
直接修改原方法字節碼比生成包裝方法更高效,且不改變方法簽名。掌握 InstructionList、InstructionFactory、局部變量管理是核心。實際項目中常用於性能監控、日誌埋點、事務管理等切面編程場景。