javassist文档翻译

1、读和写字节码

Javassist是一个处理Java字节码的库,java字节码是使用二进制格式存储在文件中的话,我们就称之为一个字节码文件,每个字节码文件包含着一个class类或一个interface。

Javassist.CtClass对应着一个对象的字节码文件,CtClass对象就是用来处理字节码文件的,以下就是一个非常简单的例子:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();

这段代码首先获取了一个用来控制字节码修改的ClassPool对象,ClassPool对象是一个CtClass对象的集合,这个类可以读取一个字节码文件,并构造出一个CtClass对象,我们使用CtClass便可以修改一个已经定义的类。

上面的例子中,CtClass对象代表着一个从classPool类中拿到的test.Rectangle字节码对象,并定义成cc,ClassPool通过getDefault方法从默认的系统的搜索路径中获取的。

ClassPool 是一个包含CtClass对象的hash表结构,它通过get()方法通过特定的key值从hash表里拿CtClass,如果没有找到的话,它变会构建一个新的CtClass。

CtClass对象是可以修改的,上面的例子中它的父类被修改成"test.Point"类,如果writeFile()方法被执行的话,最终它代表的类的字节码文件将会被修改。

Javassist也提供了一个直接获取修改字节码文件后的字节数组

byte[] b = cc.toBytecode()

同时也可以通过加载toClass()方法拿到对应的字节码对象,toClass()代表请求当前线程的ClassLoader去加载CtClass代表的字节码文件。

  • Frozen classes

如果一个CtClass对象被writeFile()、toClass()、toBytecode()转化成一个class文件,Javassist冻住了该CtClass对象的话,后续我们就不能修改这个CtClass对象了,这是为了提醒开发者,JVM并不允许我们修改一个已经被加载的字节码文件。

一个处于冰冻状态的CtClass如果执行defrost()方法后,就接触冰冻状态了,这个CtClass对象又允许我们修改了。

CtClasss cc = ...;
    :
cc.writeFile();
cc.defrost();
cc.setSuperclass(...);    // OK since the class is not frozen.

当ClassPool.doPruning 设置为true时,当Javassist冰冻这个对象时,Javassist可以修改CtClass代表的类结构,这样是为了减少内存消耗,删除一些类中不必要的属性,例如代码的属性结构-方法体,因此当一个CtClass对象被修剪后,方法的字节码是不可以访问的,但方法名,签名和注解除外。已经被修剪过的CtClass
是不能再次被解冻的。另外ClassPool的doPruning字段默认是false。

为了禁止修剪一个特定的CtClass,建议一定要像下面一样调用StopPruning()
方法:

CtClasss cc = ...;
cc.stopPruning(true);
    :
cc.writeFile();                             // convert to a class file.
// cc is not pruned.

CtClass对象cc是不运行被修剪的,因此在调用writeFile方法被调用后,它还能够被解冻。

注意,当debug的时候,你可能想暂时暂停修剪并且冰冻和把一个被修改的class文件写进硬盘,此时用debugWriteFile()便可以达到此目的,当停止修剪,便把该CtClass写进硬盘,解冻它,此时又可以修改它。

  • 类搜索路径
    classPool的getDefault()返回的ClassPool,是通过JVM的默认class加载路径去搜索的。如果一个运行在服务器的应用,例如JBoss和Tomcat,ClassPool对象可能并不能找到用户的字节码,因为这样一个远程服务器使用多个classLoader类加载器,这种情况下,我们必须把classPath注册进ClassPool
pool.insertClassPath(new ClassClassPath(this.getClass()));

你可以注册一个路径作为类搜索路径,例如:

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath("/usr/local/javalib");

当然我们还可以添加一个URL作为类搜索路径:

ClassPool pool = ClassPool.getDefault();
ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist.");
pool.insertClassPath(cp);

这个程序添加“http://www.javassist.org:80/java/”作为字节码搜索路径,这个URL被当做搜索属于org.javassist包下的classes,例如为了加载一个org.javassist.test.main,它的字节码文件可以从下面的路径中获取:

http://www.javassist.org:80/java/org/javassist/test/Main.class

此外,你还可以使用ByteArrayClassPath直接拿一个byte数组构造出一个CtClass对象,例如

ClassPool cp = ClassPool.getDefault();
byte[] b = a byte array;
String name = class name;
cp.insertClassPath(new ByteArrayClassPath(name, b));
CtClass cc = cp.get(name);

如果你不知道类的全名称,然后你可以使用ClassPool的makeClass()方法构造出CtClass对象。

ClassPool cp = ClassPool.getDefault();
InputStream ins = an input stream for reading a class file;
CtClass cc = cp.makeClass(ins);

2、ClassPool

避免OOM
ClassPool如果加载了很多内存较大的对象,可能会导致很大的内存消耗,
为了避免这种问题,你可以使用CtClass类的detch()方法,把CtClass对象从ClassPool中移除,例如:

CtClass cc = ... ;
cc.writeFile();
cc.detach();

如果一个CtClass对象已经被detach了,那么就不能调用它的任何方法了,
然而,你可以调用get()方法从ClassPool中new一个新的同样的CtClass对象。当然我们也可以自己new一个ClassPool,如下:

