Java 动态编程初探之 Javassist:最近需要通过配置生成代码,减少重复编码和维护成本。用到了一些动态的特性,和大家分享下心得。
我们常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是 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.javaclass 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 一个代理对象,代理到常驻内存的字节码对象中,这样避免了每次反射的开销。
就爱阅读 www.92to.com 网友整理上传, 为您提供最全的知识大全, 期待您的分享,转载请注明出处。
来源: