Javassist 指南1

1、讀寫字節(jié)碼

Javassist 是一個能處理 Java字節(jié)碼 的類庫,Java字節(jié)碼存儲在class文件中,每一個class文件都包含了一個Java類或一個接口類。

在Javassist中,使用Javassist.CtClass來表示一個class文件,所以說CtClass類就是用來處理class文件的。舉個例子:

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

上面的程序首先獲取了一個ClassPool對象,該對象相當(dāng)于CtClass的一個集合。ClassPool對象在需要時會構(gòu)造一個CtClass對象,并將其記錄下來,當(dāng)下次再次獲取該CtClass對象時就能直接返回。
為了修改類定義,我們需要從ClassPool對象中獲取該類對應(yīng)的CtClass對象。通過ClassPoolget()我們可以獲取到CtClass對象。上面的程序通過ClassPool獲取了一個CtClass對象,該對象代表test. Rectangle這個類。ClassPoolgetDefault()方法返回的ClassPool對象,會從默認(rèn)的系統(tǒng)路徑中尋找CtClass對象。

如果我們?nèi)タ?code>ClassPool的源碼,我們會知道,ClassPool就是hashtable,其中keyclass對應(yīng)的類名,valueclass對應(yīng)的CtClass對象。如果在ClassPool中找不到CtClass對象,則先會new一個CtClass對象,然后將該對象存儲到hashtable中,最后將CtClass對象返回。

CtClass對象是可以被修改的。在上面的程序中,我們將test.Rectangle的父類改成了test.Point。當(dāng) CtClass() 執(zhí)行writeFile()時,這一改動點會寫入到class文件中。writeFile()會將CtClass對象轉(zhuǎn)換成class文件并寫入到本地存儲。Javassist同時還提供了一個直接獲取class對應(yīng)的二進制流的方法,如下所示。

byte[] b = cc.toBytecode();

你還可以將CtClass轉(zhuǎn)換成Class對象。

Class clazz = cc.toClass();

toClass方法會請求當(dāng)前線程的ClassLoader去加載CtClass代表的class類,它返回了一個Class對象表示class類。更多細(xì)節(jié)請查看這一章節(jié)

定義新類

當(dāng)我們需要定義一個新類時,可以使用ClassPoolmakeClass()方法。

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");

上面的程序定義了一個名為Point的新類,該類不含有任何成員變量。Point類的成員方法可以通過CtNewMethod的工廠方法創(chuàng)建,并通過CtClassaddMethod()方法添加到Point類。

makeClass()方法不能創(chuàng)建一個新的接口類,但是使用makeInterface()方法就可以。接口類中的成員方法可以使用CtNewMethodabstractMethod()進行創(chuàng)建,因為接口方法就是一個抽象方法。

凍結(jié)類

如果一個CtClass對象通過writeFile()、toClass()、toBytecode()方法轉(zhuǎn)換成class文件,那么Javassist就會將CtClass對象凍結(jié)起來,防止該CtClass對象被修改。也就是說,凍結(jié)了的CtClass對象是不允許被修改的。因為一個類只能被JVM加載一次。

一個已凍結(jié)的CtClass是可以被解凍的,解凍后的CtClass又被允許修改類了,舉個例子:

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

上面的程序在defrost()方法被調(diào)用后,CtClass對象又可以被修改了。

如果ClassPooldoPruning成員變量被設(shè)置為true,當(dāng)Javassist凍結(jié)CtClass對象時,會將CtClass對象的內(nèi)部數(shù)據(jù)結(jié)構(gòu)進行裁剪。裁剪掉一些無用的屬性是為了減少內(nèi)存消耗。所以,CtClass對象被裁剪后,方法的字節(jié)碼是不允許被訪問的,但是方法名、方法簽名、注解信息是可以被訪問的。已經(jīng)被裁剪過的CtClass對象不能被解凍了。所以,ClassPooldoPruning成員變量默認(rèn)是false。