ClassPool cp = new ClassPool(true);
// if needed, append an extra search path by appendClassPath()

3、Class Loader

  • 3.1 CtClass的toClass方法

CtClass有一个非常方便的toClass方法,这个方法会请求当前线程的classLoader去加载CtClass对象对应的对象,调用的时候必须要有合适的权限,否则的话会抛出SecurityException异常,下面的例子会展示如何使用CtClass()方法。

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

如果在执行toClass()方法之前,Hello类被加载过,那么程序将抛出LinkageError异常,因为class loader不能同时加载两个不同版本的hello class 类, 例如:

public static void main(String[] args) throws Exception {
    Hello orig = new Hello();
    ClassPool cp = ClassPool.getDefault();
    CtClass cc = cp.get("Hello");
        :
}

如果程序运行在服务器端,即存在多个类加载器的情况,toClass()使用默认的ClassLoader加载类可能会出现ClassCastException,为了避免这种异常,我们应该给toClass()方法设置一个合理的ClassLoader去加载类,例如:

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

不同的类加载器加载的类,及时类的名称一样,但其实还是两个不同的类。如果JVM加载一个类后,会做一次强制转换,有可能便会抛出强制转换的异常。如下面代码所示:

MyClassLoader myLoader = new MyClassLoader();
Class clazz = myLoader.loadClass("Box");
Object obj = clazz.newInstance();
Box b = (Box)obj;    // this always throws ClassCastException.

在我们现实开发中要尽量避免出现多个classLoader加载两个名字一样的类的情况。

  • 3.2 使用javassist.Loader

Javassist提供了一个javassist.Loader的类加载器,这个类加载器使用一个ClassPool对象读取一个字节码文件。下面是Javassist.Loader加载一个已经修改的特定类例子:

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

如果一个用户想按照需要修改一个已经加载的类,用户可以给javassist.Loader添加一个监听,当classLoader加载类的时候,事件监听将会进行回调。这个事件监听必须实现下面的接口:

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

start()方法将会在这个listener添加到javassist.Loader对象时执行,也就是javassist.Loader的addTranslator()方法被调用时执行;
onLoad()方法会在javassist.Loader加载一个class之前调用。

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("com.lianjia.link.mytransformplugin.Test", args);
  }
}

Test类的代码如下:

 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("com.lianjia.link.mytransformplugin.Hello");
    CtMethod m = cc.getDeclaredMethod("say");
    m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
    Class c = cc.toClass();
    Hello h = (Hello)c.newInstance();
    h.say();
  }

}

上面代码是用javassist.Loader去加载Test这个类,但Test这个类是系统的ClassLoader已经加载过的,所以上述代码会报错。

  • 3.3 自定义一个classLoader
import javassist.*;

import java.io.IOException;

public class SampleLoader extends ClassLoader {
  /* Call MyApp.main().
   */
  public static void main(String[] args) throws Throwable {
    SampleLoader s = new SampleLoader();
    Class c = s.loadClass("com.lianjia.link.mytransformplugin.Test");
    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();
    }
  }
}

执行结果:

Hello.say():

Hello

Process finished with exit code 0

4、定制化修改类的结构

Javassist和java反射的api很类似,CtClass提供了getName(),getSuperClass(),getMethods(),同事CtClass也提供了修改类的方法,Javassist也允许添加一个新的字段、构造方法和普通方法,构造一个方法体也是可能的。
Methods在Javassist中对应着CtMethod类,它提供了一些修改类方法的功能,注意如果一个方法是集成基类的,要修改这类的话,要通过代表基类的CtMethod来实现。一个CtMethod对象对应着每一个定义的方法。

Javassist不允许移除一个方法或字段,但是它允许改变方法和字段的名称,如果一个方法不再需要之后,它应该通过CtMethod的setName()方法和setModifiers()方法,被重命名和修改成私有的。
Javassist不允许给已经存在的方法添加一个额外的参数,如果非要给现有方法添加参数,你可以定义一个新的方法去实现。

Javassist同时也提供了api直接来修改一个类的字节码文件,例如通过CtClass的getClassFile()方法返回一个代表不成熟字节码文件的ClassFile对象,通过CtMethod的getMethodInfo()方法返回一个代表方法结构的MethodInfo对象,这些api使用到了java虚拟机定义的语法,所以我们必须要对字节码知识有所了解。详情请看javassist.bytecode package.

Javassist修改字节码文件要用到Javassist.runtime包,它支持在程序运行的时候,使用一些包含$的特殊标识符,下面我们会介绍这些特殊运算符的用法,需要了解更多的话,可以了解下javassist.runtime包的文档。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 227,572评论 6 531
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 98,071评论 3 414
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 175,409评论 0 373
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 62,569评论 1 307
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,360评论 6 404
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,895评论 1 321
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,979评论 3 440
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,123评论 0 286
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,643评论 1 333
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,559评论 3 354
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,742评论 1 369
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,250评论 5 356
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,981评论 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,363评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,622评论 1 280
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,354评论 3 390
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,707评论 2 370