javassist文檔翻譯

1、讀和寫字節(jié)碼

Javassist是一個處理Java字節(jié)碼的庫,java字節(jié)碼是使用二進制格式存儲在文件中的話,我們就稱之為一個字節(jié)碼文件,每個字節(jié)碼文件包含著一個class類或一個interface。

Javassist.CtClass對應(yīng)著一個對象的字節(jié)碼文件,CtClass對象就是用來處理字節(jié)碼文件的,以下就是一個非常簡單的例子:

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

這段代碼首先獲取了一個用來控制字節(jié)碼修改的ClassPool對象,ClassPool對象是一個CtClass對象的集合,這個類可以讀取一個字節(jié)碼文件,并構(gòu)造出一個CtClass對象,我們使用CtClass便可以修改一個已經(jīng)定義的類。

上面的例子中,CtClass對象代表著一個從classPool類中拿到的test.Rectangle字節(jié)碼對象,并定義成cc,ClassPool通過getDefault方法從默認的系統(tǒng)的搜索路徑中獲取的。

ClassPool 是一個包含CtClass對象的hash表結(jié)構(gòu),它通過get()方法通過特定的key值從hash表里拿CtClass,如果沒有找到的話,它變會構(gòu)建一個新的CtClass。

CtClass對象是可以修改的,上面的例子中它的父類被修改成"test.Point"類,如果writeFile()方法被執(zhí)行的話,最終它代表的類的字節(jié)碼文件將會被修改。

Javassist也提供了一個直接獲取修改字節(jié)碼文件后的字節(jié)數(shù)組

byte[] b = cc.toBytecode()

同時也可以通過加載toClass()方法拿到對應(yīng)的字節(jié)碼對象,toClass()代表請求當前線程的ClassLoader去加載CtClass代表的字節(jié)碼文件。

  • Frozen classes

如果一個CtClass對象被writeFile()、toClass()、toBytecode()轉(zhuǎn)化成一個class文件,Javassist凍住了該CtClass對象的話,后續(xù)我們就不能修改這個CtClass對象了,這是為了提醒開發(fā)者,JVM并不允許我們修改一個已經(jīng)被加載的字節(jié)碼文件。

一個處于冰凍狀態(tài)的CtClass如果執(zhí)行defrost()方法后,就接觸冰凍狀態(tài)了,這個CtClass對象又允許我們修改了。

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

當ClassPool.doPruning 設(shè)置為true時,當Javassist冰凍這個對象時,Javassist可以修改CtClass代表的類結(jié)構(gòu),這樣是為了減少內(nèi)存消耗,刪除一些類中不必要的屬性,例如代碼的屬性結(jié)構(gòu)-方法體,因此當一個CtClass對象被修剪后,方法的字節(jié)碼是不可以訪問的,但方法名,簽名和注解除外。已經(jīng)被修剪過的CtClass
是不能再次被解凍的。另外ClassPool的doPruning字段默認是false。

為了禁止修剪一個特定的CtClass,建議一定要像下面一樣調(diào)用StopPruning()
方法:

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

CtClass對象cc是不運行被修剪的,因此在調(diào)用writeFile方法被調(diào)用后,它還能夠被解凍。

注意,當debug的時候,你可能想暫時暫停修剪并且冰凍和把一個被修改的class文件寫進硬盤,此時用debugWriteFile()便可以達到此目的,當停止修剪,便把該CtClass寫進硬盤,解凍它,此時又可以修改它。

  • 類搜索路徑
    classPool的getDefault()返回的ClassPool,是通過JVM的默認class加載路徑去搜索的。如果一個運行在服務(wù)器的應(yīng)用,例如JBoss和Tomcat,ClassPool對象可能并不能找到用戶的字節(jié)碼,因為這樣一個遠程服務(wù)器使用多個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/”作為字節(jié)碼搜索路徑,這個URL被當做搜索屬于org.javassist包下的classes,例如為了加載一個org.javassist.test.main,它的字節(jié)碼文件可以從下面的路徑中獲取:

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

此外,你還可以使用ByteArrayClassPath直接拿一個byte數(shù)組構(gòu)造出一個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()方法構(gòu)造出CtClass對象。

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

