Java字节码Javassist之ClassLoader(三)

文章目录

  • toClass方法
  • Java中的类加载
  • 使用javassist.Loader
  • 编写类加载器
  • 修改一个系统类
  • 运行时重新加载类

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

  • 1.通过调用ClassPool.get()获取一个CtClass对象。
  • 2.修改它。
  • 3.在该CtClass对象上调用writeFile()或toBytecode()以获得修改后的类文件。

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

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

那么原来的Hello类在main的第一行被加载,对toClass()的调用会抛出一个异常,因为类加载器不能同时加载两个不同版本的Hello类。
  如果程序运行在JBoss和Tomcat等应用服务器上,那么toClass()使用的上下文类加载器可能不合适。在这种情况下,您将看到一个意外的ClassCastException。为了避免这种异常,必须显式地为toClass()提供适当的类加载器。例如,如果bean是会话bean对象,那么以下代码:

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

你应该给class()一个已经加载了你的程序的类加载器。提供toClass()是为了方便。如果您需要更复杂的功能,您应该编写自己的类加载器。

Java中的类加载

  在Java中,多个类加载器可以共存,每个类加载器创建自己的名称空间。不同的类加载器可以装入具有相同类名的不同类文件。加载的两个类视为不同的类。这个特性使我们可以在同一个JVM中运行多个应用即使这个应用程序包含具有相同名字的不同的类。
  注意: JVM不允许动态地重新加载一个类。类加载器加载类后,就不能在运行时期间加载该类的修改版本。因此,您不能在JVM加载类之后更改类的定义。然而,JPDA (Java平台调试器体系结构)为重新加载类提供了有限的能力。
  如果相同的类文件由两个不同的类加载器器加载,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的类与用作th的Box类是不同版本的。
  多个类加载器呈现树形结构。除引导类加载器外,每个类加载器都有一个父类加载器,父类加载器通常装入该子类加载器的类。由于加载器类的请求可以沿着类加载器的这个层次结构进行委托,因此类可以由您没有请求类加载的类加载器装入。因此,被请求装入类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加载的,windows的启动器和真正的加载器都是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被请求装入Point,它也必须委托给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对象的封装。例如,在PL加载的Point中,x字段是私有的。然而,如果L用以下定义加载Point, Window类可以直接访问x的值:

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.Loader

  Javassist提供了一个类加载器Javassist . loader。这个类加载器使用javassist。用于读取类文件的ClassPool对象。
例如,javassist加载器可用于加载用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时,方法start()被调用。当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);
    }
}

  javassist.Loader搜索类的顺序与java.lang.ClassLoader不同。ClassLoader首先将装入操作委托给父类加载器,然后仅在父类加载器找不到类时才尝试装入这些类。
javassist.Loader尝试在委托给父类加载器之前加载类。它仅在以下情况下进行委托:

  • 在ClassPool对象上调用get()不能找到类。
  • 这些类是通过使用delegateLoadingOf()指定的,由父类加载器装入。

  这个搜索顺序允许通过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 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();
        }
    }
}

% java SampleLoader
类加载器装入类MyApp (./class/MyApp.class),并使用命令行参数调用MyApp.main()
  这是使用Javassist的最简单方法。但是,如果要编写更复杂的类加载器,则可能需要详细了解Java的类加载机制。例如,上面的程序将MyApp类放在与SampleLoader类所属的名称空间分离的名称空间中,因为这两个类是由不同的类加载器装入的。因此,MyApp类不能直接访问类SampleLoader。

修改一个系统类

  像java.lang.String这样的系统类不能由系统类加载器以外的类加载器装入。因此SampleLoader或javassist上面显示的加载器不能在加载时修改系统类。
  如果应用程序需要这样做,则必须静态修改系统类。例如,下面的程序在java.lang.String中添加了一个新的字段hiddenValue:

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运行时环境二进制代码许可。

运行时重新加载类

  如果JVM启动时启用了JPDA (Java平台调试器体系结构),则可以动态地重新加载类。JVM加载类之后,可以卸载旧版本的类定义,并重新加载新版本的类定义。也就是说,该类的定义可以在运行时期间动态修改。但是,新的类定义必须在一定程度上与旧的类定义兼容。JVM不允许在两个版本之间更改模式。它们具有相同的方法和字段集。
  Javassist为在运行时重新加载类提供了一个方便的类。有关更多信息,请参阅javassist.tools.HotSwapper的API文档。


[资料来源]  http://www.javassist.org/tutorial/tutorial.html

你可能感兴趣的:(Java,java,jvm,tomcat)