本文譯自Getting Started with Javassist,若是謬誤之處,還請指出。html
bytecode讀寫java
ClassPoolexpress
Class loader數組
自有和定製服務器
Bytecode操控接口數據結構
Genericsapp
Varargs框架
J2MEide
裝箱和拆箱函數
調試
Javassist是用來處理java字節碼的類庫, java字節碼通常存放在後綴名稱爲class的二進制文件中。每一個二進制文件都包含一個java類或者是java接口。
Javasist.CtClass是對類文件的抽象,處於編譯中的此對象能夠用來處理類文件。下面的代碼用來展現一下其簡單用法:
ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("test.Rectangle"); cc.setSuperclass(pool.get("test.Point")); 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()方法來直接獲取修改的字節碼:
byte[] b = cc.toBytecode();
你能夠經過以下代碼直接加載CtClass:
Class clazz = cc.toClass();
toClass()方法被調用,將會使得當前線程中的context class loader加載此CtClass類,而後生成 java.lang.Class對象。更多的細節 ,請參見this section below.
新建類
新建一個類,可使用ClassPool.makeClass()方法來實現:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");
上面的代碼展現的是建立無成員方法的Point類,若是須要附帶方法的話,咱們能夠用CtNewMethod附帶的工廠方法建立,而後利用CtClass.addMethod()將其追加就能夠了 。
makeClass()不能用於建立新的接口。可是makeInterface()能夠。接口的方法能夠用CtNewmethod.abstractMethod()方法來建立,須要注意的是,在這裏,一個接口方法實際上是一個abstract方法。
凍結類
若是CtClass對象被writeFile(),toClass()或者toBytecode()轉換成了類對象,Javassist將會凍結此CtClass對象。任何對此對象的後續更改都是不容許的。之因此這樣作,主要是由於此類已經被JVM加載,因爲JVM自己不支持類的重複加載操做,因此不容許更改。
一個凍結的CtClass對象,能夠經過以下的代碼進行解凍,若是想更改類的話,代碼以下:
CtClasss cc = ...; : cc.writeFile(); cc.defrost(); 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()方法來進行阻止:
CtClasss cc = ...; cc.stopPruning(true); : cc.writeFile(); //轉換爲類文件. //cc不會被精簡.
這樣,CtClass對象就不會被精簡了。當writeFile()方法調用以後,咱們就能夠進行解凍,而後隨心所欲了。
須要注意的是:在調試的時候, debugWriteFile()方法能夠很方便的防止CtClass對象精簡和凍住。
類搜索路徑
ClassPool.getDefault()方法的搜索路徑和JVM的搜索路徑是一致的。若是程序運行在JBoss或者Tomcat服務器上,那麼ClassPool對象也許不可以找到用戶類,緣由是應用服務器用的是多個class loader,其中包括系統的class loader來加載對象。正因如此,ClassPool須要 附加特定的類路徑才行。 假設以下的pool實例表明ClassPool對象:
pool.insertClassPath(new ClassClassPath(this.getClass()));
上面的代碼段註冊了this所指向的類路徑下面的類對象。你能夠用其餘的類對象來代替this.getClass()。這樣就能夠加載其餘不一樣的類對象了。
你也能夠註冊一個目錄名字來做爲類搜索路徑。好比下面代碼中,使用/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);
上面這段代碼將會添加「http://www.javassist.org:80/java/」到類搜索路徑。這個URL主要用來搜索org.javassist包下面的類。好比加載org.javassist.test.Main類,此類將會從以下路徑獲取:
http://www.javassist.org:80/java/org/javassist/test/Main.class
此外,你甚至能夠直接使用一串字節碼,而後建立出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加載了ByteArrayClasPath構建的對象,而後利用get()方法並經過類名,將對象賦值給了CtClass對象。
若是你不知道類的全名,你也能夠用makeClass()來實現:
ClassPool cp = ClassPool.getDefault(); InputStream ins = an input stream for reading a class file; CtClass cc = cp.makeClass(ins);
makeClass()方法利用給定的輸入流構建出CtClass對象。你能夠用餓漢方式直接建立出ClassPool對象,這樣當搜索路徑中有大點的jar文件須要加載的時候,能夠提高一些性能,之因此 這樣作,緣由是ClassPool對象按需加載類文件,因此它可能會重複搜索整個jar包中的每一個類文件,正由於如此,makeClass()能夠用於優化查找的性能。被makeClass()方法加載過的CtClass對象將會留存於ClassPool對象中,不會再進行讀取。
用戶能夠擴展類搜索路徑。能夠經過定義一個新的類,擴展自ClassPath接口,而後返回一個insertClassPath便可。這種作法能夠容許其餘資源被包含到搜索路徑中。
一個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移除。代碼以下:
CtClass cc = ... ; cc.writeFile(); cc.detach();
此CtClass對象被移除後,不能再調用其任何方法。可是你能夠調用ClassPool.get()方法來建立一個新的CtClass實例。
另外一個方法就是用新的ClassPool對象來替代舊的ClassPool對象。若是舊的ClassPool對象被垃圾回收了,那麼其內部的CtClass對象也都會被垃圾回收掉。下面的代碼能夠用來建立一個新的ClassPool對象:
ClassPool cp = new ClassPool(true); //若是須要的話,利用appendClassPath()來添加額外的搜索路徑
上面的代碼和ClassPool.getDefault()來建立ClassPool,效果是同樣的。須要注意的是,ClasssPool.getDefault()是一個單例工廠方法,它可以建立出一個惟一的ClassPool對象並進行重複利用。new ClassPool(true)是一個很快捷的構造方法,它可以建立一個ClassPool對象而後追加系統搜索路徑到其中。和以下的代碼建立行爲表現一致:
ClassPool cp = new ClassPool(); cp.appendSystemPath(); // or append another path by appendClassPath()
級聯ClassPools
若是應用運行在JBOSS/Tomcat上, 那麼建立多個ClassPool對象將會頗有必要。由於每一個類加載其都將會持有一個ClassPool的實例。應用此時最好不用getDefault()方法來建立ClassPool對象,而是使用構造來建立。
多個ClassPool對象像java.lang.ClassLoader同樣作級聯,代碼以下:
ClassPool parent = ClassPool.getDefault(); ClassPool child = new ClassPool(parent); child.insertClassPath("./classes");
若是child.get()被調用,子ClassPool將會首先從父ClassPool進行查找。當父ClassPool查找不到後,而後將會嘗試從./classes目錄進行查找。
若是child.childFirstLookup = true, 子ClassPool將會首先查找本身的目錄,而後查找父ClassPool,代碼以下:
ClassPool parent = ClassPool.getDefault(); ClassPool child = new ClassPool(parent); child.appendSystemPath(); //和默認的搜索地址一致. child.childFirstLookup = true; //修改子類搜索行爲.
爲新類重命名
能夠從已有類建立出新的類,代碼以下:
ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("Point"); 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對象已經不存在了,請看以下代碼:
ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("Point"); CtClass cc1 = pool.get("Point"); //cc1和cc是一致的. cc.setName("Pair"); CtClass cc2 = pool.get("Pair"); //cc2和cc是一致的. 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的實例,代碼片斷以下(以前已經展現過):
ClassPool cp = new ClassPool(true);
若是你有兩個ClassPool對象,那麼你能夠從這兩個對象中分別取出具備相同類文件,可是隸屬於不一樣的CtClass對象生成的,此時能夠經過修改這倆CtClass對象來生成不一樣的類。
從凍結類中建立新類
當CtClass對象經過writeFile()方法或者toBytecode()轉變成類文件的時候,Javassist將不容許對這個CtClass對象有任何修改。所以,當表明Point類的CtClass對象被轉換成了類文件,你不可以先拷貝Point類,而後修更名稱爲Pair類,由於Point類中的setName()方法是沒法被執行的,錯誤使用示例以下:
ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("Point"); cc.writeFile(); cc.setName("Pair"); // wrong since writeFile() has been called.
爲了可以避免這種限制,你應該使用getAndRename()方法,正確示例以下:
ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("Point"); cc.writeFile(); 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()方法:
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()方法中, say()方法被插入了println()方法,以後這個被修改的Hello類實例被建立,say()方法被調用。
須要注意的是,上面代碼中,Hello類是放在toClass()以後被調用的,若是不這麼作的話,JVM將會先加載Hello類,而不是在toClass()方法加載Hello類以後再調用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");
CtMethod m = cc.getDeclaredMethod("say"); m.insertBefore("{ System.out.println(\"Hello.say():\"); }"); Class c = cc.toClass(); Hello h = (Hello)c.newInstance(); h.say();
}
main方法中,第一行的Hello類會被加載,以後調用toClass()將會報錯,由於一個類加載器沒法在同一時刻加載兩個不一樣的Hello類版本。
若是程序跑在JBoss/Tomcat上,利用toClass()方法可能會有些問題。在這種狀況下,你將會遇到ClassCastException錯誤,爲了不這種錯誤,你必須爲toClass()方法提供很是明確的類加載器。好比,在以下代碼中,bean表明你的業務bean對象的時候:
CtClass cc = ...; 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。
好比,下面的例子會拋錯:
MyClassLoader myLoader = new MyClassLoader(); Class clazz = myLoader.loadClass("Box"); Object obj = clazz.newInstance(); Box b = (Box)obj; //會拋出ClassCastException錯誤.
Box類被兩個類加載器所加載,試想一下,假設CL類加載器加載的類包含此代碼段,因爲此代碼段指向MyClassLoader,Class,Object,Box,因此CL加載器也會將這些東西加載進來(除非它是其它類加載器的代理)。所以變量b就是CL中的Box類。從另外一方面說來,myLoader也加載了Box類,obj對象是Box類的實例,所以,代碼的最後一行將一直拋出ClassCastException錯誤,由於obj和b是Box類的不一樣實例副本。
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(); } }假如Window類被L加載器所加載,那麼Window的虛擬加載器和實際加載器都是L。因爲Window類中引用了Box類,JVM將會加載Box類,這裏,假設L將此加載任務代理給了其父加載器PL,那麼Box的類加載器將會變成L,可是其實際加載器將會是PL。所以,在此種狀況下,Point類的虛擬加載器將不是L,而是PL,由於它和Box的實際加載器是同樣的。所以L加載器將永遠不會加載Point類。
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(); } }如今看來,Window類指向了Point,所以類加載器L要想加載Point的話,它必須代理PL。必須杜絕的狀況是,兩個類加載器加載同一個類的狀況。其中一個類加載器必須可以代理另外一個才行。
Point p = box.getSize();沒有拋出錯誤,Window將會破壞Point對象的包裝。舉個例子吧,被PL加載的Point類中,x字段是私有的。可是,若是L利用以下的定義加載了Point類的話,那麼Window類是能夠直接訪問x字段的:
public class Point { public int x, y; // not private public int getX() { return x; } : }想要了解java中更多的類加載器信息,如下信息也許有幫助:
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類的父類,以後程序會加載這個修改的類並建立test.Rectangle類的實例出來。
public interface Translator { public void start(ClassPool pool) throws NotFoundException, CannotCompileException; public void onLoad(ClassPool pool, String classname) throws NotFoundException, CannotCompileException; }當利用javassist.Loader.addTranslator()將事件監聽器添加到javassist.Loader對象上的時候,上面的start()方法將會被觸發。而onLoad()方法的觸發先於javassist.Loader加載一個類,所以onLoad()方法能夠改變已加載的類的定義。
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方法,由於javassistLoader會調用這些方法來獲取類文件。
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所翻譯。
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文件夾下,須要確保當前文件夾不在類搜索目錄下,不然將會被SampleLoader的父類加載器,也就是系統默認的類加載器所加載。./class目錄名稱在insertClassPath方法中必需要有所體現,固然此目錄名稱是能夠隨意改變的。接下來咱們運行以下命令:
% java SampleLoader
此時,類加載器將會加載MyApp類(./class/MyApp.class)並調用MyApp.main方法。
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"文件。
% 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類成功的被加載,而後打印出了hiddenValue。
好比,Point類有一個move方法,其子類ColorPoint不會重寫move方法, 那麼在這裏,兩個move方法,將會被CtMethod對象正確的識別。若是CtMethod對象的方法定義被修改,那麼此修改將會反映到兩個方法上。若是你想只修改ColorPoint類中的move方法,你須要首先建立ColorPoint的副本,那麼其CtMethod對象將也會被複制,CtMethod對象可使用CtNewMethod.copy方法來實現。
Javassist不支持移除方法或者字段,可是支持修更名字。因此若是一個方法再也不須要的話,能夠在CtMethod中對其進行重命名並利用setName方法和setModifiers方法將其設置爲私有方法。
Javassist不支持爲已有的方法添加額外的參數。可是能夠經過爲一個新的方法建立額外的參數。好比,若是你想添加一個額外的int參數newZ到Point類的方法中:
void move(int newX, int newY) { x = newX; y = newY; }
你應當在Point類中添加以下方法:
void move(int newX, int newY, int newZ) { // do what you want with newZ. move(newX, newY); }
Javassist同時也提供底層的API來直接修改原生的類文件。好比,CtClass類中的getClassFile方法能夠返回一個ClassFile對象來表明一個原生的類文件。而CtMethod中的getMethodInfo方法則返回MethodInfo對象來表明一個類中的method_info結構。底層的API單詞大多數來自於JVM,因此用於用起來不會感受到陌生。更多的內容,能夠參看 javassist.bytecode
package.
Javassist修改類文件的時候,通常不須要javassist.runtime包,除非一些特別的以$符號開頭的。這些特殊符號會在後面進行講解。更多的內容,能夠參考javassist.runtime包中的API文檔。
System.out.println("Hello"); { System.out.println("Hello"); } if (i < 0) { i = -i; }代碼段能夠指向字段和方法,也能夠爲編譯器添加-g選項來讓其指向插入的方法中的參數。不然,只能利用$0,$1,$2...這種以下的變量來進行訪問。雖然不容許訪問方法中的本地變量,可是在方法體重定義一個新的本地變量是容許的。例外的是,編譯器開啓了-g選項的話,insertAt方法是容許代碼段訪問本地變量的。
$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對象,表明傳入的代碼段 . |
ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("Point"); CtMethod m = cc.getDeclaredMethod("move"); m.insertBefore("{ System.out.println($1); System.out.println($2); }"); cc.writeFile();須要注意的是,insertBefore方法中的代碼段是被大括號{}包圍的,此方法只接受一個被大括號包圍的代碼段入參。
int fact(int n) { if (n <= 1) return n; else return n * fact(n - 1); }爲了使用$cflow,首先須要引用$cflow,用於監聽fact方法的調用:
CtMethod cm = ...; cm.useCflow("fact");useCflow()方法就是用來聲明$cflow變量。任何可用的java命名均可以用來進行識別。此名稱也能夠包含.(點號),好比"my.Test.face"也是能夠的。
cm.insertBefore("if ($cflow(fact) == 0)" + " System.out.println(\"fact \" + $1);");代碼段將fact方法進行編譯以便於可以看到對應的參數。因爲$cflow(fact)被選中,那麼對fact方法的遞歸調用將不會顯示參數。
Object result = ... ; $_ = ($r)result;若是結果類型爲基礎數據類型,那麼($r)須要遵循以下的規則:
$_ = ($r)foo();是一個有效的申明。
return ($r)result;這裏,result是一個本地變量,因爲($r)這裏作了轉換,那麼返回結果是無效的。此時的return申明和沒有任何返回的return申明是等價的:
return;
Integer i = ($w)5;結果類型依據($w)後面的表達式來肯定,若是表達式是double類型,那麼包裝類型則爲java.lang.Double。若是($w)後面的表達式不是基礎類型,那麼($w)將不進行任何轉換。
CtMethod m = ...; CtClass etype = ClassPool.getDefault().get("java.io.IOException"); m.addCatch("{ System.out.println($e); throw $e; }", etype);此方法體m被翻譯出來後,展現以下:
try {
the original method body
}
catch (java.io.IOException e) {
System.out.println(e);
throw e;
}
須要注意的是,插入的代碼段必須以throw或者return命令結尾。
$0 , $1 , $2 , ... |
this 和實參 |
$args |
參數列表.$args類型爲 Object數組 . |
$$ |
全部參數. |
$cflow( ...) |
cflow變量 |
$r |
結果類型. 用於表達式轉換. |
$w |
包裝類型. 用於表達式轉換. |
$sig |
java.lang.Class對象數組,表明正式的參數類型 . |
$type |
java.lang.Class 對象,表明正式的結果類型. |
$class |
java.lang.Class對象,表明當前操做的方法 (等價於$0的類型). |
CtMethod cm = ... ; cm.instrument( new ExprEditor() { public void edit(MethodCall m) throws CannotCompileException { if (m.getClassName().equals("Point") && m.getMethodName().equals("move")) m.replace("{ $1 = 0; $_ = $proceed($$); }"); } });上面例子能夠看出,經過搜索cm方法體中,經過替換掉Point類中的move方法爲以下代碼後,
{ $1 = 0; $_ = $proceed($$); }move方法中的第一個參數將永遠爲0,須要注意的替換的代碼不只僅是表達式,也能夠是聲明或者代碼塊,可是不能是try-catch聲明。
{ before-statements; $_ = $proceed($$); after-statements; }此代碼段能夠是方法調用,字段訪問,對象建立等等。
$_ = $proceed();上面表達式表明着讀訪問操做,也能夠用以下聲明來表明寫訪問操做:
$proceed($$);目標表達式中的本地變量是能夠經過replace方法傳遞到被instrument方法查找到的代碼段中的,若是編譯的時候開啓了-g選項的話。
$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. |
$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. |
$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. . |
$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. . |
$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. . |
String[][] s = new String[3][4];那麼,$1和$2的值將分別爲3和4,而$3則是不可用的。
String[][] s = new String[3][];那麼,$1的值爲3,而$2是不可用的。
$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. |
$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. |
$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. |
CtClass point = ClassPool.getDefault().get("Point"); CtMethod m = CtNewMethod.make( "public int xmove(int dx) { x += dx; }", point); point.addMethod(m);添加了一個公共方法xmove到Point類中,此例子中,x是Point類中的int字段。
CtClass point = ClassPool.getDefault().get("Point"); CtMethod m = CtNewMethod.make( "public int ymove(int dy) { $proceed(0, dy); }", point, "this", "move");上面代碼建立以下ymove方法定義:
public int ymove(int dy) { this.move(0, dy); }須要注意的是,$proceed已經被this.move替換掉了。
CtClass cc = ... ; CtMethod m = new CtMethod(CtClass.intType, "move", new CtClass[] { CtClass.intType }, cc); cc.addMethod(m); m.setBody("{ x += $1; }"); cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);若是一個abstract方法被添加到了類中,此時Javassist會將此類也變爲abstract,爲了解決這個問題,你不得不利用setBody方法將此類變回非abstract狀態。
CtClass cc = ... ; CtMethod m = CtNewMethod.make("public abstract int m(int i);", cc); CtMethod n = CtNewMethod.make("public abstract int n(int i);", cc); cc.addMethod(m); cc.addMethod(n); m.setBody("{ return ($1 <= 0) ? 1 : (n($1 - 1) * $1); }"); n.setBody("{ return m($1); }"); cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);首先,你須要建立兩個abstract方法並把他們添加到類中。
CtClass point = ClassPool.getDefault().get("Point"); CtField f = new CtField(CtClass.intType, "z", point); point.addField(f);上面的diam會添加z字段到Point類中。
CtClass point = ClassPool.getDefault().get("Point"); CtField f = new CtField(CtClass.intType, "z", point); point.addField(f, "0"); // initial value is 0.如今,addField方法接收了第二個用於計算初始值的參數。此參數能夠爲任何符合要求的java表達式。須要注意的是,此表達式不可以以分號結束(;)。
CtClass point = ClassPool.getDefault().get("Point"); CtField f = CtField.make("public int z = 0;", point); point.addField(f);
public @interface Author { String name(); int year(); }能夠按照以下方式來使用:
@Author(name="Chiba", year=2005) public class Point { int x, y; }此時,這些註解的值就能夠用getAnnotations方法來獲取,此方法將會返回包含了註解類型的對象列表。
CtClass cc = ClassPool.getDefault().get("Point"); Object[] all = cc.getAnnotations(); Author a = (Author)all[0]; String name = a.name(); int year = a.year(); System.out.println("name: " + name + ", year: " + year);上面代碼打印結果以下:
name: Chiba, year: 2005因爲Point類的註解只有@Author,因此all列表的長度只有一個,且all[0]就是Author對象。名字和年齡這倆註解字段值能夠經過調用Author對象中的name方法和year來獲取。
ClassPool pool = ClassPool.getDefault(); pool.importPackage("java.awt"); CtClass cc = pool.makeClass("Test"); CtField f = CtField.make("public Point p;", cc); cc.addField(f);第二行表明引入java.awt包,那麼第三行就不會拋出錯誤,由於編譯器能夠將Point類識別爲java.awt.Point。
class A {} class B extends A {} class C extends B {} class X { void foo(A a) { .. } void foo(B b) { .. } }若是編譯的表達式是x.foo(new C()),其中x變量指向了X類實例,此時編譯器儘管能夠正確的編譯foo((B)new C()),可是它依舊會將會調用foo(A)。
javassist.CtClass.intType.getName()咱們會訪問javassist.Ctclass中的靜態字段intType,而後調用其getName方法。而在Javassist中,咱們能夠按照以下的表達式來書寫:
javassist.CtClass#intType.getName()這樣編譯器就可以快速的解析此表達式了。
BufferedInputStream fin = new BufferedInputStream(new FileInputStream("Point.class")); ClassFile cf = new ClassFile(new DataInputStream(fin));這個代碼片斷展現了從Point.class類中建立出一個ClassFile對象出來。
上面的代碼生成了Foo.class這個類文件,它包含了對以下類的擴展:ClassFile cf = new ClassFile(false, "test.Foo", null); cf.setInterfaces(new String[] { "java.lang.Cloneable" }); FieldInfo f = new FieldInfo(cf.getConstPool(), "width", "I"); f.setAccessFlags(AccessFlag.PUBLIC); cf.addField(f); cf.write(new DataOutputStream(new FileOutputStream("Foo.class")));
ClassFile提供了addField方法和addMethod方法來添加字段或者方法(須要注意的是,在字節碼層面上說來,構造器也被視爲方法),同時也提供了addAttribute方法來爲類文件添加屬性。package test; class Foo implements Cloneable { public int width; }5.2 添加和刪除成員
ClassFile cf = ... ; MethodInfo minfo = cf.getMethod("move"); // we assume move is not overloaded. CodeAttribute ca = minfo.getCodeAttribute(); 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)
下面的代碼段展現了方法體中的全部指令:
CodeIterator ci = ... ; while (ci.hasNext()) { int index = ci.next(); int op = ci.byteAt(index); System.out.println(Mnemonic.OPCODE[op]); }
ConstPool cp = ...; // constant pool table Bytecode b = new Bytecode(cp, 1, 0); b.addIconst(3); b.addReturn(CtClass.intType); CodeAttribute ca = b.toCodeAttribute();代碼將會產生以下的序列:
iconst_3 ireturn你也能夠利用Bytecode中的get方法來獲取一個字節碼數組序列,以後能夠將此數組插入到另外一個代碼段中。
上面的代碼流程是建立了默認的構造函數後,而後將其添加到cf指向的類中。具體說來就是,Bytecode對象首先被轉換成了CodeAttribute對象,接着被添加到minfo所指向的方法中。此方法最終被添加到cf類文件中。ClassFile cf = ... Bytecode code = new Bytecode(cf.getConstPool()); code.addAload(0); code.addInvokespecial("java/lang/Object", MethodInfo.nameInit, "()V"); code.addReturn(null); code.setMaxLocals(1); MethodInfo minfo = new MethodInfo(cf.getConstPool(), MethodInfo.nameInit, "()V"); minfo.setCodeAttribute(code.toCodeAttribute()); cf.addMethod(minfo);
Vector<String> v = new Vector<String>(); : String s = v.get(0);編譯後的字節碼等價於以下代碼:
Vector v = new Vector(); : String s = (String)v.get(0);因此,當你寫了一套字節碼轉換器後,你能夠移除掉全部的類型參數。因爲嵌入在Javassist的編譯器不支持泛型,因此利用其編譯的時候,你不得不在調用端作顯式的類型轉換。好比,CtMethod.make方法。可是若是源碼是利用常規的Java編譯器,好比javac,來編譯的話,是無需進行類型轉換的。
public class Wrapper<T> { T value; public Wrapper(T t) { value = t; } }想添加Getter<T>接口到Wrapper<T>類中:
public interface Getter<T> { T get(); }那麼實際上,你須要添加的接口是Getter(類型參數<T>已經被抹除),須要添加到Wrapper中的方法以下:
public Object get() { return value; }須要注意的是,非類型參數是必須的。因爲get方法返回了Object類型,那麼調用端若是用Javassist編譯的話,就須要進行顯式類型轉換。好比,以下例子,類型參數T是String類型,那麼(String)就必須被按照以下方式插入:
Wrapper w = ... String s = (String)w.get();當使用常規的Java編譯器編譯的時候,類型轉換是不須要的,由於編譯器會自動進行類型轉換。
public int length(int... args) { return args.length; }下面的Javassist代碼將會生成如上的方法:
CtClass cc = /* target class */; CtMethod m = CtMethod.make("public int length(int[] args) { return args.length; }", cc); m.setModifiers(m.getModifiers() | Modifier.VARARGS); cc.addMethod(m);參數類型int...變成了int[]數組,Modifier.VARARGS被添加到了方法修改器中。
length(new int[] { 1, 2, 3 });而不是這樣來:
length(1, 2, 3);
m.getMethodInfo().rebuildStackMapForME(cpool);這裏,cpool是ClassPool對象,此對象能夠利用CtClass對象中的getClassPool來獲取,它負責利用給定的類路徑來找尋類文件。爲了獲取全部的CtMethods對象,能夠經過調用CtClass對象的getDeclaredMethods來進行。
Integer i = 3;能夠看出,此裝箱操做是隱式的。可是在Javassist中,你必須顯式的將值類型從int轉爲Integer:
Integer i = new Integer(3);
CtClass.debugDump = "./dump";此時,全部的被修改的類文件將會被保存到./dump目錄中。