在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类。然后加载了修改后的类,同时创建了一个test.Rectangle类的新实例。
如果用户想要在类被加载时按需修改的话,用户可以添加一个javassist.Loader的事件监听。添加事件监听器在类加载器加载类时会被通知。事件监听类必须实现下面的接口:
public interface Translator {
public void start(ClassPool pool)
throws NotFoundException, CannotCompileException;
public void onLoad(ClassPool pool, String classname)
throws NotFoundException, CannotCompileException;
}
通过javassist.Loader的addTranslator()将事件监听器添加到javassist.Loader中时,方法start()会被调用。方法onLoad()在javassist.Loader加载类之前调用。onLoad()可以修改加载类的定义。
例如,下面的事件监听器在所有类被加载之前,修改所有的类为公共类。
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()不必去调用toBytecode()或者writeFile(),因为javassist.Loader调用了这些方法来获取类文件。
为了通过MyTranslator对象运行MyApp应用,编写如下主函数:
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试图在委托给父加载器之前加载类。它只有在下面的情况才会进行委托:
- 调用ClassPool对象的get()并不能发现的类
- 使用delegateLoadingOf()被指定由父加载器加载的类
这个扫描顺序允许通过Javassist加载修改类。然而,它因为某些原因无法加载修改类时,它会委托给父类加载。一旦一个类被父类加载,其它的被该类引用的类也会被父加载器加载,因此它们从不被修改。所有在类C中所引用的类都被类C的实际加载器加载。如果你的程序加载修改类失败,你应该确认是否所有类被修改类引用的类都已经被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"); // MyApp.class must be there.
}
/* 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);
// modify the CtClass object here
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是一个应用程序,为了执行这个程序,首先把类文件放到./class目录下,它不能包含在扫描路径中。否则,MyApp.class会被默认系统类加载器加载,它是SampleLoader的父类。目录名./class是在构造方法里的insertClassPath()指定。如果需要,你可以选择一个和./class不同的目录。然后做如下操作:
% java SampleLoader
类加载器会加载MyApp(./class/MyApp.class)同时使用命令行参数调用MyApp.main()。
这是使用Javassist最简单的方式。然而,如果你写一个更复杂的类加载器,你可能需要更详细的关于Java类加载器机制的知识。例如,上面的程序将把MyApp类放在与SampleLoader不同的命名空间中,因为两个类被不同的类加载器加载。因此,MyApp类无法直接访问类SampleLoader。
3. 修改系统类
如java.lang.String的系统类不能被除了系统类加载器之外的类加载加载。因此,上面展示的SampleLoader或者javassist.Loader无法在加载时修改系统类。
如果你的应用需要,系统类必须静态修改。例如,下面的程序添加了一个新的属性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的系统类不应该被发布,因为这样做会违反Java 2 Runtime Environment二进制代码许可证。
4. 运行时重新加载类
如果JVM使用JPDA(Java Platform Debugger Architecture)开启的模式运行,一个类可以被动态重新加载。在JVM加载类之后,旧版本的类定义可以被卸载,新的可以被再次加载。这意味着,类的定义可以在运行器被动态加载。然而,新的类定义必须与旧的兼容。JVM不允许两个版本之间的不兼容。它们需要拥有相同的方法和属性。
Javassist提供了一个方便的类用于在运行期间重新加载类。更多的信息,可以查看javassist.tools.HotSwapper文档的API。