在本系列前幾篇文章中,我們已探討如何使用 Javassist 修改編譯後的類文件並將其寫回磁碟。這種靜態轉換適合永久性變更,但若需在每次應用啟動時套用不同的修改,則顯得不夠靈活。本文將展示如何利用 JVM 的類載入機制,結合 Javassist 實現「載入時織入」,使類在被載入 JVM 之前即可被動態轉換,從而支援面向切面編程(AOP)風格的即時行為修改。
JVM 透過類載入器層級結構載入類,每個載入器(除啟動類載入器外)皆有父載入器,並遵循「雙親委派模型」:優先請求父載入器載入,失敗才嘗試自行載入。要攔截類載入,需提供自訂 ClassLoader 並將其置於系統類載入器之下,使其成為應用類的實際載入者。
以下代碼繼承 java.net.URLClassLoader,重寫 loadClass() 與 findClass() 方法,打印每個類的載入請求與實際載入來源:
public class VerboseLoader extends URLClassLoader {
protected VerboseLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public Class loadClass(String name) throws ClassNotFoundException {
System.out.println("loadClass: " + name);
return super.loadClass(name);
}
protected Class findClass(String name) throws ClassNotFoundException {
Class clas = super.findClass(name);
System.out.println("findClass: loaded " + name + " from this loader");
return clas;
}
public static void main(String[] args) { /* ...啟動邏輯... */ }
}
執行 java VerboseLoader Run 可見只有 Run 類由自訂載入器載入,核心類皆委派給父載入器。
Javassist 內建 javassist.Loader 類,封裝了自訂類載入器的建立與啟動流程。核心在於 javassist.ClassPool 採用觀察者模式:向其註冊 javassist.Translator 實例,當類被請求時,onWrite() 回調允許在類定義交付前修改其字節碼。
public class JavassistRun {
public static void main(String[] args) {
Translator xlat = new VerboseTranslator();
ClassPool pool = ClassPool.getDefault(xlat);
Loader loader = new Loader(pool);
String[] pargs = Arrays.copyOfRange(args, 1, args.length);
loader.run(args[0], pargs);
}
static class VerboseTranslator implements Translator {
public void start(ClassPool pool) {}
public void onWrite(ClassPool pool, String cname) {
System.out.println("onWrite called for " + cname);
}
}
}
輸出顯示 onWrite called for Run,證明攔截生效。
承接第 4 部分的計時範例,我們將靜態字節碼修改轉為載入時動態植入。關鍵在於 Translator.onWrite() 中比對目標類名,匹配則取得 CtClass 並注入計時邏輯。
public static class SimpleTranslator implements Translator {
private String m_className, m_methodName;
public SimpleTranslator(String cname, String mname) {
m_className = cname; m_methodName = mname;
}
public void start(ClassPool pool) {}
public void onWrite(ClassPool pool, String cname)
throws NotFoundException, CannotCompileException {
if (cname.equals(m_className)) {
CtClass clas = pool.get(cname);
addTiming(clas, m_methodName); // 參見第 4 部分實現
}
}
}
啟動方式:java TranslateTiming 目標類 目標方法 主類 參數...
為支援更靈活的切點選擇,設計一套類似 Java 方法聲明的模式語法,支援返回類型、類/方法名通配符、參數列表精確或無序匹配。例如:
java.lang.String test.StringBuilder.buildString(int)*buildString(int, java.lang.String) — 精確順序*buildString [int,java.lang.String] — 無序且可有額外參數TimingTranslator 在 onWrite() 中遍歷類的所有方法,依模式匹配返回類型、參數、名稱,符合者植入計時碼。完整匹配邏輯見附件下載。
本文示範了如何結合 Javassist 的 ClassPool、Loader、Translator 實現載入時類轉換,並發展出模式驅動的批量計時工具。下一部分將深入 Javassist 的字節碼搜尋/替換高階特性,進一步揭示其作為 AOP 框架的核心優勢。