Apache BCEL (Byte Code Engineering Library) 允許運行時修改類別檔。本文示範掃描特定方法呼叫點,在其前後插入計時輸出邏輯,實現無侵入式效能分析。
原始碼
String result = inst.buildString(Integer.parseInt(argv[i]));
System.out.println("Constructed string of length " + result.length());
轉換後
long startTime = ToolUtil.printStart();
String result = inst.buildString(Integer.parseInt(argv[i]));
ToolUtil.printEnd("buildString", startTime);
System.out.println("Constructed string of length " + result.length());
public class ToolUtil {
public static long printStart() {
System.out.println("start start start start");
return System.currentTimeMillis();
}
public static void printEnd(String methodName, long start) {
System.out.println("Call to " + methodName + " took " + (System.currentTimeMillis()-start) + " ms.");
System.out.println("end end end end end end");
}
}
public class BCELTiming {
private static void modifyWrapper(ClassGen cgen, String targetClass, String targetMethod, String targetSig) {
InstructionFactory factory = new InstructionFactory(cgen);
ConstantPoolGen cp = cgen.getConstantPool();
for (Method method : cgen.getMethods()) {
MethodGen mg = new MethodGen(method, cgen.getClassName(), cp);
InstructionList il = mg.getInstructionList();
if (il == null) continue;
InstructionHandle[] handles = il.getInstructionHandles();
for (InstructionHandle ih : handles) {
Instruction ins = ih.getInstruction();
// 尋找 INVOKEVIRTUAL 指令
if (ins.getOpcode() == Constants.INVOKEVIRTUAL) {
INVOKEVIRTUAL invoke = (INVOKEVIRTUAL) ins;
ConstantMethodref cmr = (ConstantMethodref) cp.getConstant(invoke.getIndex());
ConstantClass cc = (ConstantClass) cp.getConstant(cmr.getClassIndex());
String className = cc.getBytes(cp.getConstantPool());
ConstantNameAndType cnt = (ConstantNameAndType) cp.getConstant(cmr.getNameAndTypeIndex());
String methodName = cnt.getName(cp.getConstantPool());
String sig = cnt.getSignature(cp.getConstantPool());
// 匹配目標方法
if (className.equals(targetClass) && methodName.equals(targetMethod) && sig.equals(targetSig)) {
// 1. 移除舊方法,準備重建
cgen.removeMethod(mg.getMethod());
// 2. 前置插入:呼叫 printStart() 並存入區域變數
LocalVariableGen lvg = mg.addLocalVariable("startTime", Type.LONG, null, null);
InstructionList before = new InstructionList();
before.append(factory.createInvoke("ToolUtil", "printStart", Type.LONG, Type.NO_ARGS, Constants.INVOKESTATIC));
before.append(InstructionFactory.createStore(Type.LONG, lvg.getIndex()));
il.insert(ih.getPrev().getPrev(), before); // 插入呼叫指令前
// 3. 後置插入:載入區域變數、方法名,呼叫 printEnd()
InstructionList after = new InstructionList();
after.append(factory.createConstant(methodName));
after.append(factory.createLoad(Type.LONG, lvg.getIndex()));
after.append(factory.createInvoke("ToolUtil", "printEnd", Type.VOID, new Type[]{Type.STRING, Type.LONG}, Constants.INVOKESTATIC));
il.insert(ih.getNext().getNext(), after); // 插入呼叫指令後
// 4. 完成方法重建
mg.stripAttributes(false);
mg.setMaxStack();
mg.setMaxLocals();
cgen.addMethod(mg.getMethod());
break; // 假設每方法僅一處呼叫點
}
}
}
}
}
public static void main(String[] args) throws Exception {
JavaClass jc = new ClassParser(args[0]).parse();
ClassGen cgen = new ClassGen(jc);
modifyWrapper(cgen, "StringBuilder", "buildString", "(I)Ljava/lang/String;");
cgen.getJavaClass().dump(args[0]); // 覆蓋原 class 檔
}
}
InstructionList 尋找 INVOKEVIRTUAL/INVOKESTATIC/INVOKEINTERFACEConstantMethodref → ConstantClass + ConstantNameAndType 還原類名、方法名、簽名MethodGen.addLocalVariable() 自動分配 Slot,避免衝突ih.getPrev().getPrev() 定位至呼叫指令前的邊界 (視具體指令序列微調)stripAttributes/setMaxStack/setMaxLocals 更新屬性表| 優點 | 缺點/限制 |
|---|---|
| 無原始碼修改,部署期增強 | 需處理所有呼叫約定 (virtual/static/interface/special) |
| 可實現 AOP 類橫切關注點 | 複雜控制流 (迴圈/分支/異常) 插入點計算極難 |
| 效能分析/除錯/安全審計通用 | 大型專案全量掃描耗時,建議配合建構工具整合 |