stopPruning()方法可以禁止CtClass的裁剪,如下所示。

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

上面的程序中調(diào)用了stopPruning()方法禁止了CtClass的裁剪操作,因此在writeFile()方法執(zhí)行后,CtClass對象可以被解凍。

注意:在調(diào)試的時候,你可能想在將class文件寫入到磁盤后,臨時性的禁止CtClass對象的裁剪和凍結(jié)。這個時候你可以調(diào)用debugWriteFile()方法,它會臨時的禁止裁剪,在寫入class文件后會自動解凍CtClass對象,之后該CtClass對象還是可以被裁剪和凍結(jié)的。

類搜索路徑

ClassPool.getDefault()方法默認(rèn)是在JVM的類搜索路徑下返回的ClassPool對象。如果一個程序運行在網(wǎng)頁服務(wù)器上,例如JBossTomcat,那么ClassPool對象可能就找不到用戶所需要的類,因為網(wǎng)頁服務(wù)器使用多個類加載器作為系統(tǒng)類加載器。在這樣的情況下,額外的類路徑必須要注冊到ClassPool中,假設(shè)下面的pool代表一個ClassPool對象:

pool.insertClassPath(new ClassClassPath(this.getClass()));

上面的代碼將this所指向的類對象所在的路徑加入到了ClassPoolclass path中。你可以使用任意的類對象替代上面的this.getClass(),這樣類對象的路徑就會注冊到ClassPool的類加載路徑中來。

當(dāng)然,你也可以將一個文件夾注冊到類加載路徑。例如,下面的程序就將/usr/local/javalib這個文件夾添加到了類加載路徑。

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

上面的程序?qū)?code>http://www.javassist.org:80/java/加入到了類加載路徑。該URL只能被用來查找屬于org.javassit包目錄下的類。舉個例子,如果要加載org.javassist.test.Main這個類,那么它就可以從下面的路徑中獲?。?/p>

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

此外,你可以直接將字節(jié)數(shù)組作為ClassPool的類加載路徑,并構(gòu)造出已CtClass兌現(xiàn)。這個時候,你可以使用ByteArrayClassPath??聪旅娴睦樱?/p>

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

CtClass對象對應(yīng)著byte[] b所表示的類文件。當(dāng)get()方法被調(diào)用時,ClassPool從給定的ByteArrayClassPath讀取類文件,而get()方法中的參數(shù)name必須要ByteArrayClassPath方法的參數(shù)name相匹配。

如果你不知道一個類的全稱路徑,那么你可以使用ClassPoolmakeClass方法來得到一個CtClass對象:

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

makeClass()方法從給定的輸入流中構(gòu)造出一個CtClass對象。你可以使用makeClass()方法將類文件提供給ClassPool對象。如果搜索路徑中包含大體積的jar包,這有可能會提高性能。由于ClassPool會按需讀取類文件,所以它極有可能會為了查找每一個類文件而重復(fù)查找整個jar包。makeClass()方法可以用來優(yōu)化這樣的搜索。通過makeClass()方法構(gòu)造出來的CtClass對象會被存儲到ClassPool中,而其所在的類文件不會被重復(fù)加載。

用戶可以擴展類的搜索路徑。他們可以定義一個新的類,并實現(xiàn)ClassPath接口,然后調(diào)用ClassPoolinsertClassPath()方法。這種方法允許我們將非標(biāo)準(zhǔn)的資源庫加載到類搜索路徑中。

2、ClassPool

ClassPoolCtClass對象的一個容器。一旦CtClass對象被創(chuàng)建出來后,它就永遠(yuǎn)存儲在了ClassPool中。這是因為當(dāng)我們需要編譯一個class的源代碼時,我們需要用到這個class表示的CtClass對象。

舉個例子,假設(shè)我們需要將一個新方法getter()添加到CtClass對象,該CtClass對象代表Point這個類。然后程序需要編譯一段代碼,該代碼中包含Point.getter()方法的調(diào)用,然后將編譯的結(jié)果添加到另外一個名為Line的類的方法中。如果代表PointCtClass丟失了,那么編譯器就無法編譯getter()這個方法了。注意:Point類中原來并沒有getter()這個方法,因此為了能夠正確編譯該方法,ClassPool必須包含程序運行期間的所有CtClass對象。

避免內(nèi)存溢出

如果CtClass對象非常大,ClassPool就會出現(xiàn)內(nèi)存溢出(雖然這種情況很少發(fā)生,因為Javassist會通過 多種方式 去盡量減少內(nèi)存消耗)。為了避免這樣的情況出現(xiàn),我們可以明確地將一些不必要的CtClass對象從ClassPool中去掉。當(dāng)你調(diào)用CtClass對象的detach方法時,CtClass對象就會從ClassPool中去掉??聪旅娴睦樱?/p>

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

在調(diào)用了CtClassdetach方法之后,你不能調(diào)用CtClass的任何方法了。但是,你可以通過調(diào)用ClassPoolget()方法來創(chuàng)建一個新的CtClass對象。當(dāng)你調(diào)用了get()方法后,ClassPool會從類文件中再次讀取內(nèi)容并重新創(chuàng)建一個CtClass對象,該對象會從get()方法中返回。

另外一個避免內(nèi)存溢出的方法是,創(chuàng)建一個新的ClassPool對象來代替舊的ClassPool對象。如果舊的ClassPool對象被回收了,那么它包含的CtClass對象也會被回收。如果我們要創(chuàng)建一個新的ClassPool對象,那么需要執(zhí)行如下代碼:

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

上面的程序創(chuàng)建了一個默認(rèn)的ClassPool對象,這和我們使用ClassPool.getDefault()方法創(chuàng)建的ClassPool對象是一樣的。但是,使用ClassPool.getDefault()工廠方法創(chuàng)建ClassPool對象顯得更加方便。

注意:new ClassPool(true)構(gòu)造了一個ClassPool對象并加入了系統(tǒng)的類搜索路徑,它等同于下面的代碼:

ClassPool cp = new ClassPool();
cp.appendSystemPath();  // or append another path by appendClassPath()

級聯(lián)的ClassPools

如果程序正在Web程序服務(wù)器上運行,那么需要創(chuàng)建多個ClassPool實例;對于每一個ClassLoader對象都需要創(chuàng)建一個ClassPool實例。程序應(yīng)該通過ClassPool的構(gòu)造函數(shù)而不是通過getDefault()方法來創(chuàng)建ClassPool對象。
多個ClassPool對象可以像java.lang.ClassLoader一樣級聯(lián),例如:

ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.insertClassPath("./classes");

如果調(diào)用child.get()方法,那么子ClassPool首先會將委托給父ClassPool。如果父ClassPool中找不到該class文件,那么子ClassPool會嘗試從./classes文件夾下進行查找。

如果child.childFirstLookup屬性設(shè)置成true,那么子ClassPool會在委托給父ClassPool之前從自己的目錄下查找class文件。例如:

ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.appendSystemPath();         // the same class path as the default one.
child.childFirstLookup = true;    // changes the behavior of the child.

復(fù)制一個類來定義一個新類

我們可以從一個已經(jīng)存在的類中定義一個新類。程序如下:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.setName("Pair");

上面的程序首先獲取了Point類對應(yīng)的CtClass對象,然后調(diào)用了CtClasssetName()方法將一個新名字Pair賦值給了CtClass對象。在這個調(diào)用之后,這個CtClass對象所代表的的類的名稱Point被改變成了Pair,類定義的其他部分不變。

注意:CtClasssetName()方法改變了ClassPool對象中的記錄。從ClassPool的實現(xiàn)角度來看,ClassPool對象是CtClass對象的哈希表。setName()方法只是改變了CtClass對象在哈希表的key值,key值從原始類名變成了一個新類名。

