Java動態編程初探——Javassist


最近需要通過配置生成代碼,減少重復編碼和維護成本。用到了一些動態的特性,和大家分享下心得。

我們常用到的動態特性主要是反射,在運行時查找對象屬性、方法,修改作用域,通過方法名稱調用方法等。在線的應用不會頻繁使用反射,因為反射的性能開銷較大。其實還有一種和反射一樣強大的特性,但是開銷卻很低,它就是Javassit。

Javassit其實就是一個二方包,提供了運行時操作Java字節碼的方法。大家都知道,Java代碼編譯完會生成.class文件,就是一堆字節碼。JVM(准確說是JIT)會解釋執行這些字節碼(轉換為機器碼並執行),由於字節碼的解釋執行是在運行時進行的,那我們能否手工編寫字節碼,再由JVM執行呢?答案是肯定的,而Javassist就提供了一些方便的方法,讓我們通過這些方法生成字節碼。

類似字節碼操作方法還有ASM。幾種動態編程方法相比較,在性能上Javassist高於反射,但低於ASM,因為Javassist增加了一層抽象。在實現成本上Javassist和反射都很低,而ASM由於直接操作字節碼,相比Javassist源碼級別的api實現成本高很多。幾個方法有自己的應用場景,比如Kryo使用的是ASM,追求性能的最大化。而NBeanCopyUtil采用的是Javassist,在對象拷貝的性能上也已經明顯高於其他的庫,並保持高易用性。實際項目中推薦先用Javassist實現原型,若在性能測試中發現Javassist成為了性能瓶頸,再考慮使用其他字節碼操作方法做優化。

Javassist的使用很簡單,首先獲取到class定義的容器ClassPool,通過它獲取已經編譯好的類(Compile time class),並給這個類設置一個父類,而writeFile講這個類的定義從新寫到磁盤,以便后面使用。

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

由CtClass可以方便的獲取字節碼和加載字節碼:

byte[] b = cc.toBytecode();
Class clazz = cc.toClass();

如果需要定義一個新類,只需要

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

同樣的還可以通過CtMethod和CtField構造方法和成員甚至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不僅可以生成類、變量和方法,還可以操作現有的方法,這在AOP上非常有用,比如做方法調用的埋點

// Point.java
class Point {
    int x, y;
    void move(int dx, int dy) { x += dx; y += dy; }
}

// 對已有代碼每次move執行時做埋點
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();

其中$1和$2表示調用棧中的第一和第二個參數,寫到磁盤后的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加載字節碼。

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

2 發現在簡單的測試中可以load的類,在tomcat中無法load。這是因為,ClassPool.getDefault()查找的路徑和底層的JVM路徑。而tomcat中定義了多個classloader,因此額外的class路徑需要注冊到ClassPool中。

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

3 我想在運行時修改類的一個方法,但是JVM是不允許動態的reload類定義的。一旦classloader加載了一個class,在運行時就不能重新加載這個class的另一個版本,調用toClass()會拋LinkageError。因此需要繞過這種方式定義全新的class。而toClass()其實是當前thread所在的classloader加載class。

4 Javassist生成的字節碼由於沒有class聲明,字節碼創建變量及方法調用都需要通過反射。這點在在線的應用上的性能損失是不能接受的,受到NBeanCopyUtil實現的啟發,可以定義一個Interface,Javassist的字節碼實現這個Interface,而調用方通過這個接口調用字節碼,而不是反射,這樣避免了反射調用的開銷。還有一點字節碼new一個變量也是通過反射,因此通過代理的方法,將每個pv都需要new的字節碼對象改為每次new一個代理對象,代理到常駐內存的字節碼對象中,這樣避免了每次反射的開銷。

參考資料:

http://asm.ow2.org/

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.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM