Java動態(tài)編程初探——Javassist
最近需要通過配置生成代碼,減少重復編碼和維護成本抒寂。用到了一些動態(tài)的特性结啼,和大家分享下心得嘶窄。
我們常用到的動態(tài)特性主要是反射恨狈,在運行時查找對象屬性、方法敞映,修改作用域沸伏,通過方法名稱調(diào)用方法等。在線的應用不會頻繁使用反射动分,因為反射的性能開銷較大毅糟。其實還有一種和反射一樣強大的特性,但是開銷卻很低澜公,它就是Javassit姆另。
Javassit其實就是一個二方包,提供了運行時操作Java字節(jié)碼的方法坟乾。大家都知道迹辐,Java代碼編譯完會生成.class文件,就是一堆字節(jié)碼甚侣。JVM(準確說是JIT)會解釋執(zhí)行這些字節(jié)碼(轉(zhuǎn)換為機器碼并執(zhí)行)明吩,由于字節(jié)碼的解釋執(zhí)行是在運行時進行的,那我們能否手工編寫字節(jié)碼殷费,再由JVM執(zhí)行呢印荔?答案是肯定的,而Javassist就提供了一些方便的方法详羡,讓我們通過這些方法生成字節(jié)碼仍律。
類似字節(jié)碼操作方法還有ASM。幾種動態(tài)編程方法相比較实柠,在性能上Javassist高于反射水泉,但低于ASM,因為Javassist增加了一層抽象窒盐。在實現(xiàn)成本上Javassist和反射都很低草则,而ASM由于直接操作字節(jié)碼,相比Javassist源碼級別的api實現(xiàn)成本高很多登钥。幾個方法有自己的應用場景畔师,比如Kryo使用的是ASM,追求性能的最大化牧牢。而NBeanCopyUtil采用的是Javassist看锉,在對象拷貝的性能上也已經(jīng)明顯高于其他的庫姿锭,并保持高易用性。實際項目中推薦先用Javassist實現(xiàn)原型伯铣,若在性能測試中發(fā)現(xiàn)Javassist成為了性能瓶頸呻此,再考慮使用其他字節(jié)碼操作方法做優(yōu)化。
Javassist的使用很簡單腔寡,首先獲取到class定義的容器ClassPool焚鲜,通過它獲取已經(jīng)編譯好的類(Compile time class),并給這個類設置一個父類放前,而writeFile講這個類的定義從新寫到磁盤忿磅,以便后面使用。
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();
由CtClass可以方便的獲取字節(jié)碼和加載字節(jié)碼:
byte[] b = cc.toBytecode();
Class clazz = cc.toClass();
如果需要定義一個新類凭语,只需要
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");
同樣的還可以通過CtMethod和CtField構(gòu)造方法和成員甚至Annotation葱她。
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("foo");
CtMethod mthd = CtNewMethod.make("public Integer
getInteger() { return null; }", cc);
cc.addMethod(mthd);
CtField f = new CtField(CtClass.intType, "i", cc);
point.addField(f);
clazz = cc.toClass(); Object instance = class.newInstance();
Javassist不僅可以生成類、變量和方法似扔,還可以操作現(xiàn)有的方法吨些,這在AOP上非常有用,比如做方法調(diào)用的埋點
// Point.java
class Point {
int x, y;
void move(int dx, int dy) { x += dx; y += dy; }
}
// 對已有代碼每次move執(zhí)行時做埋點
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();
其中2表示調(diào)用棧中的第一和第二個參數(shù)炒辉,寫到磁盤后的class定義類似:
class Point {
int x, y;
void move(int dx, int dy) {
{ System.out.println(dx); System.out.println(dy); }
x += dx; y += dy;
}
}
在使用Javassist時遇到過一些問題豪墅。
1 因為tomcat和jboss使用的是獨立的classloader,而Javassist是通過默認的classloader加載類黔寇,因此直接對tomcat context中定義的類做toClass會拋出ClassCastException異常偶器,可以用tomcat的classloader加載字節(jié)碼。
CtClass cc = ...;
Class c = cc.toClass(bean.getClass().getClassLoader());
2 發(fā)現(xiàn)在簡單的測試中可以load的類啡氢,在tomcat中無法load状囱。這是因為,ClassPool.getDefault()查找的路徑和底層的JVM路徑倘是。而tomcat中定義了多個classloader亭枷,因此額外的class路徑需要注冊到ClassPool中。
pool.insertClassPath(new ClassClassPath(this.getClass()));
3 我想在運行時修改類的一個方法搀崭,但是JVM是不允許動態(tài)的reload類定義的叨粘。一旦classloader加載了一個class,在運行時就不能重新加載這個class的另一個版本瘤睹,調(diào)用toClass()會拋LinkageError升敲。因此需要繞過這種方式定義全新的class。而toClass()其實是當前thread所在的classloader加載class轰传。
4 Javassist生成的字節(jié)碼由于沒有class聲明驴党,字節(jié)碼創(chuàng)建變量及方法調(diào)用都需要通過反射。這點在在線的應用上的性能損失是不能接受的获茬,受到NBeanCopyUtil實現(xiàn)的啟發(fā)港庄,可以定義一個Interface倔既,Javassist的字節(jié)碼實現(xiàn)這個Interface,而調(diào)用方通過這個接口調(diào)用字節(jié)碼鹏氧,而不是反射渤涌,這樣避免了反射調(diào)用的開銷。還有一點字節(jié)碼new一個變量也是通過反射把还,因此通過代理的方法实蓬,將每個pv都需要new的字節(jié)碼對象改為每次new一個代理對象,代理到常駐內(nèi)存的字節(jié)碼對象中吊履,這樣避免了每次反射的開銷安皱。
參考資料:
http://notatube.blogspot.com/2010/11/project-lombok-trick-explained.html
http://www.csg.ci.i.u-tokyo.ac.jp/~chiba/javassist/tutorial/tutorial.html
http://www.ibm.com/developerworks/cn/java/coretech/java-dynamic.htm