Javassist-官方文档中文翻译-第三章-ClassLoader详解

这是把官方的文档给翻译了,顺便学习,一共10章,可以到下面地址查看,里面可能有翻译不准的地方,欢迎指正

https://github.com/IndustriousSnail/javassist-learn

简介

如果一开始你就知道要修改哪个类,那么最简单的方式如下:

  • 1.调用ClassPool.get() 来获取一个CtClass对象。
  • 2.修改它
  • 3.调用writeFile()toBytecode() 来获取一个修改后的class文件

如果一个类是否要被修改是在加载时确定的,用户就必须让Javassist和类加载器协作。Javassist可以和类加载器一块儿使用,以便于可以在加载时修改字节码。用户可以自定义类加载器,也可以使用Javassist提供好的。

3.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");
        // 获取say方法
        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()Hellosay() 方法的方法体中插入了println() 的调用。然后构建了被修改后的Hello的实例,然后调用了该实例的say() 方法。

注意,上面这段程序有一个前提,就是Hello类在调用toClass() 之前没有被加载过。否则,在toClass() 请求加载被修改后的Hello类之前,JVM就会加载原始的Hello类。因此,加载被修改后的Hello类就会失败(抛出LinkageError)。例如:

public static void main(String[] args) throws Exception {
    Hello orig = new Hello();
    ClassPool cp = ClassPool.getDefault();
    CtClass cc = cp.get("Hello");
    Class c = cc.toClass();  // 这句会报错
}

main函数的第一行加载了Hello类,cc.toClass() 这行就会抛出异常。原因是类加载器不能同时加载两个不同版本的Hello类。

如果你的程序运行在JBOSS或Tomcat的应用服务器上,那么你再用toClass() 就有点不合适了。这种情况下,将会抛出ClassCastException异常。为了避免这个异常,你必须给toClass() 一个合适的类加载器。例如,假设bean是你的会话bean对象,那么这段代码:

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

这段代码可以正常运行。你应该给toClass() 的类加载器是加载你程序的加载器(上面的例子中,就是bean对象的class的类加载器)。

toClass() 已经很方便了。你要是想更复杂的类加载器,你应该自定义类加载器。

3.2 Java中的类加载

在Java中,多个类加载器可以共存,它们可以创建自己的命名空间。不同的类加载器能够加载有着相同类名的不同的类文件。被加载过的两个类会被视为不同的东西。这个特点可以让我们在一个JVM中运行多个应用程序,尽管它们包含了有着相同名称的不同的类。

JVM不允许动态重新加载一个类。一旦类加载加载过一个类之后,在运行期就不能在加载该类的另一个被修改过的版本。因此,你不能在JVM加载过一个类之后修改它的定义。但是,JPDA(Java Platform Debugger Architecture)提供了重新加载类的一些能力。详细请看3.6

如果两个不同的类加载器加载里一个相同的Class文件,那么JVM会生成两个不同的Class,虽然它们拥有相同的名字和定义。这两个Class会被视为两个不同的东西。因为这两个Class不是完全相同的,所以一个Class的实例不能赋值给另一个Class的变量。这两个类之间的类型转换会失败,抛出ClassCastException异常。

例如,下面这个代码片段就会抛出该异常:

MyClassLoader myLoader = new MyClassLoader();
Class clazz = myLoader.loadClass("Box");
Object obj = clazz.newInstance();
Box b = (Box)obj;    // 这里总是会抛出ClassCastException异常.

Box类被两个类加载器所加载。假定CL类加载器加载了这段代码片段。因为该代码中引用了MyClassLoader,Class,Object,所以CL也会加载这些类(除非它代理了其它啊类加载器)。因此,b 变量的类型是CL加载的Box。但是obj变量的类型是myLoader加载的Box,虽然都是Box,但是不一样。所以,最后一段代码一定会抛出ClassCastException,因为bobj是两个不同版本的Box

多个类加载形成了一个树型结构。除了启动加载器之外,其他的类加载器都有一个父类加载,子类加载器通常由父类加载器加载。由于加载类的请求可以沿着加载器的层次结构进行委托,所以你请求加载类的加载器,并不一定真的是由这个加载器加载的,也可能换其他加载器加载了。因此(举例),请求加载类C的加载器可能不是真正加载类C的加载器。不同的是,我们将前面的加载器称为C的发起者(initiator),后面的加载器称为C实际的加载器(real loader)。

除此之外,如果类加载器CL请求加载一个类C(C的发起者)委托给了它的父类加载器PL,那么类加载器CL也不会加载类C定义中引用的任何其他类。对于那些类,CL不是它们的发起者,相反,父加载器PL则会称为它们的发起者,并且回去加载它们。类C定义中引用的类,由类C的实际的加载器去加载。

要理解上面的行为,可以参考下面代码:

public class Point {    // PL加载该类
    private int x, y;
    public int getX() { return x; }
        :
}

public class Box {      // L是发起者,但实际的加载器是PL
    private Point upperLeft, size;
    public int getBaseX() { return upperLeft.x; }
        :
}

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

假定类加载器L加载Window类。加载Window的发起者和实际加载者都是L。因为Window的定义引用了类Box,所以JVM会让 L去加载Box类。这里,假定L将该任务委托给了父类加载器PL,所以加载Box的发起者是L,但实际加载者是PL。这种情况下,PL作为Box的实际加载者,就会去加载Box中定义中引用的Point类,所以Point的发起者和实际加载者都是PL。因此加载器L从来都没有请求过加载Point类。

把上面的例子稍微改一下:

public class Point {
    private int x, y;
    public int getX() { return x; }
        :
}

public class Box {      // 发起者是L,但实际加载者是PL
    private Point upperLeft, size;
    public Point getSize() { return size; }
        :
}

public class Window {    // Window由加载器L加载
    private Box box;
    public boolean widthIs(int w) {  // 增加了方法,方法中有对Point类的引用。
        Point p = box.getSize();
        return w == p.getX();
    }
}

上面中,Window也引用了Point。这样,如果加载器L需要加载Point的话,L也必须委托给PL你必须避免让两个类加载器重复加载同一个类。两个加载器中的一个必须委托给另一个加载器。

如果当Point被加载时,L没有委托给PL,那么widthIs()就会抛出ClassCastException。因为Window里的PointL加载的,而Box中的PointPL加载器加载的。你用box.getSize() 返回的PL.PointL的Point,那么就会JVM就会认为它们是不同的实例,进而抛出异常。

这样有些不方便,但是需要有这种限制。比如:

Point p = box.getSize();

如果这条语句没有抛出异常,那么Window的代码就有可能打破Point的封装。例如,PL加载的Pointx变量是private,但是L加载器加载的Pointx变量是public(下面的代码定义),那么不就打破了封装定义。

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.

3.3 使用javassist.Loader

Javassist提供了一个类加载器javasist.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的一个新实例。

如果用户想根据需要在类被加载的时候修改类,那么用户可以增添一个事件监听器给javassist.Loader。该事件监听器会在类加载器加载类时被通知。事件监听器必须实现下面这个接口:

public interface Translator {
    public void start(ClassPool pool)
        throws NotFoundException, CannotCompileException;
    public void onLoad(ClassPool pool, String classname)
        throws NotFoundException, CannotCompileException;
}

当使用javassist.LoaderaddTranslator() 方法增添事件监听器时,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对象的application(带main方法,可以运行的)类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这样的应用类不能访问加载器的类,不如Main2MyTranslatorClassPool。因为他们是被不同的加载器加载的。应用类时javassist.Loader加载的,然而像Main2这些是被默认的java类加载器加载的。

javassist.Loader搜索类的顺序和java.lang.ClassLoader.ClassLoader不同。JavaClassLoader首先会委托父加载器进行加载操作,父加载器找不到的时候,才会由子加载器加载。而javassist.Loader首先尝试加载类,然后才会委托给父加载器。只有在下面这些情况才会进行委托:

  • 调用get()方法后在ClassPool对象中找不到
  • 使用delegateLoadingOf() 方法指定要父类加载器去加载

这个搜索顺序机制允许Javassist加载修改后的类。然而,如果它因为某些原因找不到修改后的类的话,就会委托父加载器去加载。一旦该类被父加载器加载,那么该类中引用的类也会用父加载器加载,并且它们不能再被修改了。回想下,之前类C的实际加载器加载了类C所有引用的类。如果你的程序加载一个修改过的类失败了,那么你就得想想是否那些类是否使用了被javassist.Loader加载的类。

3.4 自定义一个类加载器

一个简单的类加载器如下:

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文件到 ./class 目录下,该目录不能包含在类搜索路径下。否则,MyApp.class将会被默认的系统类加载器加载,也就是SampleLoader的父类加载器。你也可以把insertClassPath中的 ./class 放入构造函数的参数中,这样你就可以选择自己想要的路径了。 运行java程序:

> java SampleLoader

类加载器加载了类MyApp(./class/MyApp.class),并且调用了MyApp.main() ,并传入了命令行参数。

这是使用Javassist最简单的方式。如果你想写个更复杂的类加载器,你可能需要更多的java类加载机制的知识。例如,上面的程序把MyApp的命名空间和SampleLoader的命名空间是分开的,因为它们两个类是由不同的类加载器加载的。因此,MyApp不能直接访问SampleLoader类。

3.5 修改系统类

除了系统类加载器,系统类不能被其他加载器加载,比如java.lang.String。因此,上面的SampleLoaderjavassist.Loader在加载期间不能修改系统类。

如果你的程序非要那么做,请“静态的”修改系统类。例如,下面的程序给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 Runtime Environment binary code 协议。

3.6 运行期重新加载一个类

启动JVM时,如果开启了JPDA(Java Platform Debugger Architecture),那么class就可以动态的重新加载了。在JVM加载一个类之后,旧的类可以被卸载,然后重新加载一个新版的类。意思就是,类的定义可以在运行期动态的修改。但是,新类一定要能和旧类相互兼容。JVM不允许两个版本存在模式的改变,它们需要有相同的方法和属性。

Javassist提供了一个很方便的类,用于在运行期改变类。想了解更多信息,可以看javassist.tools.HotSwapper的API文档

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

iioSnail

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值