Javassist 指南1

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对象。通过ClassPoolget()我们可以获取到CtClass对象。上面的程序通过ClassPool获取了一个CtClass对象,该对象代表test. Rectangle这个类。ClassPoolgetDefault()方法返回的ClassPool对象,会从默认的系统路径中寻找CtClass对象。

如果我们去看ClassPool的源码,我们会知道,ClassPool就是hashtable,其中keyclass对应的类名,valueclass对应的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类。更多细节请查看这一章节

定义新类

当我们需要定义一个新类时,可以使用ClassPoolmakeClass()方法。

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");

上面的程序定义了一个名为Point的新类,该类不含有任何成员变量。Point类的成员方法可以通过CtNewMethod的工厂方法创建,并通过CtClassaddMethod()方法添加到Point类。

makeClass()方法不能创建一个新的接口类,但是使用makeInterface()方法就可以。接口类中的成员方法可以使用CtNewMethodabstractMethod()进行创建,因为接口方法就是一个抽象方法。

冻结类

如果一个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对象又可以被修改了。

如果ClassPooldoPruning成员变量被设置为true,当Javassist冻结CtClass对象时,会将CtClass对象的内部数据结构进行裁剪。裁剪掉一些无用的属性是为了减少内存消耗。所以,CtClass对象被裁剪后,方法的字节码是不允许被访问的,但是方法名、方法签名、注解信息是可以被访问的。已经被裁剪过的CtClass对象不能被解冻了。所以,ClassPooldoPruning成员变量默认是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对象。如果一个程序运行在网页服务器上,例如JBossTomcat,那么ClassPool对象可能就找不到用户所需要的类,因为网页服务器使用多个类加载器作为系统类加载器。在这样的情况下,额外的类路径必须要注册到ClassPool中,假设下面的pool代表一个ClassPool对象:

pool.insertClassPath(new ClassClassPath(this.getClass()));

上面的代码将this所指向的类对象所在的路径加入到了ClassPoolclass 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相匹配。

如果你不知道一个类的全称路径,那么你可以使用ClassPoolmakeClass方法来得到一个CtClass对象:

ClassPool cp = ClassPool.getDefault();
InputStream ins = an input streamfor reading a class file;
CtClass cc = cp.makeClass(ins);

makeClass()方法从给定的输入流中构造出一个CtClass对象。你可以使用makeClass()方法将类文件提供给ClassPool对象。如果搜索路径中包含大体积的jar包,这有可能会提高性能。由于ClassPool会按需读取类文件,所以它极有可能会为了查找每一个类文件而重复查找整个jar包。makeClass()方法可以用来优化这样的搜索。通过makeClass()方法构造出来的CtClass对象会被存储到ClassPool中,而其所在的类文件不会被重复加载。

用户可以扩展类的搜索路径。他们可以定义一个新的类,并实现ClassPath接口,然后调用ClassPoolinsertClassPath()方法。这种方法允许我们将非标准的资源库加载到类搜索路径中。

2、ClassPool

ClassPoolCtClass对象的一个容器。一旦CtClass对象被创建出来后,它就永远存储在了ClassPool中。这是因为当我们需要编译一个class的源代码时,我们需要用到这个class表示的CtClass对象。

举个例子,假设我们需要将一个新方法getter()添加到CtClass对象,该CtClass对象代表Point这个类。然后程序需要编译一段代码,该代码中包含Point.getter()方法的调用,然后将编译的结果添加到另外一个名为Line的类的方法中。如果代表PointCtClass丢失了,那么编译器就无法编译getter()这个方法了。注意:Point类中原来并没有getter()这个方法,因此为了能够正确编译该方法,ClassPool必须包含程序运行期间的所有CtClass对象。

避免内存溢出

如果CtClass对象非常大,ClassPool就会出现内存溢出(虽然这种情况很少发生,因为Javassist会通过 多种方式 去尽量减少内存消耗)。为了避免这样的情况出现,我们可以明确地将一些不必要的CtClass对象从ClassPool中去掉。当你调用CtClass对象的detach方法时,CtClass对象就会从ClassPool中去掉。看下面的例子:

CtClass cc = ... ;
cc.writeFile();
cc.detach();

在调用了CtClassdetach方法之后,你不能调用CtClass的任何方法了。但是,你可以通过调用ClassPoolget()方法来创建一个新的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对象,然后调用了CtClasssetName()方法将一个新名字Pair赋值给了CtClass对象。在这个调用之后,这个CtClass对象所代表的的类的名称Point被改变成了Pair,类定义的其他部分不变。

注意:CtClasssetName()方法改变了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.

cc1cc2cc同一个CtClass对象,然而cc3则不是。注意:在cc.setName("Pair")执行完成后,cccc1引用的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.

为了绕开这个限制,你应该调用ClassPoolgetAndRename()方法,如下所示:

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 方法

CtClasstoClass方法使用当前线程的类加载器来加载该CtClass代表的class类。如果要调用该方法,调用者必须具有相应的权限,否则会抛出SecurityExceptiontoClass的使用方法如下:

public class Hello {
    public voidsay() {
        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加载了上面的代码片段。又因为上面的代码片段引用了MyClassLoaderClassObjectBox,这样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 intgetX() { 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 intgetX() { 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 intgetX() { 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是不能在运行时修改系统类的。

如果你的应用程序想修改系统类,系统类必须被静态的修改。例如,下面的程序就将一个新的字段hiddenValuejava.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 使用指南(一)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值