最近需要通過(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://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