Java動態編程初探——Javassist

 

最近須要經過配置生成代碼,減小重複編碼和維護成本。用到了一些動態的特性,和你們分享下心得。html

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

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

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

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

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上很是有用,好比作方法調用的埋點this

 

// 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

相關文章
相關標籤/搜索