因此,如果后續(xù)調(diào)用ClassPool的get("Point")方法,那么該方法肯定不會返回上面的程序的cc對象。這個時候,ClassPool會重新讀取Point.class類,然后構(gòu)建一個新的CtClass對象。這是因為關(guān)聯(lián)Point這個名字的CtClass對象已經(jīng)不存在了??聪旅娴睦樱?/p>

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
CtClass cc1 = pool.get("Point");   // cc1 is identical to cc.
cc.setName("Pair");
CtClass cc2 = pool.get("Pair");    // cc2 is identical to cc.
CtClass cc3 = pool.get("Point");   // cc3 is not identical to cc.

cc1、cc2cc同一個CtClass對象,然而cc3則不是。注意:在cc.setName("Pair")執(zhí)行完成后,cccc1引用的CtClass對象都表示Pair類。

ClassPool對象主要用于維護類和CtClass對象之間的一一對應(yīng)的關(guān)系。Javassit不允許使用兩個不同的CtClass對象來表示同一個類,除非有兩個不同的ClassPool對象。

如果要創(chuàng)建另外一個默認(rèn)的ClassPool對象,那么可以執(zhí)行如下代碼(之前已展示):

ClassPool cp = new ClassPool(true);

如果你有兩個ClassPool對象,這個時候你可以從每一個ClassPool中去獲取代表同一個類的不同CtClass對象。

重命名凍結(jié)類來定義一個新類

一旦CtClass對象通過writeFile()或者toBytecode()轉(zhuǎn)換成一個類文件,那么Javassist會禁止我們對該CtClass對象作進一步的修改。因此,在CtClass代表的Point類被轉(zhuǎn)換成class文件后,你就不能通過執(zhí)行setName()方法來定義一個Point的拷貝類。下面的例子是錯誤的:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
cc.setName("Pair");    // wrong since writeFile() has been called.

為了繞開這個限制,你應(yīng)該調(diào)用ClassPoolgetAndRename()方法,如下所示:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
CtClass cc2 = pool.getAndRename("Point", "Pair");

當(dāng)getAndRename()方法被調(diào)用時,ClassPool首先會讀取Point.class類,然后創(chuàng)建一個新的CtClass對象來表示Point類。然而,它會在哈希表中記錄CtClass之前就將CtClass對象重命名為Pair。因此getAndRename()方法可以在writeFile()或者toBytecode()調(diào)用之后執(zhí)行。

3、Class loader

如果事先就知道哪些類需要修改,最簡便的修改方法如下:

  • 通過ClassPool.get()方法獲取一個CtClass對象。
  • 修改CtClass對象。
  • 調(diào)用CtClass對象的writeFile()或者toBytecode()方法獲得修改后的類文件。
    如果一個類是否需要修改是在運行時決定的,那么用戶必須使用類加載器。使用類加載器的javassist可以在運行時修改字節(jié)碼。用戶可以定義他們自己的類加載器,也可以使用Javassist提供的類加載器。

如果在加載時,用戶能夠確定是否要修改某個類,用戶必須使用Javassist與類加載器協(xié)作。Javassist可以使用類加載器在加載時修改字節(jié)碼。用戶可以自定義類加載器,也可以使用Javassist提供的類加載器。

3.1 CtClass類的toClass 方法

CtClasstoClass方法使用當(dāng)前線程的類加載器來加載該CtClass代表的class類。如果要調(diào)用該方法,調(diào)用者必須具有相應(yīng)的權(quán)限,否則會拋出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()方法。然后構(gòu)造了一個修改后的Hello類的對象,然后調(diào)用該對象的say()方法。

注意:上面的程序的執(zhí)行有一個前提,那就是在toClass()方法被調(diào)用前,Hello這個類從未被加載過,否則JVM會在toClass()方法被調(diào)用前先加載原始的Hello類,這樣當(dāng)我們?nèi)ゼ虞d修改后的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");
        :
}

