javassist翻译之三《类加载器》

如果事先知道哪些类必须被修改,修改这些类最简单的方法如下:

  • 通过调用ClassPool.get( ) 获取一个CtClass对象;
  • 修改它;
  • 对该CtClass对象调用writeFile( )或toBytecode( )方法以获取修改的类文件;

但是如果在加载时确定类是否被修改,则用户必须使Javassist与类加载器协作。Javassist可以与类加载器一起使用,以便字节码在加载时可以被修改。Javassist的用户可以定义他们自己的类加载器的版本,但是也可以使用Javassist提供的类加载器。


3.1 The toClass method in CtClass

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在请求加载修改后的Hello类之前加载过原始的Hello类,那么加载修改后的Hello类将失败(LinkageError is thrown)。例如:

public class Hello {
    public void say() {
        System.out.println("Hello");
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Hello orig = new Hello();    //JVM提前加载Hello类

        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();        // 报错,因为JVM中不允许一个类重复加载
        Hello h = (Hello)c.newInstance();
        h.say();
    }
}

如上代码所示:如果类Hello在调用toClass( )之前,已经在JVM中加载了一个原始的Hello类,将会抛出异常。因为类加载器不能允许同时加载两个不同版本的Hello类。

如果程序在JBoss和Tomcat等应用服务器上运行,toClass( )使用的上下文类加载器可能不合适。下面的例子中,你将看到一个意想不到的ClassCastException异常。为了避免这种异常,必须为toClass( ) 方法定义一个合适的类加载器。例如,如果一个bean是你的会话bean对象,那么应该用下面的方式:

CtClass cc = ...;
Class c = cc.toClass(bean.getClass().getClassLoader());

上面的这种方式就可以很好的工作。你应该将程序的类加载器作为toClass( )的参数(在上面的例子中,参数为bean对象的类的类加载器)。

toClass( ) 是为了方便而提供的。 如果你需要更复杂的功能,你应该写你自己的类加载器。


3.2 Class loading in Java

在Java中,多个类加载器可以共存,每个类加载器都创建自己的名称空间。不同的类加载器可以使用相同的类名加载不同的类文件。被类加载器装载的两个类被认为是不同的两个类。此功能使我们能够在单个JVM上运行多个应用程序,即使这些程序包含具有相同名称的不同类。

注意:
JVM不允许动态重载一个类。 
一旦类加载器加载一个类,它就不能在运行时重新加载该类的修改版本。因此,在JVM加载之后,您不能改变类的定义。
然而,JPDA(Java Platform Debugger Architecture))提供了为重新加载类提供了有限的能力。

如果同一个类文件被两个不同的类加载器加载,则JVM将创建两个具有相同名称和定义的不同类。这两个Class被认为是不同的Class。由于两个类不相同,所以一个类的实例不能分配给另一个类的变量。如果你这样这样做了,则两个类之间的转换操作会失败并抛出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类型的不同。

多个类加载器形成一个树形结构。Each class loader except the bootstrap loader has a parent class loader, which has normally loaded the class of that child class loader. 由于加载类的请求可以沿着这个类加载器的层次结构进行委托,所以一个类可以通过一个类加载器加载,并且这个正在被加载的类不能够被请求。因此,被请求加载类“C”的类加载器可能与实际加载类”C”的加载器不同。为了区分,我们称前加载器为C的启动器,我们称后加载器为C的实际加载器。

此外,如果请求加载类C(C的发起者)的类加载器CL委派给父类加载器PL,则类C的定义中引用的任何类定义都不会被类加载器CL所加载。CL不是这些类的发起者。相反,父类加载器PL成为它们的启动器,并且请求加载它们。 C类是由C的实际加载器加载的。

为了理解上面的意思,看个例子:

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(); }
}

假设类加载器L加载了一个类Window。类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 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。 你必须避免让两个类加载器加载同一个类。 两个装载者必须有一个委托给另一个。

如果L在加载Point时没有委托给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 private
    public int getX() { return x; }
        :
}

3.3 Using 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( )方法。 在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调用这些方法来获取类文件。

要在应用程序类MyApp中使用MyTranslator对象,请按以下方式编写一个主类:

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.Loader的搜索顺序允许Javassist加载修改后的类。 但是,如果由于某种原因无法找到修改的类,它将委托给父类加载器。 一旦父类加载器加载了一个类,那么该类中引用的其他类也将由父类加载器加载,因此它们不会被修改。 回想一下,类“C”中引用的所有类都是由C的真实加载器加载的。如果程序未能加载修改后的类,则应确保使用该类的所有类是否已由javassist.Loader加载。


3.4 Writing a class 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 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 Modifying a system class

像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二进制代码许可证。


3.6 Reloading a class at runtime

如果在启用JPDA(Java Platform Debugger Architecture)的情况下启动JVM,则可以动态地重载一个类,在JVM加载一个类之后,可以卸载旧版本的类定义,并且可以重新加载一个新类。也就是说,该类的定义可以在运行时动态修改。但是,新的类定义必须与旧的定义兼容。JVM不允许在两个版本之间进行架构更改,他们要有相同的方法和字段。

Javassist为在运行时重新加载类提供了一个方便的类。 有关更多信息,请参阅javassist.tools.HotSwapper的API文档。

你可能感兴趣的:(javassist,javassist,javassist翻译)