在Javassist之Classloader(一)中我們講述了Javassist的toClass()以及Java的類加載器,本次我們將介紹Javassist的加載器,以及自定義加載器。
1. 使用javassist.Loader
Javassist提供了一個javassist.loader類加載器。這個類加載器使用一個javassist.ClassPool對象來讀取類文件。
例如,javassist.Loader可以被用來讀取被Javassist修改的特殊類。
import javassist.*;
import test.Rectangle;
public class Main {
public static void main(String[] args) throws Throwable {
ClassPool pool = ClassPool.getDefault();
Loader cl = new Loader(pool);
CtClass ct = pool.get("test.Rectangle");
ct.setSuperclass(pool.get("test.Point"));
Class c = cl.loadClass("test.Rectangle");
Object rect = c.newInstance();
:
}
}
這段程序修改了一個test.Rectangle類。test.Rectangle的父類被設置為test.Point類。然后加載了修改后的類,同時創(chuàng)建了一個test.Rectangle類的新實例。
如果用戶想要在類被加載時按需修改的話,用戶可以添加一個javassist.Loader的事件監(jiān)聽。添加事件監(jiān)聽器在類加載器加載類時會被通知。事件監(jiān)聽類必須實現(xiàn)下面的接口:
public interface Translator {
public void start(ClassPool pool)
throws NotFoundException, CannotCompileException;
public void onLoad(ClassPool pool, String classname)
throws NotFoundException, CannotCompileException;
}
通過javassist.Loader的addTranslator()將事件監(jiān)聽器添加到javassist.Loader中時,方法start()會被調(diào)用。方法onLoad()在javassist.Loader加載類之前調(diào)用。onLoad()可以修改加載類的定義。
例如,下面的事件監(jiān)聽器在所有類被加載之前,修改所有的類為公共類。
public class MyTranslator implements Translator {
void start(ClassPool pool)
throws NotFoundException, CannotCompileException {}
void onLoad(ClassPool pool, String classname)
throws NotFoundException, CannotCompileException
{
CtClass cc = pool.get(classname);
cc.setModifiers(Modifier.PUBLIC);
}
}
注意onLoad()不必去調(diào)用toBytecode()或者writeFile(),因為javassist.Loader調(diào)用了這些方法來獲取類文件。
為了通過MyTranslator對象運行MyApp應用,編寫如下主函數(shù):
import javassist.*;
public class Main2 {
public static void main(String[] args) throws Throwable {
Translator t = new MyTranslator();
ClassPool pool = ClassPool.getDefault();
Loader cl = new Loader();
cl.addTranslator(pool, t);
cl.run("MyApp", args);
}
}
為了運行這段程序,如下:
% java Main2 arg1 arg2...
MyApp類和其它應用類被MyTranslator翻譯。
注意如MyApp的應用類不能訪問例如Main2,MyTranslator和ClassPool的類加載器,因為它們被不同的類加載器加載。應用類被javassist.Loader加載,反之Main2等類是被默認的Java類加載器加載的。
javassist.Loader與java.lang.ClassLoader按不同的順序掃描類。ClassLoader首先委托加載行為給父加載器,它只會加載父加載器無法加載的類。另一方面,javassist.Loader試圖在委托給父加載器之前加載類。它只有在下面的情況才會進行委托:
- 調(diào)用ClassPool對象的get()并不能發(fā)現(xiàn)的類
- 使用delegateLoadingOf()被指定由父加載器加載的類
這個掃描順序允許通過Javassist加載修改類。然而,它因為某些原因無法加載修改類時,它會委托給父類加載。一旦一個類被父類加載,其它的被該類引用的類也會被父加載器加載,因此它們從不被修改。所有在類C中所引用的類都被類C的實際加載器加載。如果你的程序加載修改類失敗,你應該確認是否所有類被修改類引用的類都已經(jīng)被javassist.Loader加載。
2. 寫一個類加載
一個簡單的使用Javassist的類加載器如下:
import javassist.*;
public class SampleLoader extends ClassLoader {
/* Call MyApp.main().
*/
public static void main(String[] args) throws Throwable {
SampleLoader s = new SampleLoader();
Class c = s.loadClass("MyApp");
c.getDeclaredMethod("main", new Class[] { String[].class })
.invoke(null, new Object[] { args });
}
private ClassPool pool;
public SampleLoader() throws NotFoundException {
pool = new ClassPool();
pool.insertClassPath("./class"); // <em>MyApp.class must be there.</em>
}
/* Finds a specified class.
* The bytecode for that class can be modified.
*/
protected Class findClass(String name) throws ClassNotFoundException {
try {
CtClass cc = pool.get(name);
// <em>modify the CtClass object here</em>
byte[] b = cc.toBytecode();
return defineClass(name, b, 0, b.length);
} catch (NotFoundException e) {
throw new ClassNotFoundException();
} catch (IOException e) {
throw new ClassNotFoundException();
} catch (CannotCompileException e) {
throw new ClassNotFoundException();
}
}
}
類MyApp是一個應用程序,為了執(zhí)行這個程序,首先把類文件放到./class目錄下,它不能包含在掃描路徑中。否則,MyApp.class會被默認系統(tǒng)類加載器加載,它是SampleLoader的父類。目錄名./class是在構(gòu)造方法里的insertClassPath()指定。如果需要,你可以選擇一個和./class不同的目錄。然后做如下操作:
% java SampleLoader
類加載器會加載MyApp(./class/MyApp.class)同時使用命令行參數(shù)調(diào)用MyApp.main()。
這是使用Javassist最簡單的方式。然而,如果你寫一個更復雜的類加載器,你可能需要更詳細的關于Java類加載器機制的知識。例如,上面的程序?qū)袽yApp類放在與SampleLoader不同的命名空間中,因為兩個類被不同的類加載器加載。因此,MyApp類無法直接訪問類SampleLoader。
3. 修改系統(tǒng)類
如java.lang.String的系統(tǒng)類不能被除了系統(tǒng)類加載器之外的類加載加載。因此,上面展示的SampleLoader或者javassist.Loader無法在加載時修改系統(tǒng)類。
如果你的應用需要,系統(tǒng)類必須靜態(tài)修改。例如,下面的程序添加了一個新的屬性hiddenValue到java.lang.String:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("java.lang.String");
CtField f = new CtField(CtClass.intType, "hiddenValue", cc);
f.setModifiers(Modifier.PUBLIC);
cc.addField(f);
cc.writeFile(".");
這歌程序生成了一個“./java/lang/String.class”文件。
做以下操作使用修改的String類運行你的MyApp程序:
% java -Xbootclasspath/p:. MyApp arg1 arg2...
假設MyApp的定義如下:
public class MyApp {
public static void main(String[] args) throws Exception {
System.out.println(String.class.getField("hiddenValue").getName());
}
}
如果修改的String類被正確加載,MyApp打印hiddenValue。
注意:程序使用這種技術復寫的rt.jar的系統(tǒng)類不應該被發(fā)布,因為這樣做會違反Java 2 Runtime Environment二進制代碼許可證。
4. 運行時重新加載類
如果JVM使用JPDA(Java Platform Debugger Architecture)開啟的模式運行,一個類可以被動態(tài)重新加載。在JVM加載類之后,舊版本的類定義可以被卸載,新的可以被再次加載。這意味著,類的定義可以在運行器被動態(tài)加載。然而,新的類定義必須與舊的兼容。JVM不允許兩個版本之間的不兼容。它們需要擁有相同的方法和屬性。
Javassist提供了一個方便的類用于在運行期間重新加載類。更多的信息,可以查看javassist.tools.HotSwapper文檔的API。