javassist的API一些基本操作

 

最近需要通过配置生成代码,减少重复编码和维护成本。用到了一些动态的特性,和大家分享下心得。

我们常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是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一个代理对象,代理到常驻内存的字节码对象中,这样避免了每次反射的开销。

测试用例的完整代码:

package Javassist;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMember;
import javassist.CtMethod;
import javassist.CtNewMethod;


/**
 * 测试javassist的API
 * @author lenovo
 *
 */
public class javassitApi {

	/**
	 * 处理类的基本方法
	 * @throws Exception 
	 */
	public static void test01() throws Exception {
		ClassPool pool = ClassPool.getDefault(); //获得类池
		CtClass cc = pool.get("Javassist.Emp");
		//获得字节码
		byte[] bytes = cc.toBytecode();
		System.out.println(Arrays.toString(bytes));
		
		System.out.println(cc.getName()); //获得全类名
		System.out.println(cc.getSimpleName()); //获得简要类名
		System.out.println(cc.getSuperclass()); //获得父类名
	}
	
	/**
	 * 测试产生新的方法加入到Emp类里
	 * @throws Exception 
	 */
	public static void test02() throws Exception {
		ClassPool pool = ClassPool.getDefault();
		CtClass cc = pool.get("Javassist.Emp");
		CtMethod m = new CtMethod(CtClass.intType, "add", 
				new CtClass[]{CtClass.intType, CtClass.intType}, cc);
		m.setModifiers(Modifier.PUBLIC); //设置两个成员变量的类型为公开
		m.setBody("{return $1+$2;}"); //设置返回值$1+$2分别代表我传入的两个形参,$0代表this
		cc.addMethod(m);
		
		//通过反射调用新生成的方法
		Class clazz = cc.toClass(); //把CtClass转化成我们反射里的class对象
		Object obj = clazz.newInstance(); //调用Emp无参构造器来创建一个对象
		Method method = clazz.getDeclaredMethod("add", int.class, int.class);
		Object rs = method.invoke(obj, 200, 300);
		System.out.println(rs);
	}
	
	/**
	 * 修改已有的方法的信息
	 * @throws Exception
	 */
	public static void test03() throws Exception {
		ClassPool pool = ClassPool.getDefault();
		CtClass cc = pool.get("Javassist.Emp");
		
		CtMethod cm = cc.getDeclaredMethod("sayHello", new CtClass[] {CtClass.intType});
		cm.insertBefore("System.out.println($1); System.out.println(\"start!!!\");");//将此方法加到sayHello方法之前
		
		//通过反射调用新生成的方法
		Class clazz = cc.toClass(); //把CtClass转化成我们反射里的class对象
		Object obj = clazz.newInstance(); //调用Emp无参构造器来创建一个对象
		Method method = clazz.getDeclaredMethod("sayHello", int.class);
		Object rs = method.invoke(obj, 200);

	}
	
	
	/**
	 * 添加新属性
	 * @throws Exception
	 */
	public static void test04() throws Exception {
		ClassPool pool = ClassPool.getDefault();
		CtClass cc = pool.get("Javassist.Emp");
		
		CtField f1 = new CtField(CtClass.intType, "salary", cc);
		f1.setModifiers(Modifier.PRIVATE);
		cc.addField(f1);
		cc.getDeclaredField("ename");//获取指定的属性
		
		//增加相应的set和get方法
		cc.addMethod(CtNewMethod.getter("getsalary", f1));
		cc.addMethod(CtNewMethod.getter("setsalary", f1));

	}
	
	/**
	 * 获得构造器
	 * @throws Exception
	 */
	public static void test05() throws Exception {
		ClassPool pool = ClassPool.getDefault();
		CtClass cc = pool.get("Javassist.Emp");
		CtConstructor[] cs = cc.getConstructors();//获得所有的构造器
		for(CtConstructor c: cs) {
			System.out.println(cs);
		}
	}
	
	/**
	 * javassist操作注解
	 * @throws Exception
	 */
	public static void test06() throws Exception {
		ClassPool pool = ClassPool.getDefault();
		CtClass cc = pool.get("Javassist.Emp");
		
		Object[] all = cc.getAnnotations();
		Author a = (Author)all[0];
		String name = a.name();
		int year = a.year();
		System.out.println("name: " + name + ", year: " + year);
		
	}
	
	
	public static void main(String[] args) throws Exception {
//		test01();
//		test02();
//		test03();
//		test04();
//		test05();
		test06();
	}

}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

金州饿霸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值