本文譯自Getting Started with Javassist,若是謬誤之處,還請指出。html
bytecode讀寫java
ClassPoolexpress
Class loader數組
自有和定製服務器
Bytecode操控接口數據結構
Genericsapp
Varargs框架
J2MEless
裝箱和拆箱ide
調試
1. bytecode讀寫
Javassist是用來處理java字節碼的類庫, java字節碼通常存放在後綴名稱爲class的二進制文件中。每一個二進制文件都包含一個java類或者是java接口。
Javasist.CtClass是對類文件的抽象,處於編譯中的此對象能夠用來處理類文件。下面的代碼用來展現一下其簡單用法:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("test.Rectangle");
3: cc.setSuperclass(pool.get("test.Point"));
4: cc.writeFile();
這段程序首先獲取ClassPool的實例,它主要用來修改字節碼的,裏面存儲着基於二進制文件構建的CtClass對象,它可以按需建立出CtClass對象並提供給後續處理流程使用。當須要進行類修改操做的時候,用戶須要經過ClassPool實例的.get()方法,獲取CtClass對象。從上面代碼中咱們能夠看出,ClassPool的getDefault()方法將會查找系統默認的路徑來搜索test.Rectable對象,而後將獲取到的CtClass對象賦值給cc變量。
從易於擴展使用的角度來講,ClassPool是由裝載了不少CtClass對象的HashTable組成。其中,類名爲key,CtClass對象爲Value,這樣就能夠經過搜索HashTable的Key來找到相關的CtClass對象了。若是對象沒有被找到,那麼get()方法就會建立出一個默認的CtClass對象,而後放入到HashTable中,同時將當前建立的對象返回。
從ClassPool中獲取的CtClass對象,是能夠被修改的。從上面的 代碼中,咱們能夠看到,原先的父類,由test.Rectangle被改爲了test.Point。這種更改能夠經過調用CtClass().writeFile()將其持久化到文件中。同時,Javassist還提供了toBytecode()方法來直接獲取修改的字節碼:
1: byte[] b = cc.toBytecode();
你能夠經過以下代碼直接加載CtClass:
1: Class clazz = cc.toClass();
toClass()方法被調用,將會使得當前線程中的context class loader加載此CtClass類,而後生成 java.lang.Class對象。更多的細節 ,請參見this section below.
新建類
新建一個類,可使用ClassPool.makeClass()方法來實現:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.makeClass("Point");
上面的代碼展現的是建立無成員方法的Point類,若是須要附帶方法的話,咱們能夠用CtNewMethod附帶的工廠方法建立,而後利用CtClass.addMethod()將其追加就能夠了 。
makeClass()不能用於建立新的接口。可是makeInterface()能夠。接口的方法能夠用CtNewmethod.abstractMethod()方法來建立,須要注意的是,在這裏,一個接口方法實際上是一個abstract方法。
凍結類
若是CtClass對象被writeFile(),toClass()或者toBytecode()轉換成了類對象,Javassist將會凍結此CtClass對象。任何對此對象的後續更改都是不容許的。之因此這樣作,主要是由於此類已經被JVM加載,因爲JVM自己不支持類的重複加載操做,因此不容許更改。
一個凍結的CtClass對象,能夠經過以下的代碼進行解凍,若是想更改類的話,代碼以下:
1: CtClasss cc = ...;
2: :
3: cc.writeFile();
4: cc.defrost();
5: cc.setSuperclass(...); // OK since the class is not frozen.
調用了defrost()方法以後,CtClass對象就能夠隨意修改了。
若是ClassPool.doPruning被設置爲true,那麼Javassist將會把已凍結的CtClass對象中的數據結構進行精簡,此舉主要是爲了防止過多的內存消耗。而精簡掉的部分,都是一些沒必要要的屬性(attriute_info結構)。所以,當一個CtClass對象被精簡以後,方法是沒法被訪問和調用的,可是方法名稱,簽名,註解能夠被訪問。被精簡過的CtClass對象能夠被再次解凍。須要注意的是,ClassPool.doPruning的默認值爲false。
爲了防止CtClass類被無故的精簡,須要優先調用stopPruning()方法來進行阻止:
1: CtClasss cc = ...;
2: cc.stopPruning(true);
3: :
4: cc.writeFile(); //轉換爲類文件,cc不會被精簡.
這樣,CtClass對象就不會被精簡了。當writeFile()方法調用以後,咱們就能夠進行解凍,而後隨心所欲了。
須要注意的是:在調試的時候, debugWriteFile()方法能夠很方便的防止CtClass對象精簡和凍住。
類搜索路徑
ClassPool.getDefault()方法的搜索路徑和JVM的搜索路徑是一致的。若是程序運行在JBoss或者Tomcat服務器上,那麼ClassPool對象也許不可以找到用戶類,緣由是應用服務器用的是多個class loader,其中包括系統的class loader來加載對象。正因如此,ClassPool須要 附加特定的類路徑才行。 假設以下的pool實例表明ClassPool對象:
1: pool.insertClassPath(new ClassClassPath(this.getClass()));
上面的代碼段註冊了this所指向的類路徑下面的類對象。你能夠用其餘的類對象來代替this.getClass()。這樣就能夠加載其餘不一樣的類對象了。
你也能夠註冊一個目錄名字來做爲類搜索路徑。好比下面代碼中,使用/usr/local/javalib目錄做爲搜索路徑:
1: ClassPool pool = ClassPool.getDefault();
2: pool.insertClassPath("/usr/local/javalib");
也可使用url來做爲搜索路徑:
1: ClassPool pool = ClassPool.getDefault();
2: ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist.");
3: pool.insertClassPath(cp);
上面這段代碼將會添加「http://www.javassist.org:80/java/」到類搜索路徑。這個URL主要用來搜索org.javassist包下面的類。好比加載org.javassist.test.Main類,此類將會從以下路徑獲取:
1: http://www.javassist.org:80/java/org/javassist/test/Main.class
此外,你甚至能夠直接使用一串字節碼,而後建立出CtClass對象。示例以下:
1: ClassPool cp = ClassPool.getDefault();
2: byte[] b = a byte array;
3: String name = class name;
4: cp.insertClassPath(new ByteArrayClassPath(name, b));
5: CtClass cc = cp.get(name);
從上面代碼能夠看出,ClassPool加載了ByteArrayClasPath構建的對象,而後利用get()方法並經過類名,將對象賦值給了CtClass對象。
若是你不知道類的全名,你也能夠用makeClass()來實現:
1: ClassPool cp = ClassPool.getDefault();
2: InputStream ins = an input stream for reading a class file;
3: CtClass cc = cp.makeClass(ins);
makeClass()方法利用給定的輸入流構建出CtClass對象。你能夠用餓漢方式直接建立出ClassPool對象,這樣當搜索路徑中有大點的jar文件須要加載的時候,能夠提高一些性能,之因此 這樣作,緣由是ClassPool對象按需加載類文件,因此它可能會重複搜索整個jar包中的每一個類文件,正由於如此,makeClass()能夠用於優化查找的性能。被makeClass()方法加載過的CtClass對象將會留存於ClassPool對象中,不會再進行讀取。
用戶能夠擴展類搜索路徑。能夠經過定義一個新的類,擴展自ClassPath接口,而後返回一個insertClassPath便可。這種作法能夠容許其餘資源被包含到搜索路徑中。
2. ClassPool
一個ClassPool裏面包含了諸多的CtClass對象。每當一個CtClass對象被建立的時候,都會在ClassPool中作記錄。之因此這樣作,是由於編譯器後續的源碼編譯操做可能會經過此類關聯的CtClass來獲取。
好比,一個表明了Point類的CtClass對象,新加一個getter()方法。以後,程序將會嘗試編譯包含了getter()方法的Point類,而後將編譯好的getter()方法體,添加到另一個Line類上面。若是CtClass對象表明的Point類不存在的話,那麼編譯器就不會成功的編譯getter()方法。須要注意的是原來的類定義中並不包含getter()方法 。所以,要想正確的編譯此方法,ClassPool對象必須包含程序運行時候的全部的CtClass對象。
避免內存溢出
CtClass對象很是多的時候,ClassPool將會消耗內存巨大。爲了不個問題,你能夠移除掉一些不須要的CtClass對象。你能夠經過調用CtClass.detach()方法來實現,那樣的話此CtClass對象將會從ClassPool移除。代碼以下:
1: CtClass cc = ... ;
2: cc.writeFile();
3: cc.detach();
此CtClass對象被移除後,不能再調用其任何方法。可是你能夠調用ClassPool.get()方法來建立一個新的CtClass實例。
另外一個方法就是用新的ClassPool對象來替代舊的ClassPool對象。若是舊的ClassPool對象被垃圾回收了,那麼其內部的CtClass對象也都會被垃圾回收掉。下面的代碼能夠用來建立一個新的ClassPool對象:
1: ClassPool cp = new ClassPool(true);
2: //若是須要的話,利用appendClassPath()來添加額外的搜索路徑
上面的代碼和ClassPool.getDefault()來建立ClassPool,效果是同樣的。須要注意的是,ClasssPool.getDefault()是一個單例工廠方法,它可以建立出一個惟一的ClassPool對象並進行重複利用。new ClassPool(true)是一個很快捷的構造方法,它可以建立一個ClassPool對象而後追加系統搜索路徑到其中。和以下的代碼建立行爲表現一致:
1: ClassPool cp = new ClassPool();
2: cp.appendSystemPath(); // or append another path by appendClassPath()
級聯ClassPools
若是應用運行在JBOSS/Tomcat上, 那麼建立多個ClassPool對象將會頗有必要。由於每一個類加載其都將會持有一個ClassPool的實例。應用此時最好不用getDefault()方法來建立ClassPool對象,而是使用構造來建立。
多個ClassPool對象像java.lang.ClassLoader同樣作級聯,代碼以下:
1: ClassPool parent = ClassPool.getDefault();
2: ClassPool child = new ClassPool(parent);
3: child.insertClassPath("./classes");
若是child.get()被調用,子ClassPool將會首先從父ClassPool進行查找。當父ClassPool查找不到後,而後將會嘗試從./classes目錄進行查找。
若是child.childFirstLookup = true, 子ClassPool將會首先查找本身的目錄,而後查找父ClassPool,代碼以下:
1: ClassPool parent = ClassPool.getDefault();
2: ClassPool child = new ClassPool(parent);
3: child.appendSystemPath(); //和默認的搜索地址一致.
4: child.childFirstLookup = true; //修改子類搜索行爲.
爲新類重命名
能夠從已有類建立出新的類,代碼以下:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("Point");
3: cc.setName("Pair");
此代碼首先從Point類建立了CtClass對象,而後調用setName()重命名爲Pair。以後,全部對CtClass對象的引用,將會由Point變成Pair。
須要注意的是setName()方法改變ClassPool對象中的標記。從可擴展性來看,ClassPool對象是HashTable的合集,setName()方法只是改變了key和Ctclass對象的關聯。
所以,對於get("Point")方法以後的全部調用,將不會返回CtClasss對象。ClassPool對象再次讀取Point.class的時候,將會建立一個新的CtClass,這是由於和Point關聯的CtClass對象已經不存在了,請看以下代碼:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("Point");
3: CtClass cc1 = pool.get("Point"); //cc1和cc是一致的.
4: cc.setName("Pair");
5: CtClass cc2 = pool.get("Pair"); //cc2和cc是一致的.
6: CtClass cc3 = pool.get("Point"); //cc3和cc是不一致的.
cc1和cc2將會指向cc,可是cc3卻不會。須要注意的是,在cc.setName("Pair")執行後,cc和cc1指向的CtClass對象都變成了指向Pair類。
ClassPool對象用來維護類之間和CtClass對象之間一對一的映射關係。Javassist不容許兩個不一樣的CtClass對象指向同一個類,除非兩個獨立的ClassPool存在的狀況下。這是爲實現程序轉換而保證其一致性的最鮮明的特色。
咱們知道,能夠利用ClassPool.getDefault()方法建立ClassPool的實例,代碼片斷以下(以前已經展現過):
1: ClassPool cp = new ClassPool(true);
若是你有兩個ClassPool對象,那麼你能夠從這兩個對象中分別取出具備相同類文件,可是隸屬於不一樣的CtClass對象生成的,此時能夠經過修改這倆CtClass對象來生成不一樣的類。
從凍結類中建立新類
當CtClass對象經過writeFile()方法或者toBytecode()轉變成類文件的時候,Javassist將不容許對這個CtClass對象有任何修改。所以,當表明Point類的CtClass對象被轉換成了類文件,你不可以先拷貝Point類,而後修更名稱爲Pair類,由於Point類中的setName()方法是沒法被執行的,錯誤使用示例以下:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("Point");
3: cc.writeFile();
4: cc.setName("Pair"); // wrong since writeFile() has been called.
爲了可以避免這種限制,你應該使用getAndRename()方法,正確示例以下:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("Point");
3: cc.writeFile();
4: CtClass cc2 = pool.getAndRename("Point", "Pair");
若是getAndRename()方法被調用,那麼ClassPool首先會基於Point.class來建立一個新的CtClass對象。以後,在CtClass對象被放到HashTable前,它將CtClass對象名稱從Point修改成Pair。所以,getAndRename()方法能夠在writeFile()方法或者toBytecode()方法執行後去修改CtClass對象。
3. 類加載器
若是預先知道須要修改什麼類,最簡單的修改方式以下:
1. 調用ClassPool.get()方法獲取CtClass對象
2. 修改此對象
3. 調用CtClass對象的writeFile()方法或者toBytecode()方法來生成類文件。
若是檢測類是否修改行爲發生在程序加載的時候,那麼對於用戶說來,Javassist最好提供這種與之匹配的類加載檢測行爲。事實上,javassist能夠作到在類加載的時候來修改二進制數據。使用Javassist的用戶能夠定義本身的類加載器,固然也能夠採用Javassist自身提供的。
3.1 CtClass中的toClass方法
CtClass提供的toClass()方法,能夠很方便的加載當前線程中經過CtClass對象建立的類。可是爲了使用此方法,調用方必須擁有足夠的權限才行,不然將會報SecurityException錯誤。
下面的代碼段展現瞭如何使用toClass()方法:
1: public class Hello {
2: public void say() {
3: System.out.println("Hello");
4: }
5: }
6:
7: public class Test {
8: public static void main(String[] args) throws Exception {
9: ClassPool cp = ClassPool.getDefault();
10: CtClass cc = cp.get("Hello");
11: CtMethod m = cc.getDeclaredMethod("say");
12: m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
13: Class c = cc.toClass();
14: Hello h = (Hello)c.newInstance();
15: h.say();
16: }
17: }
Test.main()方法中, say()方法被插入了println()方法,以後這個被修改的Hello類實例被建立,say()方法被調用。
須要注意的是,上面代碼中,Hello類是放在toClass()以後被調用的,若是不這麼作的話,JVM將會先加載Hello類,而不是在toClass()方法加載Hello類以後再調用Hello類,這樣作會致使加載失敗(會拋出LinkageError錯誤)。好比,若是Test.main()方法中的代碼以下:
1: public static void main(String[] args) throws Exception {
2: Hello orig = new Hello();
3: ClassPool cp = ClassPool.getDefault();
4: CtClass cc = cp.get("Hello"); CtMethod m = cc.getDeclaredMethod("say");
5: m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
6: Class c = cc.toClass();
7: Hello h = (Hello)c.newInstance();
8: h.say();}
main方法中,第一行的Hello類會被加載,以後調用toClass()將會報錯,由於一個類加載器沒法在同一時刻加載兩個不一樣的Hello類版本。
若是程序跑在JBoss/Tomcat上,利用toClass()方法可能會有些問題。在這種狀況下,你將會遇到ClassCastException錯誤,爲了不這種錯誤,你必須爲toClass()方法提供很是明確的類加載器。好比,在以下代碼中,bean表明你的業務bean對象的時候:
1: CtClass cc = ...;
2: Class c = cc.toClass(bean.getClass().getClassLoader());
則就不會出現上述問題。你應當爲toClass()方法提供已經加載過程序的類加載器才行。
toClass()的使用會帶來諸多方便,可是若是你須要更多更復雜的功能,你應當實現本身的類加載器。
3.2 java中的類加載
在java中,多個類加載器能夠共存,不一樣的類加載器會建立本身的應用區域。不一樣的類加載器能夠加載具備相同類名稱可是內容不盡相同的類文件。這種特性可讓咱們在一個JVM上並行運行多個應用。
須要注意的是JVM不支持動態的從新加載一個已加載的類。一旦類加載器加載了一個類,那麼這個類或者基於其修改的類,在JVM運行時,都不能再被加載。所以,你不可以修改已經被JVM加載的類。可是,JPDA(Java Platform Debugger Architecture)支持這種作法。具體請見 Section 3.6.
若是一個類被兩個不一樣的類加載器加載,那麼JVM會將此類分紅兩個不一樣的類,可是這兩個類具備相同的類名和定義。咱們通常把這兩個類當作是不一樣的類,因此一個類不可以被轉換成另外一個類,一旦這麼作,那麼這種強轉操做將會拋出錯誤ClassCastException。
好比,下面的例子會拋錯:
1: MyClassLoader myLoader = new MyClassLoader();
2: Class clazz = myLoader.loadClass("Box");
3: Object obj = clazz.newInstance();
4: Box b = (Box)obj; //會拋出ClassCastException錯誤.
Box類被兩個類加載器所加載,試想一下,假設CL類加載器加載的類包含此代碼段,因爲此代碼段指向MyClassLoader,Class,Object,Box,因此CL加載器也會將這些東西加載進來(除非它是其它類加載器的代理)。所以變量b就是CL中的Box類。從另外一方面說來,myLoader也加載了Box類,obj對象是Box類的實例,所以,代碼的最後一行將一直拋出ClassCastException錯誤,由於obj和b是Box類的不一樣實例副本。
多個類加載器會造成樹狀結構,除了底層引導的類加載器外,每個類加載器都有可以正常的加載子加載器的父加載器。因爲加載類的請求能夠被類加載器所代理,因此一個類可能會被你所不但願看到的類加載器所加載。所以,類C可能會被你所不但願看到的類加載器所加載,也可能會被你所但願的加載器所加載。爲了區分這種現象,咱們稱前一種加載器爲類C的虛擬引導器,後一種加載器爲類C的真實加載器。
此外,若是類加載器CL(此類加載器爲類C的虛擬引導器)讓其父加載器PL來加載類C,那麼至關於CL沒有加載任何類C相關的東西。此時,CL就不能稱做虛擬引導器。相反,其父類加載器PL將會變成虛擬引導器。全部指向類C定義的類,都會被類C的真實加載器所加載。
爲了理解這種行爲,讓咱們看看以下的例子:
1: public class Point { // 被PL加載
2: private int x, y;
3: public int getX() { return x; }
4: :
5: }
6:
7: public class Box { // 初始化器爲L可是實際加載器爲PL
8: private Point upperLeft, size;
9: public int getBaseX() { return upperLeft.x; }
10: :
11: }
12:
13: public class Window { // 被L加載器所加載
14: private Box box;
15: public int getBaseX() { return box.getBaseX(); }
16: }
假如Window類被L加載器所加載,那麼Window的虛擬加載器和實際加載器都是L。因爲Window類中引用了Box類,JVM將會加載Box類,這裏,假設L將此加載任務代理給了其父加載器PL,那麼Box的類加載器將會變成L,可是其實際加載器將會是PL。所以,在此種狀況下,Point類的虛擬加載器將不是L,而是PL,由於它和Box的實際加載器是同樣的。所以L加載器將永遠不會加載Point類。
接下來,讓咱們看一個少許更改過的例子:
1: public class Point {
2: private int x, y;
3: public int getX() { return x; }
4: :
5: }
6:
7: public class Box { // the initiator is L but the real loader is PL
8: private Point upperLeft, size;
9: public Point getSize() { return size; }
10: :
11: }
12:
13: public class Window { // loaded by a class loader L
14: private Box box;
15: public boolean widthIs(int w) {
16: Point p = box.getSize();
17: return w == p.getX();
18: }
19: }
如今看來,Window類指向了Point,所以類加載器L要想加載Point的話,它必須代理PL。必須杜絕的狀況是,兩個類加載器加載同一個類的狀況。其中一個類加載器必須可以代理另外一個才行。
當Point類加載後,L沒有代理PL,那麼widthIs()將會拋出ClassCastExceptioin。因爲Box類的實際加載器是PL,因此指向Box類的Point類將也會被PL所加載。所以,getSize()方法的最終結果將是被PL加載的Point對象的實例。反之,widthIs()方法中的p變量的類型將是被L所加載的Point類。對於這種狀況,JVM會將其視爲不一樣的類型,從而由於類型不匹配而拋出錯誤。
這種狀況,雖然不方便,可是卻頗有必要,來看一下以下代碼段:
1: Point p = box.getSize();
沒有拋出錯誤,Window將會破壞Point對象的包裝。舉個例子吧,被PL加載的Point類中,x字段是私有的。可是,若是L利用以下的定義加載了Point類的話,那麼Window類是能夠直接訪問x字段的:
1: public class Point {
2: public int x, y; // not private
3: public int getX() { return x; }
4: :
5: }
想要了解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提供了javassist.Loader這個類加載器。它使用javassist.ClassPool對象來讀取類文件。
舉個例子,使用javassist.Loader來加載Javassist修改過的類:
1: import javassist.*;
2: import test.Rectangle;
3:
4: public class Main {
5: public static void main(String[] args) throws Throwable {
6: ClassPool pool = ClassPool.getDefault();
7: Loader cl = new Loader(pool);
8:
9: CtClass ct = pool.get("test.Rectangle");
10: ct.setSuperclass(pool.get("test.Point"));
11:
12: Class c = cl.loadClass("test.Rectangle");
13: Object rect = c.newInstance();
14: :
15: }
16: }
上面的程序就修改了test.Rectangle類,先是test.Point類被設置成了test.Rectangle類的父類,以後程序會加載這個修改的類並建立test.Rectangle類的實例出來。
若是一個類被加載後,用戶想要修改爲本身想要的東西進來,那麼用戶能夠經過添加事件監聽器到javassist.Loader上。每當類加載器加載了類進來,那麼事件監聽器將會發出通知。此監聽器必須實現以下的接口:
1: public interface Translator {
2: public void start(ClassPool pool)
3: throws NotFoundException, CannotCompileException;
4: public void onLoad(ClassPool pool, String classname)
5: throws NotFoundException, CannotCompileException;
6: }
當利用javassist.Loader.addTranslator()將事件監聽器添加到javassist.Loader對象上的時候,上面的start()方法將會被觸發。而onLoad()方法的觸發先於javassist.Loader加載一個類,所以onLoad()方法能夠改變已加載的類的定義。
舉個例子,下面的事件監聽器將會在類被加載器加載以前,修改其類型爲public:
1: public class MyTranslator implements Translator {
2: void start(ClassPool pool)
3: throws NotFoundException, CannotCompileException {}
4: void onLoad(ClassPool pool, String classname)
5: throws NotFoundException, CannotCompileException
6: {
7: CtClass cc = pool.get(classname);
8: cc.setModifiers(Modifier.PUBLIC);
9: }
10: }
須要注意的是,onLoad()方法不須要調用toBytecode方法或者writeFile方法,由於javassistLoader會調用這些方法來獲取類文件。
爲了可以運行MyApp類中的MyTranslator對象,寫了一個主方法以下:
1: import javassist.*;
2:
3: public class Main2 {
4: public static void main(String[] args) throws Throwable {
5: Translator t = new MyTranslator();
6: ClassPool pool = ClassPool.getDefault();
7: Loader cl = new Loader();
8: cl.addTranslator(pool, t);
9: cl.run("MyApp", args);
10: }
11: }
想要運行它,能夠按照以下命令來:
1: % java Main2 arg1 arg2...
MyApp類和其餘的一些類,會被MyTranslator所翻譯。
須要注意的是,相似MyApp這種應用類,是不可以訪問Main2,MyTranslator,ClassPool這些類的,由於這些類是被不一樣加載器所加載的。應用類是被javassist.Loader所加載,而Main2這些是被java的默認類加載器所加載的。
javassist.Loader搜尋須要加載的類的時候,和java.lang.ClassLoader.ClassLoader是大相徑庭的。後者先使用父類加載器進行加載,若是父類加載器找不到類,則嘗試用當前加載器進行加載。而javassist.Load在以下狀況下,則嘗試直接加載:
ClassPool對象上,沒法找到get方法
或者
父類使用delegateLoadingOf()方法進行加載
Javassist能夠按照搜索的順序來加載已修改的類,可是,若是它沒法找到已修改的類,那麼將會由父類加載器進行加載操做。一旦當一個類被父加載器所加載,那麼指向此類的其餘類,也將被此父加載器所加載,由於,這些被加載類是不會被修改的。若是你的程序沒法加載一個已修改的類,你須要確認全部的類是不是被javassist.Loader所加載。
3.4 打造一個類加載器
用javassist打造一個簡單的類加載器,代碼以下:
1: import javassist.*;
2:
3: public class SampleLoader extends ClassLoader {
4: /* Call MyApp.main().
5: */
6: public static void main(String[] args) throws Throwable {
7: SampleLoader s = new SampleLoader();
8: Class c = s.loadClass("MyApp");
9: c.getDeclaredMethod("main", new Class[] { String[].class })
10: .invoke(null, new Object[] { args });
11: }
12:
13: private ClassPool pool;
14:
15: public SampleLoader() throws NotFoundException {
16: pool = new ClassPool();
17: pool.insertClassPath("./class"); // MyApp.class must be there.
18: }
19:
20: /* Finds a specified class.
21: * The bytecode for that class can be modified.
22: */
23: protected Class findClass(String name) throws ClassNotFoundException {
24: try {
25: CtClass cc = pool.get(name);
26: // modify the CtClass object here
27: byte[] b = cc.toBytecode();
28: return defineClass(name, b, 0, b.length);
29: } catch (NotFoundException e) {
30: throw new ClassNotFoundException();
31: } catch (IOException e) {
32: throw new ClassNotFoundException();
33: } catch (CannotCompileException e) {
34: throw new ClassNotFoundException();
35: }
36: }
37: }
MyApp類是一個應用程序。爲了執行這個應用,咱們首先須要將類文件放到./class文件夾下,須要確保當前文件夾不在類搜索目錄下,不然將會被SampleLoader的父類加載器,也就是系統默認的類加載器所加載。./class目錄名稱在insertClassPath方法中必需要有所體現,固然此目錄名稱是能夠隨意改變的。接下來咱們運行以下命令:
1: % java SampleLoader
此時,類加載器將會加載MyApp類(./class/MyApp.class)並調用MyApp.main方法。
這是使用基於Javassist類加載器最簡單的方式。然而,若是你想寫一個更加複雜的類加載器,你須要對Java的類加載器機制有足夠的瞭解。好比,上面的代碼中,MyApp類的命名空間和SampleLoader類的命名空間是不一樣的,是由於這兩個類是被不一樣的類加載器鎖加載的。所以,MyApp類沒法直接訪問SampleLoader類。
3.5 修改系統類
系統類,好比java.lang.String,會優先被系統的類加載器所加載。所以,上面展現的SampleLoader或者javassist.Loader在進行類加載的時候,是沒法修改系統類的。
若是須要進行修改的話,系統類必須被靜態的修改。好比,下面的代碼將會給java.lang.String添加一個hiddenValue的字段:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("java.lang.String");
3: CtField f = new CtField(CtClass.intType, "hiddenValue", cc);
4: f.setModifiers(Modifier.PUBLIC);
5: cc.addField(f);
6: cc.writeFile(".");
此段代碼會產生"./java/lang/String.class"文件。
爲了可以讓更改的String類在MyApp中運行,能夠按照以下的方式來進行:
1: % java -Xbootclasspath/p:. MyApp arg1 arg2...
假設MyApp的代碼以下:
1: public class MyApp {
2: public static void main(String[] args) throws Exception {
3: System.out.println(String.class.getField("hiddenValue").getName());
4: }
5: }
此更改的String類成功的被加載,而後打印出了hiddenValue。
須要注意的是:用如上的方式來修改rt.jar中的系統類並進行部署,會違反Java 2 Runtime Environment binary code license.
3.6 運行狀態下從新加載類
若是JVM中的JPDA(Java Platform Debugger Architecture)是可用狀態,那麼一個類是能夠被動態加載的。JVM加載類後,此類的以前版本將會被卸載,而新版本將會被加載。因此,從這裏看出,在運行時狀態,類是能夠被動態更改的。然而,新的類必須可以和舊的類兼容,是由於JVM不容許直接更改類的總體框架,他們必須有相同的方法和字段。
Javassist提供了簡單易用的方式來從新加載運行時的類。想要獲取更多內容,請翻閱javassist.tools.HotSwapper的API文檔。
4. 定製化
CtClass提供了不少方法來用進行定製化。Javassist能夠和Java的反射API進行聯合定製。CtClass提供了getName方法,getSuperclass方法,getMethods方法等等。CtClass同時也提供了方法來修改類定義,容許添加新的字段,構造,方法等。即使對於檢測方法體這種事情來講,也是可行的。
方法都是被CtMethod對象所表明,它提供了多個方法用於改變方法的定義,須要注意的是,若是方法繼承自父類,那麼在父類中的一樣方法將也會被CtMethod所表明。CtMethod對象能夠正確的表明任何方法聲明。
好比,Point類有一個move方法,其子類ColorPoint不會重寫move方法, 那麼在這裏,兩個move方法,將會被CtMethod對象正確的識別。若是CtMethod對象的方法定義被修改,那麼此修改將會反映到兩個方法上。若是你想只修改ColorPoint類中的move方法,你須要首先建立ColorPoint的副本,那麼其CtMethod對象將也會被複制,CtMethod對象可使用CtNewMethod.copy方法來實現。
Javassist不支持移除方法或者字段,可是支持修更名字。因此若是一個方法再也不須要的話,能夠在CtMethod中對其進行重命名並利用setName方法和setModifiers方法將其設置爲私有方法。
Javassist不支持爲已有的方法添加額外的參數。可是能夠經過爲一個新的方法建立額外的參數。好比,若是你想添加一個額外的int參數newZ到Point類的方法中:
1: void move(int newX, int newY) { x = newX; y = newY; }
你應當在Point類中添加以下方法
1: void move(int newX, int newY, int newZ) {
2: // do what you want with newZ.
3: move(newX, newY);
4: }
Javassist同時也提供底層的API來直接修改原生的類文件。好比,CtClass類中的getClassFile方法能夠返回一個ClassFile對象來表明一個原生的類文件。而CtMethod中的getMethodInfo方法則返回MethodInfo對象來表明一個類中的method_info結構。底層的API單詞大多數來自於JVM,因此用於用起來不會感受到陌生。更多的內容,能夠參看 javassist.bytecode
package.
Javassist修改類文件的時候,通常不須要javassist.runtime包,除非一些特別的以$符號開頭的。這些特殊符號會在後面進行講解。更多的內容,能夠參考javassist.runtime包中的API文檔。
4.1 方法體前/後穿插代碼段
CtMethod和CtConstructor提供了insertBefore,insertAfter,addCatch三個方法,它們用於在已存在的方法中插入代碼段。使用者能夠插入java代碼段是由於Javassist內置了一個簡易的java編譯器來處理這些源碼。此編譯器會將java源碼編譯成字節碼,而後插入到方法體中。
同時,在指定行號的位置插入代碼段也是容許的(只有當行號在當前類中存在)。CtMethod和CtConstructor中的insertAt方法帶有源碼輸入和行號的定義,它可以將編譯後的代碼段插入到指定了行號的位置。
insertBefore,insertAfter,addCatch和insertAt方法均接受一個String類型的表明源碼塊的入參。此代碼段能夠是簡單的控制類語句if和while,也能夠是以分號結尾的表達式,都須要用左右大括號{}進行包裝。所以,下面的示例源碼都是符合要求的代碼段:
1: System.out.println("Hello");
2: { System.out.println("Hello"); }
3: if (i < 0) { i = -i; }
代碼段能夠指向字段和方法,也能夠爲編譯器添加-g選項來讓其指向插入的方法中的參數。不然,只能利用$0,$1,$2...這種以下的變量來進行訪問。雖然不容許訪問方法中的本地變量,可是在方法體重定義一個新的本地變量是容許的。例外的是,編譯器開啓了-g選項的話,insertAt方法是容許代碼段訪問本地變量的。
insertBefore,insertAfter,addCatch和insertAt入參中的String對象,也就是用戶輸入的代碼段,會被Javassist中的編譯器編譯,因爲此編譯器支持語言擴展,不一樣的$符號有不一樣的含義:
$0
, $1
, $2
, ... this 和實參
$args
參數列表. $args的類型是
Object[]
.
$$
全部實參.例如, m($$)
等價於 m($1,$2,
...)
$cflow(
...)
cflow變量
$r
結果類型. 用於表達式轉換.
$w
包裝類型. 用於表達式轉換.
$_
結果值
$sig
java.lang.Class列表,表明正式入參類型
$type
java.lang.Class對象,表明正式入參值
.
$class
java.lang.Class對象,表明傳入的代碼段
.
傳給目標方法的參數$1,$2...將會替換掉原始的參數名稱。$1表明第一個參數,$2表明第二個參數,以此類推。這些參數的類型和原始的參數類型是一致的。$0等價於this關鍵字,若是方法爲static,那麼$0將不可用。
這些變量的使用方法以下,以Point類爲例:
1: class Point {
2: int x, y;
3: void move(int dx, int dy) { x += dx; y += dy; }
4: }
調用move方法,打印dx和dy的值,執行以下的程序
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("Point");
3: CtMethod m = cc.getDeclaredMethod("move");
4: m.insertBefore("{ System.out.println($1); System.out.println($2); }");
5: cc.writeFile();
須要注意的是,insertBefore方法中的代碼段是被大括號{}包圍的,此方法只接受一個被大括號包圍的代碼段入參。
更改以後的Point類以下:
1: class Point {
2: int x, y;
3: void move(int dx, int dy) {
4: { System.out.println(dx); System.out.println(dy); }
5: x += dx; y += dy;
6: }
7: }
$1和$2被dx和dy替換掉。
從這裏能夠看出,$1,$2,$3...是能夠被更新的。若是一個新的值被賦予了這幾個變量中的任意一個,那麼這個變量對應的參數值也會被更新。下面來講說其餘的參數。
$args變量表明全部參數列表。其類型爲Object數組類型。若是一個參數類型基礎數據類型,好比int,那麼將會被轉換爲java.lang.Integer並放到$args中。所以,$args[0]通常狀況下等價於$1,除非第一個參數的類型爲基礎數據類型。須要注意的是,$args[0]和$0是不等價的,由於$0表明this關鍵字。
若是object列表被賦值給$args,那麼列表中的每一個元素將會被分配給對應的參數。若是一個參數的類型爲基礎數據類型,那麼對應的正確的數據類型爲包裝過的類型。此轉換會發生在參數被分配以前。
$$是被逗號分隔的全部參數列表的縮寫。好比,若是move方法中的參數數量有三個,那麼
move($$)
等價於:
move($1,$2,$3)
若是move()無入參,那麼move($$)等價於move().
$$也能夠被用於其餘的場景,若是你寫了以下的表達式:
exMove($$,context)
那麼此表達式等價於:
exMove($1,$2,$3,context)
須要注意的是,$$雖然說是方法調用的通用符號,可是通常和$proceed聯合使用,後面會講到。
表明着「流程控制」。這個只讀變量會返回方法的遞歸調用深度。
假設以下的方法表明CtMethod中的對象cm:
1: int fact(int n) {
2: if (n <= 1)
3: return n;
4: else
5: return n * fact(n - 1);
6: }
爲了使用$cflow,首先須要引用$cflow,用於監聽fact方法的調用
1: CtMethod cm = ...;
2: cm.useCflow("fact");
useCflow()方法就是用來聲明$cflow變量。任何可用的java命名均可以用來進行識別。此名稱也能夠包含.(點號),好比"my.Test.face"也是能夠的。
而後,$cflow(fact)表明着方法cm遞歸調用的深度。當方法第一次被調用的時候,$cflow(fact)的值爲0,再調用一次,此值將會變爲1.好比:
1: cm.insertBefore("if ($cflow(fact) == 0)"
2: + " System.out.println(\"fact \" + $1);");
代碼段將fact方法進行編譯以便於可以看到對應的參數。因爲$cflow(fact)被選中,那麼對fact方法的遞歸調用將不會顯示參數。
$cflow的值是當前線程中,從cm方法中,最上層棧幀到當前棧幀的值。$cflow同時和cm方法在同一個方法內部的訪問權限也是不同的。
表明着結果類型,必須在轉換表達式中用做類型轉換。好比,以下用法
1: Object result = ... ;
2: $_ = ($r)result;
若是結果類型爲基礎數據類型,那麼($r)須要遵循以下的規則:
首先,若是操做數類型是基礎數據類型,($r)將會被當作普通的轉義符。相反的,若是操做數類型是包裝類型,那麼($r)將會把此包裝類型轉換爲結果類型,好比若是結果類型是int,那麼($r)會將java.lang.Integer轉換爲intl;若是結果類型是void,那麼($r)將不會進行類型轉換;若是當前操做調用了void方法,那麼($r)將會返回null。舉個例子,若是foo方法是void方法,那麼:
1: $_ = ($r)foo();
是一個有效的申明。
轉換符號($r)同時也用於return申明中,即使返回類型是void,以下的return申明也是有效的:
1: return ($r)result;
這裏,result是一個本地變量,因爲($r)這裏作了轉換,那麼返回結果是無效的。此時的return申明和沒有任何返回的return申明是等價的:
1: return;
表明包裝類型。必須在轉義表達式中用於類型轉換。($w)將基礎類型轉換爲對應的包裝類型,以下代碼示例
1: Integer i = ($w)5;
結果類型依據($w)後面的表達式來肯定,若是表達式是double類型,那麼包裝類型則爲java.lang.Double。若是($w)後面的表達式不是基礎類型,那麼($w)將不進行任何轉換。
CtMethod和CtConstructor中的insertAfter方法將編譯過的代碼插入到方法的尾部。以前給過的一些例子有關insertAfter的例子中,不只包括$0.$1這種例子的講解,並且包括$_的這種例子。說道$_變量,它用來表明方法的結果值。其變量類型是方法返回的結果類型。若是返回的結果類型是void,那麼$_的類型是Object類型,可是其值爲null。
儘管利用insertAfter插入的編譯過的代碼,是在方法返回以前被執行的,可是這種代碼也能夠在在方法拋出的exception中執行。爲了可以讓其在方法拋出的exception中執行,insertAfter方法中的第二個參數asFinally必須爲true。
當exception被拋出的時候,利用insertAfter方法插入的代碼段將會和做爲finally代碼塊來執行。此時在編譯過的代碼中,$_的值爲0或者null。當此代碼段執行完畢後,exception會被從新拋給調用端。須要注意的是,$_是永遠不會被拋給調用端的,它會直接被拋棄掉。
$type的值是java.lang.Class對象,表明着返回值的正確的類型。若是它指向的是構造器,那麼其值爲Void.class。
$class的值是java.lang.Class對象,表明着當前編輯的方法,此時和$0是等價的。
此方法用於將代碼段插入到方法體中進行執行,在執行過程當中一旦方法體拋出exception,能夠控制給發送給客戶端的返回。下面的源碼展現了利用特殊的變量$e來指向exception
1: CtMethod m = ...;
2: CtClass etype = ClassPool.getDefault().get("java.io.IOException");
3: m.addCatch("{ System.out.println($e); throw $e; }", etype);
此方法體m被翻譯出來後,展現以下:
1: try {
2: the original method body
3: }
4: catch (java.io.IOException e) {
5: System.out.println(e);
6: throw e;
7: }
須要注意的是,插入的代碼段必須以throw或者return命令結尾。
4.2 修改方法體
CtMethod和CtContructor提供setBody方法來取代整個方法體。此方法可以將傳入的代碼段編譯爲Java字節碼,而後用此字節碼將其原有的方法體給替換掉。若是給定代碼段爲空,那麼被替換的方法體將只有return 0聲明,若是結果類型爲void,那麼則只有 return null聲明。
外部傳入給setBody方法的代碼段,會包含以下的以$開頭的識別碼,這些識別碼有不一樣的含義:
$0
, $1
, $2
, ... this
和實參
$args
參數列表.$args類型爲
Object數組
.
$$
全部參數.
$cflow(
...)
cflow變量
$r
結果類型. 用於表達式轉換.
$w
包裝類型. 用於表達式轉換.
$sig
java.lang.Class對象數組,表明正式的參數類型
.
$type
java.lang.Class
對象,表明正式的結果類型.
$class
java.lang.Class對象,表明當前操做的方法
(等價於$0的類型).
須要注意的是,此時$_是不可用的。
Javassist容許修改方法體中的表達式。能夠利用javassist.expr.ExprEditor類來進行替換操做。用戶能夠經過定義ExprEditor的子類來修改表達式。爲了運行ExprEditor對象,用戶必須調用CtMethod或者CtClass中的instrument方法來進行,示例以下
1: CtMethod cm = ... ;
2: cm.instrument(
3: new ExprEditor() {
4: public void edit(MethodCall m)
5: throws CannotCompileException
6: {
7: if (m.getClassName().equals("Point")
8: && m.getMethodName().equals("move"))
9: m.replace("{ $1 = 0; $_ = $proceed($$); }");
10: }
11: });
上面例子能夠看出,經過搜索cm方法體中,經過替換掉Point類中的move方法爲以下代碼後,
1: { $1 = 0; $_ = $proceed($$); }
move方法中的第一個參數將永遠爲0,須要注意的替換的代碼不只僅是表達式,也能夠是聲明或者代碼塊,可是不能是try-catch聲明。
instrument方法能夠用來搜索方法體,若是找到了待替換的表達式,好比說方法體,字段,建立的類等,以後它會調用ExprEditor對象中的edit方法來進行修改。傳遞給edit方法的參數是找尋到的表達式對象,而後edit方法就能夠經過此表達式對象來進行替換操做。
經過調用傳遞給edit方法的表達式對象中的replace方法,能夠用來替換成給定的的表達式聲明或者代碼段。若是給定的代碼段是空的,那麼也就是說,將會執行replace("{}")方法,那麼以前的代碼段將會在方法體中被移除。若是你僅僅是想在表達式以前或者以後插入代碼段操做,那麼你須要將下面的代碼段傳遞給replace方法:
1: { before-statements;
2: $_ = $proceed($$);
3: after-statements; }
此代碼段能夠是方法調用,字段訪問,對象建立等等。
再來看看第二行聲明:
1: $_ = $proceed();
上面表達式表明着讀訪問操做,也能夠用以下聲明來表明寫訪問操做:
1: $proceed($$);
目標表達式中的本地變量是能夠經過replace方法傳遞到被instrument方法查找到的代碼段中的,若是編譯的時候開啓了-g選項的話。
MethodCall對象表明了一個方法調用,它裏面的replace方法能夠對方法調用進行替換,它經過接收準備傳遞給insertBefore方法中的以$開頭的識別符號來進行替換操做:
$0
The target object of the method call.
This is not equivalent to this
, which represents the caller-side this
object.$0
is null
if the method is static.
$1
, $2
, ...
The parameters of the method call.
$_
The resulting value of the method call.
$r
The result type of the method call.
$class
A java.lang.Class
object representing the class declaring the method.
$sig
An array of java.lang.Class
objects representing the formal parameter types.
$type
A java.lang.Class
object representing the formal result type.
$proceed
The name of the method originally called in the expression.
這裏,方法調用是指MethodCall對象。$w,$args和$$在這裏都是可用的,除非方法調用的結果類型爲void,此時,$_必須被賦值且$_的類型就是返回類型。若是調用的結果類型爲Object,那麼$_的類型就是Object類型且賦予$_的值能夠被忽略。
$proceed不是字符串,而是特殊的語法,它後面必須利用小括號()來包上參數列表。
表明構造器調用,好比this()調用和構造體中的super調用。其中的replace方法能夠用來替換代碼段。它經過接收insertBefore方法中傳入的含有以$開頭的代碼段來進行替換操做:
$0
The target object of the constructor call. This is equivalent to this
.
$1
, $2
, ...
The parameters of the constructor call.
$class
A java.lang.Class
object representing the class declaring the constructor.
$sig
An array of java.lang.Class
objects representing the formal parameter types.
$proceed
The name of the constructor originally called in the expression.
這裏,構造器調用表明着ContructorCall對象,其餘的符號,好比$w,$args和$$也是可用的。
因爲構造器調用,要麼是父類調用,要麼是類中的其餘構造器調用,因此被替換的方法體必須包含構造器調用操做,通常狀況下都是調用$proceed().
$proceed不是字符串,而是特殊的語法,它後面必須利用小括號()來包上參數列表。
此對象表明着字段訪問。ExprEditor中的edit方法中若是有字段訪問被找到,那麼就會接收到這個對象。FieldAccess中的replace方法接收待替換的字段。
在代碼段中,以$開頭的識別碼有以下特殊的含義:
$0
The object containing the field accessed by the expression. This is not equivalent to this
.this
represents the object that the method including the expression is invoked on.$0
is null
if the field is static.
$1
The value that would be stored in the field if the expression is write access.
Otherwise, $1
is not available.
$_
The resulting value of the field access if the expression is read access.
Otherwise, the value stored in $_
is discarded.
$r
The type of the field if the expression is read access.
Otherwise, $r
is void
.
$class
A java.lang.Class
object representing the class declaring the field.
$type
A java.lang.Class
object representing the field type.
$proceed
The name of a virtual method executing the original field access. .
其餘的識別符,例如$w,$args和$$都是可用的。若是表達式是可訪問的,代碼段中,$_必須被賦值,且$_的類型就是此字段的類型。
NewExpr對象表明利用new操做符來進行對象建立。其edit方法接收對象建立行爲,其replace方法則能夠接收傳入的代碼段,將現有的對象建立的表達式進行替換。
在代碼段中,以$開頭的識別碼有以下含義:
$0
null
.
$1
, $2
, ...
The parameters to the constructor.
$_
The resulting value of the object creation.
A newly created object must be stored in this variable.
$r
The type of the created object.
$sig
An array of java.lang.Class
objects representing the formal parameter types.
$type
A java.lang.Class
object representing the class of the created object.
$proceed
The name of a virtual method executing the original object creation. .
其餘的識別碼,好比$w,$args和$$也都是可用的。
此對象表示利用new操做符進行的數組建立操做。其edit方法接收數組建立操做的行爲,其replace方法則能夠接收傳入的代碼段,將現有的數組建立的表達式進行替換。
在代碼段中,以$開頭的識別碼有以下含義:
$0
null
.
$1
, $2
, ...
The size of each dimension.
$_
The resulting value of the array creation.
A newly created array must be stored in this variable.
$r
The type of the created array.
$type
A java.lang.Class
object representing the class of the created array.
$proceed
The name of a virtual method executing the original array creation. .
其餘的識別碼,好比$w,$args和$$也是可用的。
好比,若是數組建立的表達式以下:
String[][] s = new String[3][4];
那麼,$1和$2的值將分別爲3和4,而$3則是不可用的。
可是,若是數組建立的表達式以下:
String[][] s = new String[3][];
那麼,$1的值爲3,而$2是不可用的。
此對象表明instanceof表達式。其edit方法接收instanceof表達式行爲,其replace方法則能夠接收傳入的代碼段,將現有的表達式進行替換。
在代碼段中,以$開頭的識別碼有以下含義:
$0
null
.
$1
The value on the left hand side of the original instanceof
operator.
$_
The resulting value of the expression. The type of $_
is boolean
.
$r
The type on the right hand side of the instanceof
operator.
$type
A java.lang.Class
object representing the type on the right hand side of the instanceof
operator.
$proceed
The name of a virtual method executing the original instanceof
expression.
It takes one parameter (the type is java.lang.Object
) and returns true
if the parameter value is an instance of the type on the right hand side of
the original instanceof
operator. Otherwise, it returns false.
其餘的識別碼,好比$w,$args和$$也是可用的。
此對象表明顯式類型轉換。其edit方法接收顯式類型轉換的行爲,其replace方法則能夠接收傳入的代碼段,將現有的代碼段進行替換。
在代碼段中,以$開頭的識別碼有以下的含義:
$0
null
.
$1
The value the type of which is explicitly cast.
$_
The resulting value of the expression. The type of $_
is the same as the type
after the explicit casting, that is, the type surrounded by ( )
.
$r
the type after the explicit casting, or the type surrounded by ( )
.
$type
A java.lang.Class
object representing the same type as $r
.
$proceed
The name of a virtual method executing the original type casting.
It takes one parameter of the type java.lang.Object
and returns it after
the explicit type casting specified by the original expression.
其餘的識別碼,好比$w,$args和$$也是可用的。
此對象表明try-catch申明中的catch子句。其edit方法接收catch表達式行爲,其insertBefore方法將接收的代碼段進行編譯,而後將其插入到catch子句的開始部分。
在代碼段中,以$開頭的識別碼有以下的含義:
$1
The exception object caught by the catch
clause.
$r
the type of the exception caught by the catch
clause. It is used in a cast expression.
$w
The wrapper type. It is used in a cast expression.
$type
A java.lang.Class
object representing
the type of the exception caught by the catch
clause.
若是一個新的exception對象被賦值給$1,那麼它將會將此exception傳遞給原有的catch子句並被捕捉。
4.3 添加新方法或字段
Javassist一開始就容許用戶建立新的方法和構造,CtNewMethod和CtNewConstructor提供了多種靜態工廠方法來建立CtMethod或者CtConstructor對象。特別說明一下,其make方法能夠從給定的代碼段中建立CtMethod或者CtContructor對象。
好比,以下程序:
1: CtClass point = ClassPool.getDefault().get("Point");
2: CtMethod m = CtNewMethod.make(
3: "public int xmove(int dx) { x += dx; }",
4: point);
5: point.addMethod(m);
添加了一個公共方法xmove到Point類中,此例子中,x是Point類中的int字段。
make方法中的代碼段能夠包含以$開頭的識別碼,可是setBydy方法中的$_除外。若是目標對象和目標方法的名字也傳遞給了make方法,那麼此方法也能夠包含$proceed。好比:
1: CtClass point = ClassPool.getDefault().get("Point");
2: CtMethod m = CtNewMethod.make(
3: "public int ymove(int dy) { $proceed(0, dy); }",
4: point, "this", "move");
上面代碼建立以下ymove方法定義:
1: public int ymove(int dy) { this.move(0, dy); }
須要注意的是,$proceed已經被this.move替換掉了。
Javassist也提供另外一種方式來添加新方法,你能夠首先建立一個abstract方法,而後賦予它方法體:
1: CtClass cc = ... ;
2: CtMethod m = new CtMethod(CtClass.intType, "move",
3: new CtClass[] { CtClass.intType }, cc);
4: cc.addMethod(m);
5: m.setBody("{ x += $1; }");
6: cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);
若是一個abstract方法被添加到了類中,此時Javassist會將此類也變爲abstract,爲了解決這個問題,你不得不利用setBody方法將此類變回非abstract狀態。
當一個方法調用另外一個爲添加到操做類中的方法時,Javassist是沒法編譯此方法的(Javassist能夠編譯本身調用本身的遞歸方法)。爲了添加相互遞歸調用的方法到類中,你須要以下的竅門來進行。假設你想添加m和n方法到cc中:
1: CtClass cc = ... ;
2: CtMethod m = CtNewMethod.make("public abstract int m(int i);", cc);
3: CtMethod n = CtNewMethod.make("public abstract int n(int i);", cc);
4: cc.addMethod(m);
5: cc.addMethod(n);
6: m.setBody("{ return ($1 <= 0) ? 1 : (n($1 - 1) * $1); }");
7: n.setBody("{ return m($1); }");
8: cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);
首先,你須要建立兩個abstract方法並把他們添加到類中。
而後,爲方法設置方法體,方法體內部能夠實現相互調用。
最後,將類變爲非abstract的,由於addMethod添加abstract方法的時候,會自動將類變爲abstract的。
Javassist容許用戶建立一個新的字段:
1: CtClass point = ClassPool.getDefault().get("Point");
2: CtField f = new CtField(CtClass.intType, "z", point);
3: point.addField(f);
上面的diam會添加z字段到Point類中。
若是添加的字段須要設定初始值的話,代碼須要被改成以下方式來進行:
1: CtClass point = ClassPool.getDefault().get("Point");
2: CtField f = new CtField(CtClass.intType, "z", point);
3: point.addField(f, "0"); // initial value is 0.
如今,addField方法接收了第二個用於計算初始值的參數。此參數能夠爲任何符合要求的java表達式。須要注意的是,此表達式不可以以分號結束(;)。
此外,上面的代碼能夠被重寫爲以下更簡單的方式:
1: CtClass point = ClassPool.getDefault().get("Point");
2: CtField f = CtField.make("public int z = 0;", point);
3: point.addField(f);
爲了移除字段或者方法,能夠調用CtClass類中的removeField或者removeMethod來進行。而移除CtConstructor,能夠經過調用removeConstructor方法來進行。
4.4 Annotations
CtClass,CtMethod,CtField和CtConstructor提供了getAnnotations這個快捷的方法來進行註解的讀取操做,它會返回註解類型對象。
好比,以下註解方式:
1: public @interface Author {
2: String name();
3: int year();
4: }
能夠按照以下方式來使用:
1: @Author(name="Chiba", year=2005)
2: public class Point {
3: int x, y;
4: }
此時,這些註解的值就能夠用getAnnotations方法來獲取,此方法將會返回包含了註解類型的對象列表。
1: CtClass cc = ClassPool.getDefault().get("Point");
2: Object[] all = cc.getAnnotations();
3: Author a = (Author)all[0];
4: String name = a.name();
5: int year = a.year();
6: System.out.println("name: " + name + ", year: " + year);
上面代碼打印結果以下:
name: Chiba, year: 2005
因爲Point類的註解只有@Author,因此all列表的長度只有一個,且all[0]就是Author對象。名字和年齡這倆註解字段值能夠經過調用Author對象中的name方法和year來獲取。
爲了使用getAnnotations方法,相似Author這種註解類型必須被包含在當前的類路徑中,同時必須可以被ClassPool對象所訪問,若是類的註解類型沒法被找到,Javassist就沒法獲取此註解類型的默認註解值。
4.5 運行時類支持
在大部分狀況下,在Javassist中修改類並不須要Javassist運行時的支持。可是,有些基於Javassist編譯器生成的字節碼,則須要javassist.runtime這種運行時支持類包的支持(更多細節請訪問此包的API)。須要注意的是,javassist.runtime包是Javassist中進行類修改的時候,惟一可能須要調用的包。
4.6導入
全部的源碼中的類名,必須是完整的(必須包含完整的包名),可是java.lang包例外,好比,Javassist編譯器能夠將java.lang包下的Object轉換爲java.lang.Object。
爲了讓編譯器可以找到類名鎖對應的包,能夠經過調用ClassPool的importPackage方法來進行,示例以下:
1: ClassPool pool = ClassPool.getDefault();
2: pool.importPackage("java.awt");
3: CtClass cc = pool.makeClass("Test");
4: CtField f = CtField.make("public Point p;", cc);
5: cc.addField(f);
第二行表明引入java.awt包,那麼第三行就不會拋出錯誤,由於編譯器能夠將Point類識別爲java.awt.Point。
須要注意的是,importPckage方法不會影響到ClassPool中的get方法操做,只會影響到編譯器的包導入操做。get方法中的參數在任何狀況下,必須是完整的,包含包路徑的。
4.7限制
在當前擴展中,Javassist中的Java編譯器有語言層面的幾大限制,具體以下:
不支持J2SE 5.0中的新語法(包括enums和generics)。Javassist底層API纔會支持註解,具體內容能夠查看javassist.bytecode.annotation包(CtClass和CtBehavior中的getAnnotations方法)。泛型被部分支持,能夠查看後面的章節來了解更詳細的內容。
數組初始化,也就是被雙括號包圍的以逗號分隔的表達式,不支持同時初始化多個。
不支持內部類或者匿名類。須要注意的是,這僅僅是由於編譯器不支持,因此沒法編譯匿名錶達式。可是Javassist自己是能夠讀取和修改內部類或者匿名類的。
continue和break關鍵字不支持。
編譯器不可以正確的識別java的方法派發模型,若是使用了這種方式,將會形成編譯器解析的混亂。好比:
1: class A {}
2: class B extends A {}
3: class C extends B {}
4:
5: class X {
6: void foo(A a) { .. }
7: void foo(B b) { .. }
8: }
若是編譯的表達式是x.foo(new C()),其中x變量指向了X類實例,此時編譯器儘管能夠正確的編譯foo((B)new C()),可是它依舊會將會調用foo(A)。
推薦用戶使用#號分隔符來分隔類名和靜態方法或者字段名。好比在java中,正常狀況下咱們會這麼調用:
javassist.CtClass.intType.getName()
咱們會訪問javassist.Ctclass中的靜態字段intType,而後調用其getName方法。而在Javassist中,咱們能夠按照以下的表達式來書寫:
javassist.CtClass#intType.getName()
這樣編譯器就可以快速的解析此表達式了。
5. 字節碼API
爲了直接修改類文件,Javassist也提供了底層的API,想使用這些API的話,你須要有良好的Java字節碼知識儲備和類文件格式的認知,這樣,你使用這些API修改類文件的時候,才能夠爲所欲爲而不逾矩。
若是你只是想生成一個簡單的類文件,那麼javassist.bytecode.ClassFileWriter類能夠作到。它雖然體積小,可是是比javassist.bytecode.ClassFile更爲快速的存在。
5.1 獲取ClassFile對象
一個javassist.bytecode.ClassFile對象就表明着一個類文件,爲了獲取這個對象,CtClass中的getClassFile方法能夠作到。若是不想這麼作的話,你也能夠直接在類文件中構造一個javassist.bytecode.ClassFile,代碼以下:
1: BufferedInputStream fin
2: = new BufferedInputStream(new FileInputStream("Point.class"));
3: ClassFile cf = new ClassFile(new DataInputStream(fin));
這個代碼片斷展現了從Point.class類中建立出一個ClassFile對象出來。
既然能夠從類文件中建立出ClassFile,那麼也能將ClassFile回寫到類文件中。ClassFile中的write方法就能夠將類文件內容回寫到給定的DataOutputStream中。讓咱們全程展現一下這種作法:
1: ClassFile cf = new ClassFile(false, "test.Foo", null);
2: cf.setInterfaces(new String[] { "java.lang.Cloneable" });
3:
4: FieldInfo f = new FieldInfo(cf.getConstPool(), "width", "I");
5: f.setAccessFlags(AccessFlag.PUBLIC);
6: cf.addField(f);
7:
8: cf.write(new DataOutputStream(new FileOutputStream("Foo.class")));
上面的代碼生成了Foo.class這個類文件,它包含了對以下類的擴展:
1: package test;
2: class Foo implements Cloneable {
3: public int width;
4: }
5.2 添加和刪除成員
ClassFile提供了addField方法和addMethod方法來添加字段或者方法(須要注意的是,在字節碼層面上說來,構造器也被視爲方法),同時也提供了addAttribute方法來爲類文件添加屬性。
須要注意的是FiledInfo,MethodInfo和AttributeInfo對象包含了對ConstPool(const pool table)對象的指向。此ConstPool對象被添加到ClassFile對象中後,在ClassFile對象和FiledInfo對象(或者是MethodInfo對象等)中必須是共享的。換句話說,FiledInfo對象(或者MethodInfo對象等)在不一樣的ClassFile中是不能共享的。
爲了從ClassFile對象中移除字段或者方法,你必須首先經過類的getFields方法獲取全部的字段以及getMethods方法獲取全部的方法來生成java.util.List對象,而後將此對象返回。以後就能夠經過List對象上的remove方法來移除字段或者方法了,屬性的移除方式也不例外,只須要經過FiledInfo或者MethodInfo中的getAttributes方法來獲取到屬性列表後,而後將相關屬性從中移除便可。
5.3 遍歷方法體
爲了校驗方法體中的每一個字節碼指令,CodeIterator則很是有用。想要獲取這個對象的話,須要以下步驟:
1: ClassFile cf = ... ;
2: MethodInfo minfo = cf.getMethod("move"); // we assume move is not overloaded.
3: CodeAttribute ca = minfo.getCodeAttribute();
4: CodeIterator i = ca.iterator();
CodeIterator對象容許你從前到後挨個訪問字節碼指令。以下的方法是CodeIterator中的一部分:
void begin()
void move(int index)
boolean hasNext()
int next()
int byteAt(int index)
int u16bitAt(int index)
int write(byte[] code, int index)
void insert(int index, byte[] code)
下面的代碼段展現了方法體中的全部指令:
1: CodeIterator ci = ... ;
2: while (ci.hasNext()) {
3: int index = ci.next();
4: int op = ci.byteAt(index);
5: System.out.println(Mnemonic.OPCODE[op]);
6: }
5.4 字節碼序列的生成
Bytecode對象表明了字節碼序列,它是一組在持續不斷進行增加的字節碼的簡稱,來看看下面簡單的代碼片斷:
1: ConstPool cp = ...; // constant pool table
2: Bytecode b = new Bytecode(cp, 1, 0);
3: b.addIconst(3);
4: b.addReturn(CtClass.intType);
5: CodeAttribute ca = b.toCodeAttribute();
代碼將會產生以下的序列:
iconst_3 ireturn
你也能夠利用Bytecode中的get方法來獲取一個字節碼數組序列,以後能夠將此數組插入到另外一個代碼段中。
雖然Bytecode提供了一系列的方法添加特殊的指令到序列中,它同時也提供了addOpcode方法來添加8bit操做碼,提供了addIndex方法來添加索引。8bit操做碼的值是在Opcode接口中被定義的。
addOpcode方法和其餘添加特殊指令的方法能夠自動的維持堆棧的深度,除非操做流程出現了分歧,在這裏,咱們可使用Bytecode的getMaxStack方法來獲取堆棧最大深度。同時,堆棧深度和Bytecode對象內建立的CodeAtrribute對象也有關係,爲了從新計算方法體中的最大堆棧深度,可使用CodeAttribute中的computeMaxStack來進行。
Bytecode能夠用來構建一個方法,示例以下:
1: ClassFile cf = ...
2: Bytecode code = new Bytecode(cf.getConstPool());
3: code.addAload(0);
4: code.addInvokespecial("java/lang/Object", MethodInfo.nameInit, "()V");
5: code.addReturn(null);
6: code.setMaxLocals(1);
7:
8: MethodInfo minfo = new MethodInfo(cf.getConstPool(), MethodInfo.nameInit, "()V");
9: minfo.setCodeAttribute(code.toCodeAttribute());
10: cf.addMethod(minfo);
上面的代碼流程是建立了默認的構造函數後,而後將其添加到cf指向的類中。具體說來就是,Bytecode對象首先被轉換成了CodeAttribute對象,接着被添加到minfo所指向的方法中。此方法最終被添加到cf類文件中。
5.5 註解 (Meta tags)
註解在運行時態,做爲一個可見或者不可見的屬性被保存在類文件中。它們能夠從ClassFile,MethodInfo或者FieldInfo對象中經過getAttribute(AnnotationsAttribute.invisibleTag)方法來獲取。更多的謝潔,能夠看看javadoc中關於javassist.bytecode.AnnotationsAttribute類和javassist.bytecode.annotation包的描述。
Javassist也可以讓你利用一些應用層的API來訪問註解。只須要利用CtClass或者CtBehavior中的的getAnnotations方法接口。
6.泛型
Javassist底層的API能夠徹底支持Java5中的泛型。另外一方面,其更高級別的API,諸如CtClass是沒法直接支持泛型的。對於字節碼轉換來講,這也不是什麼大問題。
Java的泛型,採用的是擦除技術。當編譯完畢後,全部的類型參數都將會被擦掉。好比,假設你的源碼定義了一個參數類型Vector<String>:
1: Vector<String> v = new Vector<String>();
2: :
3: String s = v.get(0);
編譯後的字節碼等價於以下代碼:
1: Vector v = new Vector();
2: :
3: String s = (String)v.get(0);
因此,當你寫了一套字節碼轉換器後,你能夠移除掉全部的類型參數。因爲嵌入在Javassist的編譯器不支持泛型,因此利用其編譯的時候,你不得不在調用端作顯式的類型轉換。好比,CtMethod.make方法。可是若是源碼是利用常規的Java編譯器,好比javac,來編譯的話,是無需進行類型轉換的。
若是你有一個類,示例以下:
1: public class Wrapper<T> {
2: T value;
3: public Wrapper(T t) { value = t; }
4: }
想添加Getter<T>接口到Wrapper<T>類中:
1: public interface Getter<T> {
2: T get();
3: }
那麼實際上,你須要添加的接口是Getter(類型參數<T>已經被抹除),須要添加到Wrapper中的方法以下:
1: public Object get() { return value; }
須要注意的是,非類型參數是必須的。因爲get方法返回了Object類型,那麼調用端若是用Javassist編譯的話,就須要進行顯式類型轉換。好比,以下例子,類型參數T是String類型,那麼(String)就必須被按照以下方式插入:
1: Wrapper w = ...
2: String s = (String)w.get();
當使用常規的Java編譯器編譯的時候,類型轉換是不須要的,由於編譯器會自動進行類型轉換。
若是你想在運行時態,經過反射來訪問類型參數,那麼你不得不在類文件中添加泛型符號。更多詳細信息,請參閱API文檔CtClass中的setGenericSignature方法。
7.可變參數
目前,Javassist沒法直接支持可變參數。爲了讓方法能夠支持它,你須要顯式設置方法修改器,其實很簡單,假設你想生成以下的方法:
1: public int length(int... args) { return args.length; }
下面的Javassist代碼將會生成如上的方法:
1: CtClass cc = /* target class */;
2: CtMethod m = CtMethod.make("public int length(int[] args) { return args.length; }", cc);
3: m.setModifiers(m.getModifiers() | Modifier.VARARGS);
4: cc.addMethod(m);
參數類型int...變成了int[]數組,Modifier.VARARGS被添加到了方法修改器中。
爲了可以在Javassist編譯器中調用此方法,你須要這樣來:
1: length(new int[] { 1, 2, 3 });
而不是這樣來:
1: length(1, 2, 3);
8. J2ME
若是你想在J2ME執行環境中修改類文件,你須要進行預校驗操做,此操做會產生棧Map對象,此對象和JDK1.6中的J2SE棧map表有些類似。當且僅當javassist.bytecode.MethodInfo.doPreverify爲true的時候,Javassist會維護J2ME中的棧map。
你也能夠爲修改的方法手動生成一個棧map,好比,一個給定的CtMethod對象中的m,你能夠調用以下方法來生成一個棧map:
1: m.getMethodInfo().rebuildStackMapForME(cpool);
這裏,cpool是ClassPool對象,此對象能夠利用CtClass對象中的getClassPool來獲取,它負責利用給定的類路徑來找尋類文件。爲了獲取全部的CtMethods對象,能夠經過調用CtClass對象的getDeclaredMethods來進行。
9.裝箱/拆箱
在Java中,裝箱和拆箱操做是語法糖。對於字節碼說來,是不存在裝箱和拆箱的。因此Javassist的編譯器不支持裝箱拆箱操做。好比,以下的描述,在java中是可行的:
1: Integer i = 3;
能夠看出,此裝箱操做是隱式的。可是在Javassist中,你必須顯式的將值類型從int轉爲Integer:
1: Integer i = new Integer(3);
10.調試
將CtClass.debugDump設置爲目錄名稱以後,全部被Javassist生成或修改的類文件將會被保存到此目錄中。若是不想這麼作,能夠將CtClass.debugDump設置爲null,須要注意的是,它的默認值就是null。
示例代碼:
1: CtClass.debugDump = "./dump";
此時,全部的被修改的類文件將會被保存到./dump目錄中。