本文譯自Getting Started with Javassist,如果謬誤之處,還請指出。
-
bytecode讀寫
-
ClassPool
-
Class loader
-
自有和定製
-
Bytecode操控接口
-
Generics
-
Varargs
-
J2ME
-
裝箱和拆箱
-
調試
1. bytecode讀寫
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即可。這種做法可以允許其他資源被包含到搜索路徑中。
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移除。代碼如下:
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中更多的類加載器信息,以下信息也許有幫助:
- Sheng Liang and Gilad Bracha, "Dynamic Class Loading in the Java Virtual Machine",
ACM OOPSLA'98
- , pp.36-44, 1998.
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方法。3.5 修改系統類
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。
3.6 運行狀態下重新加載類
4. 定製化
比如,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文檔。
4.1 方法體前/後穿插代碼段
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對象,代表傳入的代碼段 . |
$0, $1, $2, ...
- class Point { int x, y; void move(int dx, int dy) { x += dx; y += dy; } }
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方法中的代碼段是被大括號{}包圍的,此方法只接受一個被大括號包圍的代碼段入參。
- class Point { int x, y; void move(int dx, int dy) { { System.out.println(dx); System.out.println(dy); } x += dx; y += dy; } }
$args
$args變量代表所有參數列表。其類型爲Object數組類型。如果一個參數類型基礎數據類型,比如int,那麼將會被轉換爲java.lang.Integer並放到$args中。因此,$args[0]一般情況下等價於$1,除非第一個參數的類型爲基礎數據類型。需要注意的是,$args[0]和$0是不等價的,因爲$0代表this關鍵字。$$
$$是被逗號分隔的所有參數列表的縮寫。比如,如果move方法中的參數數量有三個,那麼$cflow
代表着“流程控制”。這個只讀變量會返回方法的遞歸調用深度。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方法的遞歸調用將不會顯示參數。
$r
代表着結果類型,必須在轉換表達式中用作類型轉換。比如,如下用法:Object result = ... ; $_ = ($r)result;如果結果類型爲基礎數據類型,那麼($r)需要遵循如下的規則:
$_ = ($r)foo();是一個有效的申明。
return ($r)result;這裏,result是一個本地變量,由於($r)這裏做了轉換,那麼返回結果是無效的。此時的return申明和沒有任何返回的return申明是等價的:
return;
$w
代表包裝類型。必須在轉義表達式中用於類型轉換。($w)將基礎類型轉換爲對應的包裝類型,如下代碼示例:Integer i = ($w)5;結果類型依據($w)後面的表達式來確定,如果表達式是double類型,那麼包裝類型則爲java.lang.Double。如果($w)後面的表達式不是基礎類型,那麼($w)將不進行任何轉換。
$_
CtMethod和CtConstructor中的insertAfter方法將編譯過的代碼插入到方法的尾部。之前給過的一些例子有關insertAfter的例子中,不僅包括$0.$1這種例子的講解,而且包括$_的這種例子。說道$_變量,它用來代表方法的結果值。其變量類型是方法返回的結果類型。如果返回的結果類型是void,那麼$_的類型是Object類型,但是其值爲null。$sig
$type的值是java.lang.Class對象,代表着返回值的正確的類型。如果它指向的是構造器,那麼其值爲Void.class。$class
$class的值是java.lang.Class對象,代表着當前編輯的方法,此時和$0是等價的。addCatch()
此方法用於將代碼段插入到方法體中進行執行,在執行過程中一旦方法體拋出exception,可以控制給發送給客戶端的返回。下面的源碼展示了利用特殊的變量$e來指向exception: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命令結尾。4.2 修改方法體
$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方法來進行,示例如下: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聲明。
instrument方法可以用來搜索方法體,如果找到了待替換的表達式,比如說方法體,字段,創建的類等,之後它會調用ExprEditor對象中的edit方法來進行修改。傳遞給edit方法的參數是找尋到的表達式對象,然後edit方法就可以通過此表達式對象來進行替換操作。
{ before-statements; $_ = $proceed($$); after-statements; }此代碼段可以是方法調用,字段訪問,對象創建等等。
$_ = $proceed();上面表達式代表着讀訪問操作,也可以用如下聲明來代表寫訪問操作:
$proceed($$);目標表達式中的本地變量是可以通過replace方法傳遞到被instrument方法查找到的代碼段中的,如果編譯的時候開啓了-g選項的話。
javassist.expr.MethodCall
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. |
javassist.expr.ConstructorCall
代表構造器調用,比如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. |
javassist.expr.FieldAccess
此對象代表着字段訪問。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. . |
javassist.expr.NewExpr
$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. . |
javassist.expr.NewArray
$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是不可用的。
javassist.expr.Instanceof
此對象代表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. |
javassist.expr.Cast
$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. |
javassist.expr.Handler
$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. |
4.3 添加新方法或字段
添加一個方法
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狀態。
相互遞歸調用方法
當一個方法調用另一個爲添加到操作類中的方法時,Javassist是無法編譯此方法的(Javassist可以編譯自己調用自己的遞歸方法)。爲了添加相互遞歸調用的方法到類中,你需要如下的竅門來進行。假設你想添加m和n方法到cc中: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);
成員移除
爲了移除字段或者方法,可以調用CtClass類中的removeField或者removeMethod來進行。而移除CtConstructor,可以通過調用removeConstructor方法來進行。4.4 Annotations
CtClass,CtMethod,CtField和CtConstructor提供了getAnnotations這個快捷的方法來進行註解的讀取操作,它會返回註解類型對象。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來獲取。
4.5 運行時類支持
在大部分情況下,在Javassist中修改類並不需要Javassist運行時的支持。但是,有些基於Javassist編譯器生成的字節碼,則需要javassist.runtime這種運行時支持類包的支持(更多細節請訪問此包的API)。需要注意的是,javassist.runtime包是Javassist中進行類修改的時候,唯一可能需要調用的包。4.6導入
所有的源碼中的類名,必須是完整的(必須包含完整的包名),但是java.lang包例外,比如,Javassist編譯器可以將java.lang包下的Object轉換爲java.lang.Object。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。
4.7限制
在當前擴展中,Javassist中的Java編譯器有語言層面的幾大限制,具體如下: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()這樣編譯器就能夠快速的解析此表達式了。
5. 字節碼API
5.1 獲取ClassFile對象
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 添加和刪除成員
5.3 遍歷方法體
爲了校驗方法體中的每個字節碼指令,CodeIterator則非常有用。想要獲取這個對象的話,需要如下步驟: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()
如果存在指令的話,返回trueint next()
返回下一個指令的索引
需要注意的是,此方法並不會返回下一個指令的操作碼int byteAt(int index)
返回指定索引處的無符號8bit位長值.int u16bitAt(int index)
返回指定索引處的無符號16bit位長值.int write(byte[] code, int index)
在指定索引處寫入字節數組.void insert(int index, byte[] code)
在指定索引處寫入字節數組,其他字節碼的offset等將會自適應更改。
下面的代碼段展示了方法體中的所有指令:
CodeIterator ci = ... ; while (ci.hasNext()) { int index = ci.next(); int op = ci.byteAt(index); System.out.println(Mnemonic.OPCODE[op]); }
5.4 字節碼序列的生成
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);
5.5 註解 (Meta tags)
註解在運行時態,作爲一個可見或者不可見的屬性被保存在類文件中。它們可以從ClassFile,MethodInfo或者FieldInfo對象中通過getAttribute(AnnotationsAttribute.invisibleTag)方法來獲取。更多的謝潔,可以看看javadoc中關於javassist.bytecode.AnnotationsAttribute類和javassist.bytecode.annotation包的描述。6.泛型
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編譯器編譯的時候,類型轉換是不需要的,因爲編譯器會自動進行類型轉換。
7.可變參數
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);
8. J2ME
m.getMethodInfo().rebuildStackMapForME(cpool);這裏,cpool是ClassPool對象,此對象可以利用CtClass對象中的getClassPool來獲取,它負責利用給定的類路徑來找尋類文件。爲了獲取所有的CtMethods對象,可以通過調用CtClass對象的getDeclaredMethods來進行。
9.裝箱/拆箱
Integer i = 3;可以看出,此裝箱操作是隱式的。但是在Javassist中,你必須顯式的將值類型從int轉爲Integer:
Integer i = new Integer(3);
10.調試
CtClass.debugDump = "./dump";此時,所有的被修改的類文件將會被保存到./dump目錄中。