最近需要通過配置生成代碼,減少重復編碼和維護成本。用到了一些動態的特性,和大家分享下心得。
我們常用到的動態特性主要是反射,在運行時查找對象屬性、方法,修改作用域,通過方法名稱調用方法等。在線的應用不會頻繁使用反射,因為反射的性能開銷較大。其實還有一種和反射一樣強大的特性,但是開銷卻很低,它就是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