現(xiàn)在main方法中的第一行就將Hello類加載了,這個時候如果我們再去調(diào)用toClass()方法,就會拋出一個異常,因為類加載器不能同時加載Hello類兩次。

如果上面的程序是運行在JBoss或者Tomcat中,toClass()使用的上下文類加載器就會不適合了。在這種情況下,你有可能會看到ClassCastException異常被拋出。為了避免該異常,你必須給toClass()指定一個合適的類加載器。例如,如果 ‘bean’是你會話的bean對象,那么下面的代碼是可以正常運行的:

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

你可以給toClass()方法傳遞你程序中已使用的類加載器(上面的例子使用了bean對象的類加載器)。

不帶參數(shù)的toClass()方法是比較方便的。如果你需要更多復(fù)雜的功能,你應(yīng)該給toClass()方法傳遞你自定義的類加載器。

3.2 Java的類加載機制

在Java中,多個類加載器是可以共存的,而且每一個類加載器有自己的命名空間。不同的類加載器可以使用同樣的類名加載出兩個不同的類。加載出來的兩個類被認(rèn)為是不同的兩個類。這個特性可以讓我們在一個JVM環(huán)境中運行多個應(yīng)用程序,即使這些應(yīng)用程序包含了相同名稱的不同類。

注意:JVM不允許動態(tài)重復(fù)加載類。一旦一個類加載器加載了一個類,那么它在運行期是不能加載它的修改版本的。因此,你不能在JVM加載了該類后再去改變它的定義。然后,JPDA(Java Platform Debugger Architecture) 卻可以提供有限的能力來重新加載該類。 詳細(xì)信息見3.6節(jié)

如果同一個類文件被兩個不同的類加載器加載,那么JVM會使用相同名字和定義來創(chuàng)建兩個不同類。這兩個類被認(rèn)為是不同的兩個類。既然這兩個類是不同的,那么一個類的對象是不能賦值到另一個類對象的變量的,而這樣的轉(zhuǎn)換會失敗并拋出一個ClassCastException異常。
舉個例子,下面代碼片段會拋出一個異常:

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

Box類被兩個不同的類加載器加載。假設(shè)類加載器CL加載了上面的代碼片段。又因為上面的代碼片段引用了MyClassLoader、ClassObjectBox,這樣CL也加載了這些類(除非它委托給了另外一個類加載器)。因為變量b的類型是Box類型,它被CL加載了。另一方面,myLoader同樣加載了Box類。obj對象就是被myLoader加載出來的。因此,最后一行始終會拋出ClassCastException異常,因為obj變量和b變量代表著不同類型的Box類。

多個類加載器形成了一個樹狀結(jié)構(gòu)。除了bootstrap類加載器外,每一個類加載器都有一個父類加載器。它通常加載子類加載器的類。因為加載一個類會沿著這個層次進行委派,所以即使一個類沒有被請求加載,那么它也有可能會被加載。因此,請求加載類C的類加載器A可能和實際上加載類C的類加載器B不是同一個加載器。為了區(qū)分,我們將前者稱為類C的發(fā)起加載器,將后者稱為類C的實際加載器。

此外,如果CL類加載器被請求來加載類C,但它將加載類C的請求委托給了父類加載器PL,這樣CL加載器就永遠(yuǎn)不會被請求來加載類C中定義的其他類。這樣CL就是不是這些類的發(fā)起加載器。而PL則成為了這些類的發(fā)起加載器,PL被請求來加載它們。

為了理解上面所說的行為,讓我們看下面這個例子:

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加載的,那么Window的發(fā)起加載器和真實的加載器也是L。又因為Window類引用了Box類,那么JVM會請求使用L來加載Box類。我們假設(shè)L將該加載任務(wù)委托給其父類PL,那么Box的發(fā)起類加載器是L,而其真正的加載器是PL。在這種情況下,Point類的發(fā)起類加載器就是PL,因為它的發(fā)起類加載器必須和Box的真正的類加載器相同,因此Point類的請求加載絕對不會委派給L。

