Java動(dòng)態(tài)編程初探-Javassist

最近需要通過(guò)配置生成代碼,減少重復(fù)編碼和維護(hù)成本。用到了一些動(dòng)態(tài)的特性,和大家分享下心得。

我們常用到的動(dòng)態(tài)特性主要是反射,在運(yùn)行時(shí)查找對(duì)象屬性、方法,修改作用域,通過(guò)方法名稱(chēng)調(diào)用方法等。在線的應(yīng)用不會(huì)頻繁使用反射,因?yàn)榉瓷涞男阅荛_(kāi)銷(xiāo)較大。其實(shí)還有一種和反射一樣強(qiáng)大的特性,但是開(kāi)銷(xiāo)卻很低,它就是Javassit。

Javassit其實(shí)就是一個(gè)二方包,提供了運(yùn)行時(shí)操作Java字節(jié)碼的方法。大家都知道,Java代碼編譯完會(huì)生成.class文件,就是一堆字節(jié)碼。JVM(準(zhǔn)確說(shuō)是JIT)會(huì)解釋執(zhí)行這些字節(jié)碼(轉(zhuǎn)換為機(jī)器碼并執(zhí)行),由于字節(jié)碼的解釋執(zhí)行是在運(yùn)行時(shí)進(jìn)行的,那我們能否手工編寫(xiě)字節(jié)碼,再由JVM執(zhí)行呢?答案是肯定的,而Javassist就提供了一些方便的方法,讓我們通過(guò)這些方法生成字節(jié)碼。

類(lèi)似字節(jié)碼操作方法還有ASM。幾種動(dòng)態(tài)編程方法相比較,在性能上Javassist高于反射,但低于ASM,因?yàn)镴avassist增加了一層抽象。在實(shí)現(xiàn)成本上Javassist和反射都很低,而ASM由于直接操作字節(jié)碼,相比Javassist源碼級(jí)別的api實(shí)現(xiàn)成本高很多。幾個(gè)方法有自己的應(yīng)用場(chǎng)景,比如Kryo使用的是ASM,追求性能的最大化。而NBeanCopyUtil采用的是Javassist,在對(duì)象拷貝的性能上也已經(jīng)明顯高于其他的庫(kù),并保持高易用性。實(shí)際項(xiàng)目中推薦先用Javassist實(shí)現(xiàn)原型,若在性能測(cè)試中發(fā)現(xiàn)Javassist成為了性能瓶頸,再考慮使用其他字節(jié)碼操作方法做優(yōu)化。

Javassist的使用很簡(jiǎn)單,首先獲取到class定義的容器ClassPool,通過(guò)它獲取已經(jīng)編譯好的類(lèi)(Compile time class),并給這個(gè)類(lèi)設(shè)置一個(gè)父類(lèi),而writeFile講這個(gè)類(lèi)的定義從新寫(xiě)到磁盤(pán),以便后面使用。

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();

如果需要定義一個(gè)新類(lèi),只需要

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

同樣的還可以通過(guò)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不僅可以生成類(lèi)、變量和方法,還可以操作現(xiàn)有的方法,這在AOP上非常有用,比如做方法調(diào)用的埋點(diǎn)

// Point.java
class Point { int x, y; void move(int dx, int dy) { x += dx; y += dy; }
} // 對(duì)已有代碼每次move執(zhí)行時(shí)做埋點(diǎn)
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表示調(diào)用棧中的第一和第二個(gè)參數(shù),寫(xiě)到磁盤(pán)后的class定義類(lèi)似:


class Point { int x, y; void move(int dx, int dy) {
        { System.out.println(dx); System.out.println(dy); }
        x += dx; y += dy;
    }
}

在使用Javassist時(shí)遇到過(guò)一些問(wèn)題。

1 因?yàn)閠omcat和jboss使用的是獨(dú)立的classloader,而Javassist是通過(guò)默認(rèn)的classloader加載類(lèi),因此直接對(duì)tomcat context中定義的類(lèi)做toClass會(huì)拋出ClassCastException異常,可以用tomcat的classloader加載字節(jié)碼。

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

2 發(fā)現(xiàn)在簡(jiǎn)單的測(cè)試中可以load的類(lèi),在tomcat中無(wú)法load。這是因?yàn)椋珻lassPool.getDefault()查找的路徑和底層的JVM路徑。而tomcat中定義了多個(gè)classloader,因此額外的class路徑需要注冊(cè)到ClassPool中。

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

3 我想在運(yùn)行時(shí)修改類(lèi)的一個(gè)方法,但是JVM是不允許動(dòng)態(tài)的reload類(lèi)定義的。一旦classloader加載了一個(gè)class,在運(yùn)行時(shí)就不能重新加載這個(gè)class的另一個(gè)版本,調(diào)用toClass()會(huì)拋LinkageError。因此需要繞過(guò)這種方式定義全新的class。而toClass()其實(shí)是當(dāng)前thread所在的classloader加載class。

4 Javassist生成的字節(jié)碼由于沒(méi)有class聲明,字節(jié)碼創(chuàng)建變量及方法調(diào)用都需要通過(guò)反射。這點(diǎn)在在線的應(yīng)用上的性能損失是不能接受的,受到NBeanCopyUtil實(shí)現(xiàn)的啟發(fā),可以定義一個(gè)Interface,Javassist的字節(jié)碼實(shí)現(xiàn)這個(gè)Interface,而調(diào)用方通過(guò)這個(gè)接口調(diào)用字節(jié)碼,而不是反射,這樣避免了反射調(diào)用的開(kāi)銷(xiāo)。還有一點(diǎn)字節(jié)碼new一個(gè)變量也是通過(guò)反射,因此通過(guò)代理的方法,將每個(gè)pv都需要new的字節(jié)碼對(duì)象改為每次new一個(gè)代理對(duì)象,代理到常駐內(nèi)存的字節(jié)碼對(duì)象中,這樣避免了每次反射的開(kāi)銷(xiāo)。

參考資料:

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,323評(píng)論 25 708
  • 陰雨綿綿的五月,有人忙著恩愛(ài),有人忙著畢業(yè)也有人忙著無(wú)所事事。昨天是朋友圈暴擊,明天又是畢業(yè)散伙飯的傷感,時(shí)間總是...
    馬尾_閱讀 711評(píng)論 0 3
  • 今天腰疼的快斷了。是不是得臥一臥床啊我去。 我去啊,剛才上茅房,褐色分泌物了我??汕f(wàn)別有事啊?。?!
    秋天的貓oOo閱讀 115評(píng)論 0 0
  • 黑色的夜 沒(méi)有一絲絲光可見(jiàn) 不知是否該留戀白天那華麗的場(chǎng)面 耳畔邊寂靜的聲 一張張摘了面具的臉
    王子真心閱讀 190評(píng)論 0 1
  • 01 此刻,我和表姐坐在沙發(fā)上邊吃水果邊看電視,姐夫在廚房里忙著做飯,還時(shí)不時(shí)探頭沖我倆看看。我轉(zhuǎn)頭看見(jiàn)一臉慵懶的...
    花田大叔說(shuō)閱讀 2,466評(píng)論 6 5

友情鏈接更多精彩內(nèi)容