javassist编程指南==Class loader 类加载

2023-05-05 19:17:59 浏览数 (2)

Class loader 类加载

如果必须修改的类是预先知道的,最简单的修改类的方式可能是以下这些:

  • 1.通过调用ClassPool.get()获取一个CtClass对象。
  • 2.修改它,并且
  • 3.调用CtClass对象的writeFile()或者toBytecode()方法获得修改后的class文件

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

CtClasstoClass方法

CtClass提供了一个便捷的方法toClass,请求当前线程的类加载器去加载CtClass表示的类。调用此方法必须具有适应的权限,否则会抛出一个SecurityException异常。

代码语言:javascript复制
package org.byron4j.cookbook.javaagent;

public class Javassist3ClassLoader {

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

测试类:

代码语言:javascript复制
package org.byron4j.cookbook.javaagent;

import javassist.*;

public class Javassist3ClassLoaderTest {

    public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {
        ClassPool cp = ClassPool.getDefault();
        CtClass cc = cp.get("org.byron4j.cookbook.javaagent.Javassist3ClassLoader");
        CtMethod m = cc.getDeclaredMethod("say");

        m.insertBefore("System.out.println("Ready to say:");");
        Class c = cc.toClass();
        Javassist3ClassLoader javassist3ClassLoader = (Javassist3ClassLoader)c.newInstance();
        javassist3ClassLoader.say();
    }
}

首先,通过测试类的表示Javassist3ClassLoader类的CtClass修改其say方法,在方法体前面增加一行输出语句; 然后,通过CtClass的toClass方法请求当前线程(Javassist3ClassLoaderTest类所在的线程)去加载Javassist3ClassLoader类; 最后,通过Class对象的静态方法newInstance构造一个Javassist3ClassLoader对象,并调用其say方法,得到字节码修改后的方法执行内容结果。

注意: 上面的程序依赖于Javassist3ClassLoaderTest类所在的类加载器在调用toClass之前没有加载过Javassist3ClassLoader类。

如果程序运行在web容器中例如JBoss、Tomcat中, 上下文的类加载器使用toClass()方法可能并不适当。在这种情况下,你可能会看到一个不期望的异常ClassCastException。为了避免这种情况,你必须明白清楚地给定一个适当的类加载器给toClass方法。例如,如果bean是你的会话的bean对象:

代码语言:javascript复制
CtClass cc = ...
Class c = cc.toClass(bean.getClass().getClassLoader());

java中的类加载

在Java,多个类加载器可以共存,每个类加载器创建自己的命名空间。不同的类加载器可以加载具有相同类名的不同class文件,加载的两个类视为不同的类,这一个特性保证我们可以在一个JVM中运行多个应用程序即使这些程序包含相同类名的不同类实例。

注意: JVM不允许动态的重新加载一个类。一旦一个类加载器加载了一个类后,它就不能在运行时再重新加载一个新的版本的类了。 因此,你不能在JVM加载类后,再去变更类的定义。 但是,JPDA(Java平台调试架构)提供了有限的类重加载能力。

如果相同的class文件被不同的类加载器加载了,JVM会使用相同的名称和定义创建两个不同的类,这两个类会被看做是不同的。既然这两个类是不同的,所以一个类的实例就不能分配给另一个类类型的变量了。

代码语言:javascript复制
MyClassLoader myLoader = new MyClassLoader();
Class clazz = myLoader.loadClass("Box");
Object obj = clazz.newInstance();
Box b = (Box)obj;

多个类加载器形成一个树结构: 每个类加载器(引导加载器BootstrapClassLoader除外)都有一个父的类加载器(通常是加载了该子类加载器的类)。因为请求去加载一个类可以沿着这个类加载器层级委托,一个类可以被不是你请求的类加载器去加载。因此,被请求去加载一个类C的类加载器和实际加载这个类C的加载器可能不是同一个类加载器。以示区别,我们将前面的加载器称为C的启动器,后面的称为C的真实加载器。 此外,如果一个类加载器CL被请求去加载一个类C(C的启动器)委托给了它的父类加载器PL,之后,类加载器CL则再也不会被请求去加载类C定义中引用的任何类。 CL不是类C的引用的类的启动器,相反,PL成为了类C的引用的类的启动器,且PL将会被请求去加载它们。类C的定义的引用的类将会被类C的真实加载器去加载。

为了解释这个行为,我们思考下以下示例:

代码语言:javascript复制
public class Point {    // 被父类加载器PL加载
    private int x, y;
    public int getX() { return x; }
        :
}

public class Box {      // 引导器是CL,但是真实加载器是PL
    private Point upperLeft, size;
    public int getBaseX() { return upperLeft.x; }
        :
}

public class Window {    // 被类加载器CL加载
    private Box box;
    public int getBaseX() { return box.getBaseX(); }
}

假设一个类Window被一个类加载器CL加载了,则它的引导器和真实加载器都是CL。因为类Window的定义引用了类Box,JVM将会请求CL去加载Box。在这里,假设CL将这个任务委托给父加载器PL。Box类的引导器是CL但是真实加载器是PL。在这个案例中,Point类的引导器不是CL而是PL,因为它与Box的真实加载器相同。CL再也不会被请求去加载Point

再看一个有些细微差异的示例:

代码语言:javascript复制
public class Point {
    private int x, y;
    public int getX() { return x; }
        :
}

public class Box {      // 引导器是CL,但是真实加载器是PL
    private Point upperLeft, size;
    public Point getSize() { return size; }
        :
}

public class Window {    // 被CL加载
    private Box box;
    public boolean widthIs(int w) {
        Point p = box.getSize();
        return w == p.getX();
    }
}

现在Window类的定义也引用了Point类,在这个案例中,CL在被请求加载Point时也将委托给PL。***你必须避免存在两个不同的类加载器重复加载同一个类。***,二者中的其中一个必须委托给另外一个。 如果在Point加载的时候,CL没有委托给PL,widthIs()将会抛出一个ClassCastException。因为Box的真实加载器是PL,Box中引用的类Point类也会被PL加载。因此,getSize()方法返回值是PL加载的Point的一个实例,然而getSize()方法中的变量是CL加载的Point类型,JVM将它们视作不同的类型,所以会抛出类型不匹配的异常。

这种行为有些不方便但是是可行的,如果以下语句:

代码语言:javascript复制
Point p = box.getSize();

不会抛出一个异常,则Window的程序猿就打破了Point类的封装性。比如,Point中的所有属性x是PL加载的。然而,Window类可以直接访问x的值,如果CL通过以下定义加载Point的话:

代码语言:javascript复制
public class Point {
    public int x, y;    // 非私有属性
    public int getX() { return x; }
        :
}

如果要获取更多关于Java中的类加载问题,以下论文可能是有帮助的:

代码语言:javascript复制
    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对象读取class文件。

例如,javassist.Loader可用于使用javassist修改的指定的类:

代码语言:javascript复制
ClassPool pool = ClassPool.getDefault();
// 使用ClassPool创建Loader
Loader cl = new Loader(pool);

CtClass ct = pool.get("org.byron4j.cookbook.javaagent.Rectangle");
ct.setSuperclass(pool.get("org.byron4j.cookbook.javaagent.Point"));

Class<?> c = cl.loadClass("org.byron4j.cookbook.javaagent.Rectangle");
Object o = c.newInstance();

这个程序修改了类Rectangle类,将其父类设置为Point类,然后程序加载了修改后的Rectangle类,并且创建了一个实例。

如果用户想在加载一个类的时候按需修改它,则用户可以添加一个javassist.Loader的事件监听器。当这个类加载器加载一个类的时候就会通知添加好的事件监听器。 事件监听器必须实现以下接口:

代码语言:javascript复制
/**
* Loader的观察者
*/
public interface Translator {
    /**
    * 当对象附加到加载器对象时,加载器将调用该对象进行初始化。此方法可用于获取(用于缓存)一些将在Translator的onLoad()中访问的CtClass对象。
    * @param pool
    * @throws NotFoundException
    * @throws CannotCompileException
    */
    void start(ClassPool pool)
            throws NotFoundException, CannotCompileException;
    
    /**
    * 当Loader加载一个类后,就会通知调用该方法。Loader会在<code>onLoad()</code>方法返回后调用
    * <pre>
    *     pool.get(classname).toBytecode()
    * </pre>
    * 方法去读取class文件,classname可能是尚未创建的类的名称。
    * 如果这样的话,<code>onLoad()</code>方法必须创建那个class,以便Loader可以在<code>onLoad()</code>方法返回后读取它。
    * @param pool
    * @param classname
    * @throws NotFoundException
    * @throws CannotCompileException
    */
    void onLoad(ClassPool pool, String classname)
            throws NotFoundException, CannotCompileException;
}

javassist.Loader对象的addTranslator()方法添加事件监听器的时候,start()方法就会被调用。 onLoad()方法会在javassist.Loader加载一个类之前被调用。 以下是这两种情况的源码:

代码语言:javascript复制
// 添加事件监听器的时候,就会调用监听器的start方法
public void addTranslator(ClassPool cp, Translator t)
    throws NotFoundException, CannotCompileException {
    source = cp;
    translator = t;
    t.start(cp);
}

// 存在监听器,则在Loader的findClass方法中,先执行监听器的onLoad()方法,再通过.get(name).toBytecode()加载类
if (source != null) {
    if (translator != null)
        translator.onLoad(source, name);

    try {
        classfile = source.get(name).toBytecode();
    }
    catch (NotFoundException e) {
        return null;
    }
}

所以,translator.onLoad的方法中可以修改加载的类的定义。

以下示例,事件监听器在将所有的类改为public修饰:

代码语言:javascript复制
public class MyTranslator implements Translator {
    @Override
    public void start(ClassPool pool) throws NotFoundException, CannotCompileException {

    }

    @Override
    public void onLoad(ClassPool pool, String classname) throws NotFoundException, CannotCompileException {
        // 在类加载前执行该方法,所以可以改变类的定义
        CtClass cc = pool.get(classname);
        cc.setModifiers(Modifier.PUBLIC);
    }
}

注意onLoad()方法没有去调用toBytecode()或者writeFile(),因为javassist.Loader会调用这些方法来获取class文件。

要使用MyTranslator来运行一个应用程序,main类可以如下编写:

代码语言:javascript复制
public class Point {
    public static void main(String[] args){
        System.out.println("org.byron4j.cookbook.javaagent.Point#main invoked!");
    }
}

public static void main(String[] args) throws Throwable {
    Translator t = new MyTranslator();
    ClassPool cp = ClassPool.getDefault();
    Loader loader = new Loader();
    loader.addTranslator(cp, t);
    // loader.run方法会运行指定classname的main方法
    loader.run("org.byron4j.cookbook.javaagent.Point", args);
}

运行输出: org.byron4j.cookbook.javaagent.Point#main invoked!

注意:应用的类像Point是不能访问加载器的类如MyTranslator、ClassPool的,因为它们是被不同的加载器加载的。应用的类是由javassist.Loader加载,而其他的是由默认的JVM类加载器加载的。

javassist.Loader以和java.lang.ClassLoader不同的顺序加载类。 ClassLoader首先将加载操作委托给父加载器,如果父加载器找不到它们才由自身尝试加载类。 反过来说,javassist.Loader在委托给父加载器之前尝试加载类。只有当:

  • 类不是由ClassPool.get()找到的,或者
  • 类使用了delegateLoadingOf()去指定由父加载器加载。

这个搜索顺序允许Javassist加载修改过的类。然而,如果加载失败的话就会委托给父加载器去加载。一旦一个类由其父加载器加载了,这个类引用的其它类也会由其父加载器加载,则这些类就不会被当前类加载器修改了。 回想一下,类C中所有引用的类都是由类C的真实加载器负责加载的。***如果你的程序不能加载一个修改过的类,***你应该确保所有使用该类的类都已经被javassist.Loader加载了。

编写一个类加载器

代码语言:javascript复制
public class SampleLoader extends ClassLoader {

    private ClassPool pool;

    public SampleLoader() throws NotFoundException {
        pool = new ClassPool();
        pool.insertClassPath("./class"); //下面加载的org.byron4j.cookbook.javaagent.Point类要在此路径下
    }


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        CtClass cc = null;
        try {
            cc = pool.get(name);
            // TODO 在这里可以修改类的定义
            
            byte[] bytes = cc.toBytecode();
            return defineClass(name, bytes, 0, bytes.length);
        } catch (NotFoundException e) {
            throw new ClassNotFoundException();
        }catch (IOException e) {
            throw new ClassNotFoundException();
        } catch (CannotCompileException e) {
            throw new ClassNotFoundException();
        }

    }

    public static void main(String[] args) throws NotFoundException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        SampleLoader s = new SampleLoader();
        Class<?> c = s.loadClass("org.byron4j.cookbook.javaagent.Point");
        c.getDeclaredMethod("main", new Class[]{String[].class}).invoke(null, new Object[]{args});

    }
}

假设Point是一个应用程序,为了执行这个程序,首先指定./class为class文件目录,当然该目录不能被包含在class查找路径中。否则Point.class会被系统默认的类加载器加载(是SampleLoader的父加载器)。构造器中insertClassPath()方法指定了目录名称./class,你可以使用不同的目录名称来代替你想要加载的类路径地址。 执行该程序,类加载器会加载Point类(./class/Point.class文件)并且调用其main方法。

这是使用javassist最简单的示例。然而,如果你想编写一个更加复杂的类加载器,你需要了解更多的java类加载的机制。例如,上面的程序将Point类在命名空间与SampleLoader命名空间分开了,因为这两个由不同的类加载器去加载。

修改一个系统类

系统类像java.lang.String除了系统加载器之外不能被其他类加载器加载。因此,SampleLoader或者javassist.Loader不能在加载时去修改系统类。

如果你的应用想那样去做的话(修改系统类),必须***静态地***修改系统类。例如,添加一个新的属性字段给java.lang.String:

代码语言:javascript复制
// 添加字段给系统类:java.lang.String
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get("java.lang.String");
// 字段
CtField cf = new CtField(CtClass.intType, "hiddenValue", ctClass);
cf.setModifiers(Modifier.PUBLIC);
ctClass.addField(cf);
ctClass.writeFile();

注意: 应用程序使用这个技术覆盖rt.jar中地系统类是违反JAVA2字节码规范地。

在运行时重加载一个类

启动JVM时启动了JPDA,则一个类可以重加载。在JVM加载一个类后,旧的版本的类的定义可以卸载,新的版本可以重新加载。 换言之,类的定义可以在运行时动态修改。然而,一个新的类的定义必须与旧的类定义在某种程度上兼容。 JVM不允许两个版本之间更改模式。 它们拥有相同的方法、成员变量。

Javassist提供了一个便捷的类可以在运行时重加载一个类:javassist.tools.HotSwapper

0 人点赞