接下來,讓我們來看一個稍微修改后的例子:

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

現(xiàn)在,Window的定義中也引用了Point類。在這種情況下,如果需要加載Point類,類加載器L必須將該請求委托給加載器PL。你必須避免讓兩個不同類加載器多次加載同一個類。兩個類加載器中的其中一個必須以另一個作為父加載器。

當(dāng)Point類被加載時,如果L沒有將該請求委托給PL,那么withIs()就會拋出ClassCastException異常。因為Box類的真正加載類是PL,而Point類又是被Box類所引用,所以Point類也是被PL類所加載。這是因為,getSize()返回的對象Point是被PL所加載的,然而widthIs()中的變量p是被L所加載的。JVM就會認(rèn)為這兩個是不同的類型,然后拋出一個異常。

這樣的行為的確不是很方便,但是卻是必須的。如果下面這行代碼

Point p = box.getSize();

不拋出異常的話,那么Window類的程序員會破壞Point對象的封裝。舉個例子,被PL加載的Point類的x字段是私有的,然而如果L直接加載具有以下定義的Point類,那么Window類是可以直接訪問Point中的x值的。

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

3.3 使用javassist的類加載器

Javassist提供了自己的類加載器javassist.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類設(shè)置為test.Rectangle的父類。然后程序加載了test.Rectangle類,并創(chuàng)建了一個test.Rectangle類的對象。

如果用戶需要在類加載時按需修改類,那么用戶可以添加一個事件監(jiān)聽器到javassist.Loader。添加的事件監(jiān)聽器會在類加載器加載類時被喚醒。事件監(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;
}

當(dāng)該事件監(jiān)聽器通過addTranslator()方法被添加到javassist.Loader時,start()方法會被執(zhí)行。當(dāng)javassist.Loader加載類之前,onLoad()方法會被執(zhí)行,我們可以在onLoad()方法中修改將要被加載的類。

舉個例子,下面的事件監(jiān)聽器會在類被加載之前,修改類的修飾符為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()方法中不需要調(diào)用toBytecode()或者writeFile()方法,因為javassist.Loader已經(jīng)在獲取類文件的時候調(diào)用過了這些方法。

要使用MyTranslator對象來運行MyApp應(yīng)用程序,主入口類可以像這樣寫:

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

要運行上面的程序,可以執(zhí)行以下命令:

% java Main2 arg1 arg2...

MyApp類、程序中的其他類都會經(jīng)過MyTranslator轉(zhuǎn)換。

注意:MyApp應(yīng)用程序是不能訪問loader類,如Main2、MyTranslator、ClassPool等,因為他們是被其他類加載器加載的。MyApp應(yīng)用程序中的類是被javassist.Loader所加載,但是Main2卻是被Java默認(rèn)的類加載器加載。

javassist.Loader搜索類的順序和java.lang.ClassLoader是不一樣的。Java的ClassLoader首先會將類加載請求委托給父類加載器,當(dāng)父類加載器找不到對應(yīng)的類時,則它再自己加載。但是,javassist.Loader會首先嘗試自己加載該類,它只有在下面這些情況下才會將類加載請求委托給父類加載器:

  • 在ClassPool中調(diào)用get()方法找不到這個類時;
  • 這些類已經(jīng)通過delegateLoadingOf()執(zhí)行由父類加載器加載。

此搜索順序允許Javassist加載修改過的類。但是,如果找不到修改的類,該類將會被委托給父類加載器。一旦一個類被父類加載器加載了,那么該類引用的其他類也會被父類加載器加載,這樣他們就永遠(yuǎn)不會被修改了。回想一下,C類引用的所有類都被C類實際加載器加載。如果你的程序無法加載修改的類,你應(yīng)該確保是不是所有使用該類的類都已經(jīng)由javassist加載過了。

