在經過一段時間的休息之後,Dennis Sosnoski 又回來推出了他的 Java 編程的動態性系 列的第 5 部分。您已在前面的文章中看到了如何編寫用於轉換 Java 類文件以改變代碼行為 的程序。在本期中,Dennis將展示如何使用 Javassist 框架,把轉換與實際的類加載過程結 合起來,用以進行靈活的“即時”面向方面的特性處理。這種方法允許您決定想要在運行時 改變的內容,並潛地在每次運行程序時做出不同的修改。在整個過程中,您還將更深入地了 解向JVM 中加載類的一般問題。
在第 4 部分“ 用 Javassist 進行類轉換”中,您學習了如何使用 Javassist 框架來轉 換編譯器生成的 Java 類文件,同時寫回修改過的類文件。這種類文件轉換步驟對於做出持 久變更是很理想的,但是如果想要在每次執行應用程序時做出不同的變更,這種方法就不一 定很方便。對於這種暫時的變更,采用在您實際啟動應用程序時起作用的方法要好得多。
JVM 體系結構為我們提供了這樣做的便利途徑――通過使用 classloader 實現。通過使 用 classloader 掛鉤(hook),您可以攔截將類加載到 JVM 中的過程,並在實際加載這些 類之前轉換它們。為了說明這個過程是如何工作的,我將首先展示類加載過程的直接攔截, 然後展示 Javassist 如何提供了一種可在您的應用程序中使用的便利捷徑。在整個過程中, 我將利用取自本系列以前文章中的代碼片斷。
加載區域
運行 Java 應用程序的通常方式是作為參數向 JVM 指定主類。這對於標准操作沒有什麼 問題,但是它沒有提供及時攔截類加載過程的任何途徑,而這種攔截對大多數程序來說是很 有用的。正如我在第 1 部分“ 類和類裝入”中所討論的,許多類甚至在主類還沒有開始執 行之前就已經加載了。要攔截這些類的加載,您需要在程序的執行過程中進行某種程度的重 定向。
幸運的是,模擬 JVM 在運行應用程序的主類時所做的工作是相當容易的。您所需做的就 是使用反射(這是在不得 第 2 部分 中介紹的)來首先找到指定類中的靜態 main() 方法, 然後使用預期的命令行參數來調用它。清單 1 提供了完成這個任務的示例代碼(為簡單起見 ,我省略了導入和異常處理語句):
清單 1. Java 應用程序運行器
public class Run
{
public static void main(String[] args) {
if (args.length >= 1) {
try {
// load the target class to be run
Class clas = Run.class.getClassLoader().
loadClass(args[0]);
// invoke "main" method of target class
Class[] ptypes =
new Class[] { args.getClass() };
Method main =
clas.getDeclaredMethod("main", ptypes);
String[] pargs = new String[args.length-1];
System.arraycopy(args, 1, pargs, 0, pargs.length);
main.invoke(null, new Object[] { pargs });
} catch ...
}
} else {
System.out.println
("Usage: Run main-class args...");
}
}
}
要使用這個類來運行 Java 應用程序,只需將它指定為 java 命令的目標類,後面跟著應 用程序的主類和想要傳遞給應用程序的其他任何參數。換句話說,如果用於運行 Java 應用 程序的命令為:
java test.Test arg1 arg2 arg3
您相應地要通過如下命令使用 Run 類來運行應用程序:
java Run test.Test arg1 arg2 arg3
攔截類加載
就其本身而言,清單 1 中短小的 Run 類不是非常有用。為了實現攔截類加載過程的目標 ,我們需要采取進一步的動作,對應用程序類定義和使用我們自己的 classloader。
正如我們在第 1 部分中討論的,classloader 使用一個樹狀層次結構。每個 classloader(JVM 用於核心 Java 類的根 classloader 除外)都具有一個父 classloader 。Classloader 應該在獨自加載類之前檢查它們的父 classloader,以防止當某個層次結構 中的多個 classloader 加載同一個類時可能引發的沖突。首先檢查父 classloader 的過程 稱為 委托――classloader 將加載類的責任委托給最接近根的 classloader,後者能夠訪問 要加載類的信息。
當 清單 1 中的 Run 程序開始執行時,它已經被 JVM 默認的 System classloader(您 定義的 classpath 所指定的那一個)加載了。為了符合類加載的委托規則,我們需要對相同 的父 classloader 使用完全相同的 classpath 信息和委托,從而使我們的 classloader 成 為 System classloader 的真正替代者。幸運的是,JVM 當前用於 System classloader 實 現的 java.net.URLClassLoader 類提供了一種檢索 classpath 信息的容易途徑,它使用了 getURLs() 方法。為了編寫 classloader,我們只需從 java.net.URLClassLoader 派生子類 ,並初始化基類以使用相同的 classpath 和父 classloader 作為加載主類的 System classloader。清單 2 提供了這種方法的具體實現:
清單 2. 一個詳細的 classloader
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) {
if (args.length >= 1) {
try {
// get paths to be used for loading
ClassLoader base =
ClassLoader.getSystemClassLoader();
URL[] urls;
if (base instanceof URLClassLoader) {
urls = ((URLClassLoader)base).getURLs();
} else {
urls = new URL[]
{ new File(".").toURI().toURL() };
}
// list the paths actually being used
System.out.println("Loading from paths:");
for (int i = 0; i < urls.length; i++) {
System.out.println(" " + urls[i]);
}
// load target class using custom class loader
VerboseLoader loader =
new VerboseLoader(urls, base.getParent());
Class clas = loader.loadClass(args[0]);
// invoke "main" method of target class
Class[] ptypes =
new Class[] { args.getClass() };
Method main =
clas.getDeclaredMethod("main", ptypes);
String[] pargs = new String[args.length-1];
System.arraycopy(args, 1, pargs, 0, pargs.length);
Thread.currentThread().
setContextClassLoader(loader);
main.invoke(null, new Object[] { pargs });
} catch ...
}
} else {
System.out.println
("Usage: VerboseLoader main-class args...");
}
}
}
我們已從 java.net.URLClassLoader 派生了我們自己的 VerboseLoader 類,它列出正在 被加載的所有類,同時指出哪些類是由這個 loader 實例(而不是委托父 classloader)加 載的。這裡同樣為簡潔起見而省略了導入和異常處理語句。
VerboseLoader 類中的前兩個方法 loadClass() 和 findClass() 重載了標准的 classloader 方法。 loadClass() 方法分別針對 classloader 請求的每個類作了調用。在 此例中,我們僅讓它向控制台打印一條消息,然後調用它的基類版本來執行實際的處理。基 類方法實現了標准 classloader 委托行為,即首先檢查父 classloader 是否能夠加載所請 求的類,並且僅在父 classloader 無法加載該類時,才嘗試使用受保護的 findClass() 方 法來直接加載該類。對於 findClass() 的 VerboseLoader 實現,我們首先調用重載的基類 實現,然後在調用成功(在沒有拋出異常的情況下返回)時打印一條消息。
VerboseLoader 的 main() 方法或者從用於包含類的 loader 中獲得 classpath URL 的 列表,或者在與不屬於 URLClassLoader 的實例的 loader 一起使用的情況下,簡單地使用 當前目錄作為唯一的 classpath 條目。不管采用哪種方式,它都會列出實際正在使用的路徑 ,然後創建 VerboseLoader 類的一個實例,並使用該實例來加載命令行上指定的目標類。該 邏輯的其余部分(即查找和調用目標類的 main() 方法)與 清單 1 中的 Run 代碼相同。
清單 3 顯示了 VerboseLoader 命令行和輸出的一個例子,它用於調用清單 1 中的 Run 應用程序:
清單 3. 清單 2 中的程序的例子輸出
[dennis]$ java VerboseLoader Run
Loading from paths:
file:/home/dennis/writing/articles/devworks/dynamic/code5/
loadClass: Run
loadClass: java.lang.Object
findclass: loaded Run from this loader
loadClass: java.lang.Throwable
loadClass: java.lang.reflect.InvocationTargetException
loadClass: java.lang.IllegalAccessException
loadClass: java.lang.IllegalArgumentException
loadClass: java.lang.NoSuchMethodException
loadClass: java.lang.ClassNotFoundException
loadClass: java.lang.NoClassDefFoundError
loadClass: java.lang.Class
loadClass: java.lang.String
loadClass: java.lang.System
loadClass: java.io.PrintStream
Usage: Run main-class args...
在此例中,唯一直接由 VerboseLoader 加載的類是 Run 類。 Run 使用的其他所有類都 是核心 Java 類,它們是通過父 classloader 使用委托來加載的。這其中的大多數(如果不 是全部的話)核心類實際上都會在 VerboseLoader 應用程序本身的啟動期間加載,因此父 classloader 將只返回一個指向先前創建的 java.lang.Class 實例的引用。
Javassist 攔截
清單 2 中的 VerboseClassloader 展示了攔截類加載的基本過程。為了在加載時修改類 ,我們可以更進一步,向 findClass() 方法添加代碼,把二進制類文件當作資源來訪問,然 後使用該二進制數據。Javassist 實際上包括了直接完成此類攔截的代碼,因此與其進一步 擴充這個例子,我們不如看看如何使用 Javassist 實現。
使用 Javassist 來攔截類加載的過程要依賴我們在 第 4 部分 中使用的相同 javassist.ClassPool 類。在該文中,我們通過名稱直接從 ClassPool 請求類,以 javassist.CtClass 實例的形式取回該類的 Javassist 表示。然而,那並不是使用 ClassPool 的唯一方式――Javassist 還以 javassist.Loader 類的形式,提供一個使用 ClassPool 作為其類數據源的 classloader。
為了允許您在加載類時操作它們, ClassPool 使用了一個 Observer 模式。您可以向 ClassPool 的構造函數傳遞預期的觀察者接口(observer interface)的一個實例 javassist.Translator 。每當從 ClassPool 請求一個新的類,它都調用觀察者的 onWrite () 方法,這個方法能夠在 ClassPool 交付類之前修改該類的表示。
javassist.Loader 類包括一個便利的 run() 方法,它加載目標類,並且使用所提供的參 數數組來調用該類的 main() 方法(就像在 清單 1 中一樣)。清單 4 展示了如何使用 Javassist 類和這個方法來加載和運行目標應用程序類。這個例子中簡單的 javassist.Translator 觀察者實現僅只是打印一條關於正在被請求的類的消息。
清單 4. Javassist 應用程序運行器
public class JavassistRun
{
public static void main(String[] args) {
if (args.length >= 1) {
try {
// set up class loader with translator
Translator xlat = new VerboseTranslator();
ClassPool pool = ClassPool.getDefault(xlat);
Loader loader = new Loader(pool);
// invoke "main" method of target class
String[] pargs = new String[args.length-1];
System.arraycopy(args, 1, pargs, 0, pargs.length);
loader.run(args[0], pargs);
} catch ...
}
} else {
System.out.println
("Usage: JavassistRun main-class args...");
}
}
public static class VerboseTranslator implements Translator
{
public void start(ClassPool pool) {}
public void onWrite(ClassPool pool, String cname) {
System.out.println("onWrite called for " + cname);
}
}
}
下面是 JavassistRun 命令行和輸出的一個例子,其中使用它來調用 清單 1 中的 Run 應用程序。
[dennis]$java -cp .:javassist.jar JavassistRun Run
onWrite called for Run
Usage: Run main-class args...
運行時定時
我們在 第 4 部分中分析過的方法定時修改對於隔離性能問題來說可能一個很有用的工具 ,但它的確需要一個更靈活的接口。在該文中,我們只是將類和方法名稱作為參數傳遞給程 序,程序加載二進制類文件,添加定時代碼,然後寫回該類。對於本文,我們將把代碼轉換 為使用加載時修改方法,並將它轉換為可支持模式匹配,用以指定要定時的類和方法。
在加載類時更改代碼以處理這種修改是很容易的。在清單 4 中的 javassist.Translator 代碼的基礎上,當正在寫出的類名稱與目標類名稱匹配時,我們可以僅從 onWrite() 調用用 於添加定時信息的方法。清單 5 展示了這一點(沒有包含 addTiming() 的全部細節――請 參閱第 4 部分以了解這些細節)。
清單 5. 在加載時添加定時代碼
public class TranslateTiming
{
private static void addTiming(CtClass clas, String mname)
throws NotFoundException, CannotCompileException {
...
}
public static void main(String[] args) {
if (args.length >= 3) {
try {
// set up class loader with translator
Translator xlat =
new SimpleTranslator(args[0], args[1]);
ClassPool pool = ClassPool.getDefault(xlat);
Loader loader = new Loader(pool);
// invoke "main" method of target class
String[] pargs = new String[args.length-3];
System.arraycopy(args, 3, pargs, 0, pargs.length);
loader.run(args[2], pargs);
} catch (Throwable ex) {
ex.printStackTrace();
}
} else {
System.out.println("Usage: TranslateTiming" +
" class-name method-mname main-class args...");
}
}
public static class SimpleTranslator implements Translator
{
private String m_className;
private String 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);
}
}
}
}
模式方法
如清單 5 所示,除了使方法定時代碼在加載時工作外,在指定要定時的方法時增加靈活 性也是很理想的。我最初使用 Java 1.4 java.util.regex 包中的正則表達式匹配支持來實 現這點,然後意識到它並沒有真正帶來我想要的那種靈活性。問題在於,用於選擇要修改的 類和方法的有意義的模式種類無法很好地適應正則表達式模型。
那麼哪種模式對於選擇類和方法 有意義呢?我想要的是在模式中使用類和方法的任何幾 個特征的能力,包括實際的類和方法名稱、返回類型,以及調用參數類型。另一方面,我不 需要對名稱和類型進行真正靈活的比較――簡單的相等比較就能處理我感興趣的大多數情況 ,而對該比較添加基本的通配符就能處理其余的所有情況了。處理這種情況的最容易方法是 使模式看起來像標准的 Java 方法聲明,另外再進行一些擴展。
關於這種方法的例子,下面是幾個與 test.StringBuilder 類的 String buildString (int) 方法相匹配的模式:
java.lang.String test.StringBuilder.buildString(int)
test.StringBuilder.buildString(int)
*buildString(int)
*buildString
這些模式的通用模式首先是一個可選的返回類型(具有精確的文本),然後是組合起來的 類和方法名稱模式(具有“*”通配字符),最後是參數類型列表(具有精確的文本)。如果 提供了返回類型,必須使用一個空格將它與方法名稱匹配相隔離,而參數列表則跟在方法名 稱匹配後面。為了使參數匹配更靈活,我通過兩種方式來設置它。如果所給的參數是圓括號 括起的列表,它們必須精確匹配方法參數。如果它們是使用方括號(“[] ”)來括起的,所列出的類型全都必須作為匹配方法的參數來提供,不過該方法可 以按任何順序使用它們,並且還可以使用附加的參數。因此 *buildString(int, java.lang.String) 將匹配其名稱以“buildString”結尾的任何方法,並且這些方法精確地 按順序接受一個 int 類型和一個 String 類型的參數。 *buildString [int,java.lang.String] 將匹配具有相同名稱的方法,但是這些方法接受兩個 或更多的 參 數,其中一個是 int 類型,另一個是 java.lang.String 類型。
清單 6 給出了我編寫來處理這些模式的 javassist.Translator 子類的簡略版本。實際 的匹配代碼與本文並不真正相關,不過如果您想要查看它或親自使用它,我已將它包括在了 下載文件中。使用這個 TimingTranslator 的主程序類是 BatchTiming ,它也包括在下載文 件中。
清單 6. 模式匹配轉換程序
public class TimingTranslator implements Translator
{
public TimingTranslator(String pattern) {
// build matching structures for supplied pattern
...
}
private boolean matchType(CtMethod meth) {
...
}
private boolean matchParameters(CtMethod meth) {
...
}
private boolean matchName(CtMethod meth) {
...
}
private void addTiming(CtMethod meth) {
...
}
public void start(ClassPool pool) {}
public void onWrite(ClassPool pool, String cname)
throws NotFoundException, CannotCompileException {
// loop through all methods declared in class
CtClass clas = pool.get(cname);
CtMethod[] meths = clas.getDeclaredMethods();
for (int i = 0; i < meths.length; i++) {
// check if method matches full pattern
CtMethod meth = meths[i];
if (matchType(meth) &&
matchParameters(meth) && matchName(meth)) {
// handle the actual timing modification
addTiming(meth);
}
}
}
}
後續內容
在上兩篇文章中,您已經看到了如何使用 Javassist 來處理基本的轉換。對於下一篇文 章,我們將探討這個框架的高級特性,這些特性提供用於編輯字節代碼的查找和替換技術。 這些特性使得對程序行為做出系統性的變更很容易,其中包括諸如攔截所有方法調用或所有 字段訪問這樣的變更。它們是理解為什麼 Javassist 是 Java 程序中提供面向方面支持的卓 越框架的關鍵。請下個月再回來看看如何能夠使用 Javassist 來揭示應用程序中的方面 (aspect)。