2、ClassPool

避免OOM
ClassPool如果加載了很多內(nèi)存較大的對象,可能會導(dǎo)致很大的內(nèi)存消耗,
為了避免這種問題,你可以使用CtClass類的detch()方法,把CtClass對象從ClassPool中移除,例如:

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

如果一個CtClass對象已經(jīng)被detach了,那么就不能調(diào)用它的任何方法了,
然而,你可以調(diào)用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對象對應(yīng)的對象,調(diào)用的時候必須要有合適的權(quán)限,否則的話會拋出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();
        
    }
}

如果在執(zhí)行toClass()方法之前,Hello類被加載過,那么程序?qū)伋鯨inkageError異常,因為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");
        :
}

如果程序運行在服務(wù)器端,即存在多個類加載器的情況,toClass()使用默認的ClassLoader加載類可能會出現(xiàn)ClassCastException,為了避免這種異常,我們應(yīng)該給toClass()方法設(shè)置一個合理的ClassLoader去加載類,例如:

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

不同的類加載器加載的類,及時類的名稱一樣,但其實還是兩個不同的類。如果JVM加載一個類后,會做一次強制轉(zhuǎn)換,有可能便會拋出強制轉(zhuǎn)換的異常。如下面代碼所示:

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

在我們現(xiàn)實開發(fā)中要盡量避免出現(xiàn)多個classLoader加載兩個名字一樣的類的情況。

  • 3.2 使用javassist.Loader

Javassist提供了一個javassist.Loader的類加載器,這個類加載器使用一個ClassPool對象讀取一個字節(jié)碼文件。下面是Javassist.Loader加載一個已經(jīng)修改的特定類例子:

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

如果一個用戶想按照需要修改一個已經(jīng)加載的類,用戶可以給javassist.Loader添加一個監(jiān)聽,當classLoader加載類的時候,事件監(jiān)聽將會進行回調(diào)。這個事件監(jiān)聽必須實現(xiàn)下面的接口:

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

start()方法將會在這個listener添加到j(luò)avassist.Loader對象時執(zhí)行,也就是javassist.Loader的addTranslator()方法被調(diào)用時執(zhí)行;
onLoad()方法會在javassist.Loader加載一個class之前調(diào)用。

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這個類是系統(tǒng)的ClassLoader已經(jīng)加載過的,所以上述代碼會報錯。

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

執(zhí)行結(jié)果:

Hello.say():

Hello

Process finished with exit code 0

4、定制化修改類的結(jié)構(gòu)

Javassist和java反射的api很類似,CtClass提供了getName(),getSuperClass(),getMethods(),同事CtClass也提供了修改類的方法,Javassist也允許添加一個新的字段、構(gòu)造方法和普通方法,構(gòu)造一個方法體也是可能的。
Methods在Javassist中對應(yīng)著CtMethod類,它提供了一些修改類方法的功能,注意如果一個方法是集成基類的,要修改這類的話,要通過代表基類的CtMethod來實現(xiàn)。一個CtMethod對象對應(yīng)著每一個定義的方法。

Javassist不允許移除一個方法或字段,但是它允許改變方法和字段的名稱,如果一個方法不再需要之后,它應(yīng)該通過CtMethod的setName()方法和setModifiers()方法,被重命名和修改成私有的。
Javassist不允許給已經(jīng)存在的方法添加一個額外的參數(shù),如果非要給現(xiàn)有方法添加參數(shù),你可以定義一個新的方法去實現(xiàn)。

Javassist同時也提供了api直接來修改一個類的字節(jié)碼文件,例如通過CtClass的getClassFile()方法返回一個代表不成熟字節(jié)碼文件的ClassFile對象,通過CtMethod的getMethodInfo()方法返回一個代表方法結(jié)構(gòu)的MethodInfo對象,這些api使用到了java虛擬機定義的語法,所以我們必須要對字節(jié)碼知識有所了解。詳情請看javassist.bytecode package.

Javassist修改字節(jié)碼文件要用到Javassist.runtime包,它支持在程序運行的時候,使用一些包含$的特殊標識符,下面我們會介紹這些特殊運算符的用法,需要了解更多的話,可以了解下javassist.runtime包的文檔。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容