最近需要通过配置生成代码,减少重复编码和维护成本。用到了一些动态的特性,和大家分享下心得。
我们常用到的动态特性主要是反射,在运行时查找对象属性、开发方法 ,修改作用域,通过开发方法 名称调用开发方法 等。在线的应用开发不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是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