第一章 读写字节码
第二章 类池
第三章 类加载器
在上一章我们介绍了Javassist类池相关的一些操作,本章我们会介绍Javassist中的类加载器。
如果事先知道想要修改的类,那么修改类的最简单的方法如下:
1.通过调用ClassPool对象的get方法来获取一个CtClass对象。
2.修改这个类
3.调用CtClass 对象的writeFile方法或toBytecode方法获取修改后的类文件。
如果想要在加载时确定类是否被修改,则用户必须使 Javassist 与类加载器协作。Javassist 可以与类加载器一起使用,以便可以在加载时修改字节码。Javassist 的用户可以自定义自己的类加载器,也可以使用 Javassist 提供的类加载器。
CtClass类提供了一个方便的toClass方法,请求当前线程的上下文类加载器加载该CtClass 对象所代表的类。要调用此方法,调用者必须具有适当的权限;否则,程序可能会抛出 SecurityException异常。
以下程序展示如何使用toClass方法
public class Hello {public void say() {System.out.println("Hello");}
}public class Test {public static void main(String[] args) throws Exception {ClassPool cp = ClassPool.getDefault();CtClass cc = cp.get("Hello");CtMethod m = cc.getDeclaredMethod("say");m.insertBefore("{ System.out.println(\"Hello.say():\"); }");Class c = cc.toClass();Hello h = (Hello)c.newInstance();h.say();}
}
Test.main方法在Hello的say方法的方法体中插入一个对println方法的调用。然后构造出修改后的Hello类的实例,最后调用该实例的say方法。
请注意,上面的程序取决于这样一个事实,即在调用toClass方法之前从未加载过Hello类。否则,JVM将在toClass方法请求加载修改后的Hello类之前加载原始的Hello类。因此,加载修改后的Hello类就会失败(引发LinkageError)。例如,如果Test中的main()如下所示:
public static void main(String[] args) throws Exception {Hello orig = new Hello();ClassPool cp = ClassPool.getDefault();CtClass cc = cp.get("Hello");:
}
以上实例在main的第一行加载原始Hello类(调用了Hello类的构造方法去使类加载器加载Hello类),之后再调用toClass方法将会引发异常,因为类加载器不能同时加载Hello类的两个不同版本。
如果程序在JBoss和Tomcat等应用服务器上运行,则toClass方法使用的上下文类加载器可能不合适。在这种情况,应用程序会抛出ClassCastException异常。为避免这种异常抛出,你必须对toClass方法明确地指定一个合适的类加载器。例如,如果bean是你的会话中的bean对象,代码如下:
CtClass cc = ...;
Class c = cc.toClass(bean.getClass().getClassLoader());
想要程序正常运行的话,你应该给toClass方法一个类加载器去加载你的程序(在上面的例子中,为bean对象类的类加载器)
toClass方法是简便的,如果你需要更多复杂度功能,你应该编写你自己的类加载器。
在Java中,多个类装入器可以共存,每个类加载器都会创建自己的命名空间。不同的类加载器可以加载具有相同类名的不同类文件。被加载的两个类被视为不同的类。这个特性使我们能够在一个JVM上运行多个应用程序,即使这些程序包含具有相同类名的不同的类。
注意:JVM不允许动态地重加载类。一旦类加载器加载了一个类,它就不能在运行时重新加载该类的修改后版本。因此,您不能在JVM加载类后更改它的定义。然而,JPDA(Java平台调试器架构)提供了有限的重新加载类的能力。参见第3.6节。
如果同一个类文件由两个不同的类加载器加载,JVM将生成两个具有相同名称和定义的不同类。这两个类被视为不同的类。由于这两个类不相同,因此一个类的实例不能作为变量分配给另一个类。两个类之间的强制转换将会失败,并会抛出ClassCastException异常。
例如,以下代码段将引发异常:
MyClassLoader myLoader = new MyClassLoader();
Class clazz = myLoader.loadClass("Box");
Object obj = clazz.newInstance();
Box b = (Box)obj; // this always throws ClassCastException.
Box类由两个类加载器加载。假设类加载器CL加载包含此代码段的类。由于此代码段引用了MyClassLoader、Class、Object和Box,所以CL也会加载这些类(除非它委托给另一个类加载器)。因此,变量b的类型是CL加载的Box类。另一方面,myLoader也加载Box类。对象obj是由myLoader加载的Box类的实例。因此,最后一条语句总是抛出ClassCastException,因为对象obj的类是Box类的不同版本,而不是用作变量b的类型。
多个类加载器形成一个树结构。除启动类加载器外,每个类加载器都有一个父类加载器。它通常加载子类加载器加载的类。由于加载类的请求可以沿着类加载器的这个树形结构结构进行委托,(参考双亲委派模型)因此类可能会由你没有请求的类加载器加载。因此,请求加载类C的类加载器可能与实际加载类C的类加载器不同。为了区别,我们将前一个加载器称为C的发起加载器,将后一个类加载器称为C的真正的类加载器。
此外,如果被请求加载类C的类加载器CL(C的发起加载器)委托给父类加载器PL加载类C,则类加载器CL永远不会被请求加载在类C中引用的任何类。CL不是这些类的发起加载器。相反,父类加载器PL成为它们的发起加载器,并被请求加载它们。类C的定义所引用的类由C的真正加载器加载。
为了理解这种行为,让我们考虑以下示例。
public class Point { // loaded by PLprivate int x, y;public int getX() { return x; }:
}public class Box { // the initiator is L but the real loader is PLprivate Point upperLeft, size;public int getBaseX() { return upperLeft.x; }:
}public class Window { // loaded by a class loader Lprivate Box box;public int getBaseX() { return box.getBaseX(); }
}
假设一个类Window由类加载器L加载。Window的发起类加载器和真正的加载器都是L。由于Window引用了Box,JVM将请求L加载Box。这里,假设L将此任务委托给父类加载器PL。Box的发起类加载器是L,但真正的类加载器是PL。在这种情况下,Point的发起类加载器不是L,而是PL,因为它与Box的真正类加载器相同。因此,L从未被请求加载Point。
接下来,让我们考虑一个稍作修改的示例。
public class Point {private int x, y;public int getX() { return x; }:
}public class Box { // the initiator is L but the real loader is PLprivate Point upperLeft, size;public Point getSize() { return size; }:
}public class Window { // loaded by a class loader Lprivate Box box;public boolean widthIs(int w) {Point p = box.getSize();return w == p.getX();}
}
现在,Window的定义也指Point。在这种情况下,如果请求装入Point,类装入器L也必须委托给PL。必须避免让两个类加载器双重加载同一个类。两个加载器中的一个必须委派给另一个。
如果加载Point时L没有委托给PL,widthIs()将抛出ClassCastException。由于Box的实际加载程序是PL,因此Box中引用的Point也由PL加载。因此,getSize()的结果值是PL加载的Point的实例,而widthIs()中变量p的类型是L加载的Point。JVM将它们视为不同的类型,因此由于类型不匹配而引发异常。
这种行为有些不便,但很有必要。如果以下声明:
Point p = box.getSize();
如果没有抛出异常,那么Window的程序员可能会破坏Point对象的封装。例如,字段x在PL加载的Point中是私有的。但是,如果L使用以下定义加载Point,则Window类可以直接访问x的值:
public class Point {public int x, y; // not privatepublic int getX() { return x; }:
}
有关Java中类加载器的更多详细信息,请参阅以下文章:
Sheng Liang and Gilad Bracha, “Dynamic Class Loading in the Java Virtual Machine”,
ACM OOPSLA’98, pp.36-44, 1998.
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方法被调用。在javassist.Loader加载类之前onLoad方法被调用。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搜索类的顺序不同。java.lang.ClassLoader首先将加载操作委托给父类加载器,然后仅当父类加载器找不到类时才尝试加载类。另一方面,javassist.Loader尝试在类加载请求被委托给父类加载器之前加载类。仅在以下情况下才会把类加载请求委托给父类:
这个搜索顺序允许Javassist加载修改后的类。但是,如果由于某种原因Javassist找不到修改过的类,它将委托给父类加载器去加载类。一旦一个类被父类加载器加载,该类中引用的其他类也将被父类加载器加载,因此它们永远不会被修改。回想一下,类C中引用的所有类都是由C的真正加载器加载的。如果您的程序未能加载修改后的类,您应该确保使用该类的所有类是否都已由javassist.Loader加载。
使用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 herebyte[] 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类将由系统默认的类加载器(即SampleLoader的父类加载器)加载。目录名./class由构造函数中的insertClassPath方法指定。如果需要,可以选择其他名称而不是./class。然后执行以下操作:
% java SampleLoader
类加载器加载类MyApp(./class/MyApp.class),并使用命令行参数调用MyApp.main方法。
这是使用Javassist的最简单方法。然而,如果您编写一个更复杂的类加载器,您可能需要详细了解Java的类加载机制。例如,上面的程序将MyApp类放在一个与类SampleLoader所属的命名空间分开的命名空间中,因为这两个类由不同的类加载器加载。因此,MyApp类不能直接访问SampleLoader类。
像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 运行时环境二进制代码许可证(Java 2 Runtime Environment binary code license)。
如果JVM是在启用JPDA(Java平台调试器架构)的情况下启动的,则类是可以动态重加载的。JVM加载类后,可以卸载旧版本的类定义,并重新加载新版本的类。也就是说,该类的定义可以在运行时动态修改。然而,新的类定义必须与旧的类定义兼容。JVM不允许在两个版本之间有较大的改动。它们具有相同的方法和字段集合。
Javassist为在运行时重新加载类提供了一个方便的类。有关更多信息,请参阅javassist.tools.HotSwapper的API文档。
本篇文章介绍了Javassist的CttoClass方法 、Java类加载、如何编写类加载器、修改系统类以及运行时重新加载类。
下一篇:Maven 项目中常用的工具包