3.4 編寫一個類加載器

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

假設(shè)MyApp類是一個應(yīng)用程序,如果要執(zhí)行該應(yīng)用程序,第一步需要將該類放置在 ./class 目錄下,它不能被包含在類搜索路徑中,否則MyApp.class將會被系統(tǒng)默認(rèn)的類加載加載(它也是SampleLoader的父類加載器)。./class 目錄通過insertClassPath()方法被加載到了類搜索路徑中。你也可以選擇用別的名字代替./class,然后執(zhí)行下面命令:

% java SampleLoader

類加載器會加載MyApp類(./class/MyApp.class),然后調(diào)用MyApp.main()方法,并將命令行中的參數(shù)傳遞進去。

這是最簡單的使用Javassist的方法,然而,如果你想寫一個更加復(fù)雜點的類加載器,你可能需要Java類加載機制的一些細(xì)節(jié)知識。例如,上面的應(yīng)用程序會將MyApp類放置在一個命名空間中,而SampleLoader類則在另一個命名空間中,因為他們是由不同的類加載器加載的。因此,MyApp類是不能直接訪問SampleLoader類的。

3.5 修改系統(tǒng)類

系統(tǒng)類例如java.lang.String是不能被除系統(tǒng)加載器以外的其他加載器加載的。因此,上面程序中的SampleLoader或者javassist.Loader是不能在運行時修改系統(tǒng)類的。

如果你的應(yīng)用程序想修改系統(tǒng)類,系統(tǒng)類必須被靜態(tài)的修改。例如,下面的程序就將一個新的字段hiddenValuejava.lang.String類中:

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(".");

上面的程序會產(chǎn)生一個新文件 "./java/lang/String.class"。

如果你想要使用修改后的String類運行MyApp程序,則執(zhí)行如下命令:

% java -Xbootclasspath/p:. MyApp arg1 arg2...

假設(shè)MyApp的定義如下:

public class MyApp {
    public static void main(String[] args) throws Exception {
        System.out.println(String.class.getField("hiddenValue").getName());
    }
}

那么我們可以看到修改后的String類被成功加載了,MyApp類打印出了hiddenValue的值。

注意:如果使用上面的技術(shù)來覆蓋rt.jar中的系統(tǒng)類,那么部署該應(yīng)用會違反Java 2 運行時二進制代碼許可協(xié)議。

3.6 在運行時重新加載類

如果JVM在JPDA(Java Platform Debugger Architecture)啟用的時候啟動,那么類可以被動態(tài)的重新加載。在JVM加載了一個類后,舊版本的類可以被卸載,新版本的類就可以被重新加載了。這樣,類定義就可以在運行時被動態(tài)的修改。然而,新的類定義必須和舊的類定義有所兼容。JVM是不允許兩個版本的類的模式修改的。他們必須有相同的方法和字段。

Javassist提供了一個類,這使得在運行時重新加載類變得更加方便。如果想要獲取更多關(guān)于運行時重新加載類的信息,可以看API文檔中關(guān)于javassist.tools.HotSwapper的說明。

4、參考文檔

1、英文原文

2、譯文參考——Javassist 使用指南(一)

?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、讀和寫字節(jié)碼 Javassist是一個處理Java字節(jié)碼的庫,java字節(jié)碼是使用二進制格式存儲在文件中的話,...
    礪雪凝霜閱讀 1,388評論 0 2
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,663評論 1 32
  • 本文翻譯自 Javassist Tutorial-2 4. 自省和自定制 (Introspection and c...
    二胡閱讀 33,072評論 4 33
  • Javassist是一個用于處理Java字節(jié)碼的類庫。Java字節(jié)碼是一個以二進制文件進行存儲的class文件。每...
    bdqfork閱讀 1,263評論 0 0
  • 在Javassist之Classloader(一)中我們講述了Javassist的toClass()以及Java的...
    bdqfork閱讀 1,625評論 0 2

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