1、读写字节码
Javassist 是一个能处理 Java字节码 的类库,Java字节码存储在class文件中,每一个class文件都包含了一个Java类或一个接口类。
在Javassist中,使用Javassist.CtClass
来表示一个class文件,所以说CtClass
类就是用来处理class文件的。举个例子:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();
上面的程序首先获取了一个ClassPool
对象,该对象相当于CtClass
的一个集合。ClassPool
对象在需要时会构造一个CtClass
对象,并将其记录下来,当下次再次获取该CtClass
对象时就能直接返回。
为了修改类定义,我们需要从ClassPool
对象中获取该类对应的CtClass
对象。通过ClassPool
的get()
我们可以获取到CtClass
对象。上面的程序通过ClassPool
获取了一个CtClass
对象,该对象代表test. Rectangle
这个类。ClassPool
的getDefault()
方法返回的ClassPool对象,会从默认的系统路径中寻找CtClass
对象。
如果我们去看ClassPool
的源码,我们会知道,ClassPool
就是hashtable
,其中key
是class
对应的类名,value
是class
对应的CtClass
对象。如果在ClassPool
中找不到CtClass
对象,则先会new一个CtClass
对象,然后将该对象存储到hashtable
中,最后将CtClass
对象返回。
CtClass
对象是可以被修改的。在上面的程序中,我们将test.Rectangle
的父类改成了test.Point
。当 CtClass()
执行writeFile()
时,这一改动点会写入到class文件中。writeFile()
会将CtClass
对象转换成class文件并写入到本地存储。Javassist同时还提供了一个直接获取class对应的二进制流的方法,如下所示。
byte[] b = cc.toBytecode();
你还可以将CtClass
转换成Class
对象。
Class clazz = cc.toClass();
toClass
方法会请求当前线程的ClassLoader
去加载CtClass
代表的class
类,它返回了一个Class
对象表示class
类。更多细节请查看这一章节。
定义新类
当我们需要定义一个新类时,可以使用ClassPool
的makeClass()
方法。
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");
上面的程序定义了一个名为Point
的新类,该类不含有任何成员变量。Point
类的成员方法可以通过CtNewMethod
的工厂方法创建,并通过CtClass
的addMethod()
方法添加到Point
类。
makeClass()
方法不能创建一个新的接口类,但是使用makeInterface()
方法就可以。接口类中的成员方法可以使用CtNewMethod
的abstractMethod()
进行创建,因为接口方法就是一个抽象方法。
冻结类
如果一个CtClass
对象通过writeFile()
、toClass()
、toBytecode()
方法转换成class文件,那么Javassist就会将CtClass
对象冻结起来,防止该CtClass
对象被修改。也就是说,冻结了的CtClass
对象是不允许被修改的。因为一个类只能被JVM加载一次。
一个已冻结的CtClass是可以被解冻的,解冻后的CtClass又被允许修改类了,举个例子:
CtClasss cc = ...;
:
cc.writeFile();
cc.defrost();
cc.setSuperclass(...); // OK since the class is not frozen.
上面的程序在defrost()
方法被调用后,CtClass对象又可以被修改了。
如果ClassPool
的doPruning
成员变量被设置为true
,当Javassist冻结CtClass
对象时,会将CtClass对象的内部数据结构进行裁剪。裁剪掉一些无用的属性是为了减少内存消耗。所以,CtClass
对象被裁剪后,方法的字节码是不允许被访问的,但是方法名、方法签名、注解信息是可以被访问的。已经被裁剪过的CtClass
对象不能被解冻了。所以,ClassPool
的doPruning
成员变量默认是false
。
stopPruning()
方法可以禁止CtClass
的裁剪,如下所示。
CtClasss cc = ...;
cc.stopPruning(true);
:
cc.writeFile(); // convert to a class file.
// cc is not pruned.
上面的程序中调用了stopPruning()
方法禁止了CtClass
的裁剪操作,因此在writeFile()
方法执行后,CtClass
对象可以被解冻。
注意:在调试的时候,你可能想在将
class
文件写入到磁盘后,临时性的禁止CtClass
对象的裁剪和冻结。这个时候你可以调用debugWriteFile()
方法,它会临时的禁止裁剪,在写入class
文件后会自动解冻CtClass
对象,之后该CtClass
对象还是可以被裁剪和冻结的。
类搜索路径
ClassPool.getDefault()
方法默认是在JVM的类搜索路径下返回的ClassPool
对象。如果一个程序运行在网页服务器上,例如JBoss
或Tomcat
,那么ClassPool
对象可能就找不到用户所需要的类,因为网页服务器使用多个类加载器作为系统类加载器。在这样的情况下,额外的类路径必须要注册到ClassPool
中,假设下面的pool
代表一个ClassPool
对象:
pool.insertClassPath(new ClassClassPath(this.getClass()));
上面的代码将this所指向的类对象所在的路径加入到了ClassPool
的class path
中。你可以使用任意的类对象替代上面的this.getClass()
,这样类对象的路径就会注册到ClassPool
的类加载路径中来。
当然,你也可以将一个文件夹注册到类加载路径。例如,下面的程序就将/usr/local/javalib
这个文件夹添加到了类加载路径。
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath("/usr/local/javalib");
同样的,用户不仅仅可以将文件夹注册到类加载路径,URL也是可以的,看下面的例子:
ClassPool pool = ClassPool.getDefault();
ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist.");
pool.insertClassPath(cp);
上面的程序将http://www.javassist.org:80/java/
加入到了类加载路径。该URL只能被用来查找属于org.javassit
包目录下的类。举个例子,如果要加载org.javassist.test.Main
这个类,那么它就可以从下面的路径中获取:
http://www.javassist.org:80/java/org/javassist/test/Main.class
此外,你可以直接将字节数组作为ClassPool
的类加载路径,并构造出已CtClass
兑现。这个时候,你可以使用ByteArrayClassPath
。看下面的例子:
ClassPool cp = ClassPool.getDefault();
byte[] b = a byte array;
String name = class name;
cp.insertClassPath(new ByteArrayClassPath(name, b));
CtClass cc = cp.get(name);
CtClass
对象对应着byte[] b
所表示的类文件。当get()
方法被调用时,ClassPool
从给定的ByteArrayClassPath
读取类文件,而get()
方法中的参数name
必须要ByteArrayClassPath
方法的参数name
相匹配。
如果你不知道一个类的全称路径,那么你可以使用ClassPool
的makeClass
方法来得到一个CtClass
对象:
ClassPool cp = ClassPool.getDefault();
InputStream ins = an input stream for reading a class file;
CtClass cc = cp.makeClass(ins);
makeClass()
方法从给定的输入流中构造出一个CtClass
对象。你可以使用makeClass()
方法将类文件提供给ClassPool
对象。如果搜索路径中包含大体积的jar
包,这有可能会提高性能。由于ClassPool
会按需读取类文件,所以它极有可能会为了查找每一个类文件而重复查找整个jar包。makeClass()
方法可以用来优化这样的搜索。通过makeClass()
方法构造出来的CtClass
对象会被存储到ClassPool
中,而其所在的类文件不会被重复加载。
用户可以扩展类的搜索路径。他们可以定义一个新的类,并实现ClassPath
接口,然后调用ClassPool
的insertClassPath()
方法。这种方法允许我们将非标准的资源库加载到类搜索路径中。
2、ClassPool
ClassPool
是CtClass
对象的一个容器。一旦CtClass
对象被创建出来后,它就永远存储在了ClassPool
中。这是因为当我们需要编译一个class
的源代码时,我们需要用到这个class
表示的CtClass
对象。
举个例子,假设我们需要将一个新方法getter()
添加到CtClass
对象,该CtClass
对象代表Point
这个类。然后程序需要编译一段代码,该代码中包含Point.getter()
方法的调用,然后将编译的结果添加到另外一个名为Line
的类的方法中。如果代表Point
的CtClass
丢失了,那么编译器就无法编译getter()
这个方法了。注意:Point
类中原来并没有getter()
这个方法,因此为了能够正确编译该方法,ClassPool
必须包含程序运行期间的所有CtClass
对象。
避免内存溢出
如果CtClass
对象非常大,ClassPool
就会出现内存溢出(虽然这种情况很少发生,因为Javassist会通过 多种方式 去尽量减少内存消耗)。为了避免这样的情况出现,我们可以明确地将一些不必要的CtClass对象从ClassPool
中去掉。当你调用CtClass
对象的detach
方法时,CtClass
对象就会从ClassPool
中去掉。看下面的例子:
CtClass cc = ... ;
cc.writeFile();
cc.detach();
在调用了CtClass
的detach
方法之后,你不能调用CtClass
的任何方法了。但是,你可以通过调用ClassPool
的get()
方法来创建一个新的CtClass
对象。当你调用了get()
方法后,ClassPool
会从类文件中再次读取内容并重新创建一个CtClass
对象,该对象会从get()
方法中返回。
另外一个避免内存溢出的方法是,创建一个新的ClassPool
对象来代替旧的ClassPool
对象。如果旧的ClassPool
对象被回收了,那么它包含的CtClass
对象也会被回收。如果我们要创建一个新的ClassPool
对象,那么需要执行如下代码:
ClassPool cp = new ClassPool(true);
// if needed, append an extra search path by appendClassPath()
上面的程序创建了一个默认的ClassPool
对象,这和我们使用ClassPool.getDefault()
方法创建的ClassPool
对象是一样的。但是,使用ClassPool.getDefault()
工厂方法创建ClassPool
对象显得更加方便。
注意:new ClassPool(true)
构造了一个ClassPool
对象并加入了系统的类搜索路径,它等同于下面的代码:
ClassPool cp = new ClassPool();
cp.appendSystemPath(); // or append another path by appendClassPath()
级联的ClassPools
如果程序正在Web程序服务器上运行,那么需要创建多个ClassPool
实例;对于每一个ClassLoader
对象都需要创建一个ClassPool
实例。程序应该通过ClassPool
的构造函数而不是通过getDefault()
方法来创建ClassPool对象。
多个ClassPool
对象可以像java.lang.ClassLoader
一样级联,例如:
ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.insertClassPath("./classes");
如果调用child.get()
方法,那么子ClassPool
首先会将委托给父ClassPool
。如果父ClassPool
中找不到该class
文件,那么子ClassPool
会尝试从./classes
文件夹下进行查找。
如果child.childFirstLookup
属性设置成true
,那么子ClassPool
会在委托给父ClassPool
之前从自己的目录下查找class
文件。例如:
ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.appendSystemPath(); // the same class path as the default one.
child.childFirstLookup = true; // changes the behavior of the child.
复制一个类来定义一个新类
我们可以从一个已经存在的类中定义一个新类。程序如下:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.setName("Pair");
上面的程序首先获取了Point
类对应的CtClass
对象,然后调用了CtClass
的setName()
方法将一个新名字Pair赋值给了CtClass
对象。在这个调用之后,这个CtClass
对象所代表的的类的名称Point
被改变成了Pair
,类定义的其他部分不变。
注意:CtClass
的setName()
方法改变了ClassPool
对象中的记录。从ClassPool
的实现角度来看,ClassPool
对象是CtClass
对象的哈希表。setName()
方法只是改变了CtClass
对象在哈希表的key值,key
值从原始类名变成了一个新类名。
因此,如果后续调用ClassPool的get("Point")方法,那么该方法肯定不会返回上面的程序的cc对象。这个时候,ClassPool会重新读取Point.class类,然后构建一个新的CtClass对象。这是因为关联Point这个名字的CtClass对象已经不存在了。看下面的例子:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
CtClass cc1 = pool.get("Point"); // cc1 is identical to cc.
cc.setName("Pair");
CtClass cc2 = pool.get("Pair"); // cc2 is identical to cc.
CtClass cc3 = pool.get("Point"); // cc3 is not identical to cc.
cc1
、cc2
和cc
同一个CtClass
对象,然而cc3
则不是。注意:在cc.setName("Pair")
执行完成后,cc
和cc1
引用的CtClass
对象都表示Pair
类。
ClassPool
对象主要用于维护类和CtClass
对象之间的一一对应的关系。Javassit不允许使用两个不同的CtClass对象来表示同一个类,除非有两个不同的ClassPool对象。
如果要创建另外一个默认的ClassPool
对象,那么可以执行如下代码(之前已展示):
ClassPool cp = new ClassPool(true);
如果你有两个ClassPool
对象,这个时候你可以从每一个ClassPool
中去获取代表同一个类的不同CtClass
对象。
重命名冻结类来定义一个新类
一旦CtClass
对象通过writeFile()
或者toBytecode()
转换成一个类文件,那么Javassist会禁止我们对该CtClass
对象作进一步的修改。因此,在CtClass
代表的Point
类被转换成class
文件后,你就不能通过执行setName()
方法来定义一个Point
的拷贝类。下面的例子是错误的:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
cc.setName("Pair"); // wrong since writeFile() has been called.
为了绕开这个限制,你应该调用ClassPool
的getAndRename()
方法,如下所示:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
CtClass cc2 = pool.getAndRename("Point", "Pair");
当getAndRename()
方法被调用时,ClassPool
首先会读取Point.class
类,然后创建一个新的CtClass
对象来表示Point
类。然而,它会在哈希表中记录CtClass
之前就将CtClass
对象重命名为Pair
。因此getAndRename()
方法可以在writeFile()
或者toBytecode()
调用之后执行。
3、Class loader
如果事先就知道哪些类需要修改,最简便的修改方法如下:
- 通过
ClassPool.get()
方法获取一个CtClass
对象。 - 修改
CtClass
对象。 - 调用
CtClass
对象的writeFile()
或者toBytecode()
方法获得修改后的类文件。
如果一个类是否需要修改是在运行时决定的,那么用户必须使用类加载器。使用类加载器的javassist
可以在运行时修改字节码。用户可以定义他们自己的类加载器,也可以使用Javassist
提供的类加载器。
如果在加载时,用户能够确定是否要修改某个类,用户必须使用Javassist
与类加载器协作。Javassist
可以使用类加载器在加载时修改字节码。用户可以自定义类加载器,也可以使用Javassist
提供的类加载器。
3.1 CtClass类的toClass 方法
CtClass
的toClass
方法使用当前线程的类加载器来加载该CtClass
代表的class
类。如果要调用该方法,调用者必须具有相应的权限,否则会抛出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
类时就会抛出一个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
类加载了,这个时候如果我们再去调用toClass()
方法,就会抛出一个异常,因为类加载器不能同时加载Hello
类两次。
如果上面的程序是运行在JBoss
或者Tomcat
中,toClass()
使用的上下文类加载器就会不适合了。在这种情况下,你有可能会看到ClassCastException
异常被抛出。为了避免该异常,你必须给toClass()
指定一个合适的类加载器。例如,如果 ‘bean’是你会话的bean对象,那么下面的代码是可以正常运行的:
CtClass cc = ...;
Class c = cc.toClass(bean.getClass().getClassLoader());
你可以给toClass()
方法传递你程序中已使用的类加载器(上面的例子使用了bean
对象的类加载器)。
不带参数的toClass()
方法是比较方便的。如果你需要更多复杂的功能,你应该给toClass()
方法传递你自定义的类加载器。
3.2 Java的类加载机制
在Java中,多个类加载器是可以共存的,而且每一个类加载器有自己的命名空间。不同的类加载器可以使用同样的类名加载出两个不同的类。加载出来的两个类被认为是不同的两个类。这个特性可以让我们在一个JVM环境中运行多个应用程序,即使这些应用程序包含了相同名称的不同类。
注意:JVM不允许动态重复加载类。一旦一个类加载器加载了一个类,那么它在运行期是不能加载它的修改版本的。因此,你不能在JVM加载了该类后再去改变它的定义。然后,JPDA(Java Platform Debugger Architecture) 却可以提供有限的能力来重新加载该类。 详细信息见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的类型是Box类型,它被CL加载了。另一方面,myLoader
同样加载了Box
类。obj对象就是被myLoader
加载出来的。因此,最后一行始终会抛出ClassCastException
异常,因为obj变量和b变量代表着不同类型的Box类。
多个类加载器形成了一个树状结构。除了bootstrap类加载器外,每一个类加载器都有一个父类加载器。它通常加载子类加载器的类。因为加载一个类会沿着这个层次进行委派,所以即使一个类没有被请求加载,那么它也有可能会被加载。因此,请求加载类C的类加载器A可能和实际上加载类C的类加载器B不是同一个加载器。为了区分,我们将前者称为类C的发起加载器,将后者称为类C的实际加载器。
此外,如果CL
类加载器被请求来加载类C
,但它将加载类C
的请求委托给了父类加载器PL
,这样CL
加载器就永远不会被请求来加载类C
中定义的其他类。这样CL
就是不是这些类的发起加载器。而PL
则成为了这些类的发起加载器,PL
被请求来加载它们。
为了理解上面所说的行为,让我们看下面这个例子:
public class Point { // loaded by PL
private int x, y;
public int getX() { return x; }
:
}
public class Box { // the initiator is L but the real loader is PL
private Point upperLeft, size;
public int getBaseX() { return upperLeft.x; }
:
}
public class Window { // loaded by a class loader L
private Box box;
public int getBaseX() { return box.getBaseX(); }
}
假如Window
这个类是被类加载器L加载的,那么Window
的发起加载器和真实的加载器也是L
。又因为Window
类引用了Box
类,那么JVM
会请求使用L
来加载Box
类。我们假设L将该加载任务委托给其父类PL
,那么Box
的发起类加载器是L
,而其真正的加载器是PL
。在这种情况下,Point
类的发起类加载器就是PL
,因为它的发起类加载器必须和Box
的真正的类加载器相同,因此Point
类的请求加载绝对不会委派给L
。
接下来,让我们来看一个稍微修改后的例子:
public class Point {
private int x, y;
public int getX() { return x; }
:
}
public class Box { // the initiator is L but the real loader is PL
private Point upperLeft, size;
public Point getSize() { return size; }
:
}
public class Window { // loaded by a class loader L
private Box box;
public boolean widthIs(int w) {
Point p = box.getSize();
return w == p.getX();
}
}
现在,Window
的定义中也引用了Point
类。在这种情况下,如果需要加载Point
类,类加载器L必须将该请求委托给加载器PL
。你必须避免让两个不同类加载器多次加载同一个类。两个类加载器中的其中一个必须以另一个作为父加载器。
当Point
类被加载时,如果L没有将该请求委托给PL,那么withIs()
就会抛出ClassCastException
异常。因为Box类的真正加载类是PL,而Point
类又是被Box
类所引用,所以Point
类也是被PL类所加载。这是因为,getSize()
返回的对象Point是被PL所加载的,然而widthIs()
中的变量p是被L所加载的。JVM就会认为这两个是不同的类型,然后抛出一个异常。
这样的行为的确不是很方便,但是却是必须的。如果下面这行代码
Point p = box.getSize();
不抛出异常的话,那么Window类的程序员会破坏Point
对象的封装。举个例子,被PL加载的Point
类的x字段是私有的,然而如果L直接加载具有以下定义的Point
类,那么Window
类是可以直接访问Point
中的x值的。
public class Point {
public int x, y; // not private
public int getX() { return x; }
:
}
3.3 使用javassist的类加载器
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.Point
类设置为test.Rectangle
的父类。然后程序加载了test.Rectangle
类,并创建了一个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;
}
当该事件监听器通过addTranslator()
方法被添加到javassist.Loader
时,start()
方法会被执行。当javassist.Loader
加载类之前,onLoad()
方法会被执行,我们可以在onLoad()
方法中修改将要被加载的类。
举个例子,下面的事件监听器会在类被加载之前,修改类的修饰符为public:
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
应用程序是不能访问loader类,如Main2、MyTranslator、ClassPool
等,因为他们是被其他类加载器加载的。MyApp应用程序中的类是被javassist.Loader
所加载,但是Main2
却是被Java默认的类加载器加载。
javassist.Loader
搜索类的顺序和java.lang.ClassLoader
是不一样的。Java的ClassLoader
首先会将类加载请求委托给父类加载器,当父类加载器找不到对应的类时,则它再自己加载。但是,javassist.Loader
会首先尝试自己加载该类,它只有在下面这些情况下才会将类加载请求委托给父类加载器:
- 在ClassPool中调用
get()
方法找不到这个类时; - 这些类已经通过
delegateLoadingOf()
执行由父类加载器加载。
此搜索顺序允许Javassist加载修改过的类。但是,如果找不到修改的类,该类将会被委托给父类加载器。一旦一个类被父类加载器加载了,那么该类引用的其他类也会被父类加载器加载,这样他们就永远不会被修改了。回想一下,C
类引用的所有类都被C
类实际加载器加载。如果你的程序无法加载修改的类,你应该确保是不是所有使用该类的类都已经由javassist
加载过了。
3.4 编写一个类加载器
使用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.5 修改系统类
系统类例如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 运行时二进制代码许可协议。
3.6 在运行时重新加载类
如果JVM在JPDA(Java Platform Debugger Architecture)启用的时候启动,那么类可以被动态的重新加载。在JVM加载了一个类后,旧版本的类可以被卸载,新版本的类就可以被重新加载了。这样,类定义就可以在运行时被动态的修改。然而,新的类定义必须和旧的类定义有所兼容。JVM是不允许两个版本的类的模式修改的。他们必须有相同的方法和字段。
Javassist
提供了一个类,这使得在运行时重新加载类变得更加方便。如果想要获取更多关于运行时重新加载类的信息,可以看API文档中关于javassist.tools.HotSwapper
的说明。
4、参考文档
1、英文原文
2、译文参考——Javassist 使用指南(一)