Apache BCEL (Byte Code Engineering Library) 在 JVM 指令級別操作字節碼,提供極致控制力但 API 複雜。本文對比 Javassist,詳細講解 BCEL 雙層架構(org.apache.bcel.classfile 只讀分析 / org.apache.bcel.generic 可編輯構建),並以「為方法添加執行計時」為例,完整演示:解析類 → 重命名原方法 → 生成包裝器方法(保存時間戳 → 調用原方法 → 計算耗時 → 打印日誌 → 返回結果) → 輸出修改後的 Class 文件。附完整可運行代碼 BCELTiming 與驗證輸出。
| 包 | 用途 | 核心類 |
|---|---|---|
org.apache.bcel.classfile | 只讀分析現有類 | JavaClass、Method、Field、ConstantPool |
org.apache.bcel.generic | 構建/修改類 | ClassGen、MethodGen、InstructionList、InstructionFactory、ConstantPoolGen |
Repository 負責解析類路徑下的類文件,返回 JavaClass;修改後通過 ClassGen.getJavaClass().dump(outputStream) 輸出。
原方法採用低效字符串拼接(result += char),耗時隨長度平方增長。期望輸出:
Call to method buildString$impl took 20 ms.
Constructed string of length 1000
InstructionFactory ifact = new InstructionFactory(cgen);
InstructionList ilist = new InstructionList();
ConstantPoolGen pgen = cgen.getConstantPool();
String cname = cgen.getClassName();
MethodGen wrapgen = new MethodGen(method, cname, pgen);
wrapgen.setInstructionList(ilist);
MethodGen,名稱加 $impl 後綴,移除原方法,添加新方法。
MethodGen methgen = new MethodGen(method, cname, pgen);
cgen.removeMethod(method);
String iname = methgen.getName() + "$impl";
methgen.setName(iname);
cgen.addMethod(methgen.getMethod());
this 佔槽),累加各參數 Type.getSize()。
ilist.append(ifact.createInvoke("java.lang.System", "currentTimeMillis", Type.LONG, Type.NO_ARGS, Constants.INVOKESTATIC));
ilist.append(InstructionFactory.createStore(Type.LONG, slot));
this 與參數,INVOKEVIRTUAL/INVOKESTATIC,結果非 void 時存入 slot+2。
System.out(GETSTATIC),構建字符串常量,調用 PrintStream.print/println,中間插入 currentTimeMillis() - start(LSUB)。
IRETURN/LRETURN/ARETURN,設置 MaxStack/MaxLocals,添加包裝器方法。
public class BCELTiming {
public static void main(String[] argv) {
if (argv.length == 2 && argv[0].endsWith(".class")) {
JavaClass jclas = new ClassParser(argv[0]).parse();
ClassGen cgen = new ClassGen(jclas);
Method[] methods = jclas.getMethods();
// 查找目標方法
for (Method m : methods) if (m.getName().equals(argv[1])) { addWrapper(cgen, m); break; }
new FileOutputStream(argv[0]).write(cgen.getJavaClass().getBytes()); // 覆蓋原文件
}
}
// addWrapper() 見上述片段組合
}
[dennis]$ java StringBuilder 1000 2000 4000 8000 16000
Constructed string of length 1000 ...
[dennis]$ java -cp bcel.jar:. BCELTiming StringBuilder.class buildString
[dennis]$ java StringBuilder 1000 2000 4000 8000 16000
Call to method buildString$impl took 20 ms.
Constructed string of length 1000
Call to method buildString$impl took 79 ms.
Constructed string of length 2000
...(耗時呈指數級增長,印證 O(n²) 特性)
org.apache.bcel.verifier.VerifierFactory 確保輸出類符合 JVM 規範。java -cp bcel.jar org.apache.bcel.util.Class2HTML StringBuilder.class 生成幀式字節碼視圖,極利於調試。