美文网首页我爱编程程序员
Javassist之Classloader(一)

Javassist之Classloader(一)

作者: bdqfork | 来源:发表于2018-06-19 20:43 被阅读0次

    如果需要修改的类是预先知道的,最简单的修改方式如下:

    1. 调用ClassPool.get()获取CtClass对象
    2. 修改
    3. 调用CtClass对象的writeFile()或者toBytecode()来获取已修改的类文件

    如果在加载时不确定类是否已被修改,用户一定要配合Classloader来使用Javassist。Javassist可以和Classloader一起使用,这样就可以在加载时修改字节码。Javassist的使用者可以定义自己的Classloader,也可以使用Javassist提供的Classloader。

    1.CtClass的toClass方法

    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()在方法say()的方法体中插入了一个println()调用。然后它构建了一个修改后的Hello类的实例并调用了say()方法。

    注意上面这段程序能运行的前提是Hello类在toClass()被调用前没有被加载。如果不是,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");
            :
    }
    

    原始的Hello在main的第一行被加载,所以在调用toClass()方法时会【抛出一个异常,因为类加载器无法同时加载两个不同版本的Hello类。

    如果程序运行在一些应用服务器,如JBoss和Tomcat,toClass()使用的上下文类加载器就不是很适合。在这样的环境下,你会看到ClassCastException。为了避免这个异常,你必须明确给出一个合适的类加载给toClass()。例如,如果bean是你的会话对象,那么下面的代码:

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

    可以工作。你应该将加载你程序(在上面的例子中,指的是bean对象的类)的类加载器传给toClass()。

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

    2. Java的类加载

    在Java中,多个类加载器同时存在,每一个加载器创建一个自己的命名空间。不同的类加载器可以加载类名相同的不同类文件。被加载的两个类被视为不同的类。这个特性允许我们在一个JVM上运行多个应用程序,即使程序包含多个类名相同的类。

    注意:JVM不允许动态重载类。一旦类加载一个类,它在运行期间不能重新加载一个修改过的类。因此,你不能在JVM加载类之后再修改类的定义。然而JPDA(Java Platform Debugger Architecture)提供了有限的类重载。

    如果相同的类文件被两个不同的类加载器加载,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的类型和变量b的类型不是一个版本的Box类。

    多个类加载器形成了一课构造书。除了bootstrap loader,每一个类加载器都拥有一个父加载器,它可以加载子类加载器的类。因为加载类的请求可以被委托给这个层次的类加载器,所以一个类可能会被你没有请求的类加载器加载。因此,请求加载类C的类加载器可能与实际加载这个类C的加载器不同。为了区分,我们可以称前一个加载器为C的发起者,后一个加载器称为C的实际上的加载器。

    此外,如果类加载器CL把加载类C的请求(C的发起者)委托给父加载器PL,然后类加载器CL再也不会被请求去加载任何一个类C定义的类了。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(); }
    }
    

    加速一个类Window被类加载器L加载。发起者和实际加载器都是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。在这种情况下,类加载器L也必须委托PL,如果它被请求去加载Point。你必须避免两个类加载器同时加载同一个类。两个加载中的一个必须委托给另一个。

    如果当Point被加载,L没有委托给PL,widthIs()会抛出一个ClassCastException。因为Box实际的加载器是PL,Box引用的Point也是被PL加载。因此,getSize()的结果是由PL加载的Point实例,反之widthIs()中的变量p的类型是被L加载的Point。JVM把它们当作两个不同的类型并因此抛出一个类型不匹配的异常。

    这个机制是不方便但是必须的。如果下面的语句:

    Point p = box.getSize();
    

    没有抛出异常,然后Window的编程人员可以破坏Point对象的封装性。例如,在被PL加载的Point中的的成员变量x是私有的。然而,类Window可以直接访问x的变量,如果L用下面的定义加载Point:

    public class Point {
        public int x, y;    // not private
        public 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之Classloader(一)

        本文链接:https://www.haomeiwen.com/subject/clyzeftx.html