描述
java实现动态性的方式一个是反射,另一个就是字节码操作。字节码的性能比反射要高一些,但是并不是只用字节码就行了,反射和字节码很多时候是相辅相成的。
通过反射我们可以动态的创建一个对象,那么通过字节码我们可以做什么呢?
- 动态生成新的类
- 动态改变某个类的结构(添加、删除、修改,新的属性、方法)
也就是说字节码操作可以在运行的时候添加删除修改某个类的属性方法,也可以给类新的属性和方法,甚至可以新建一个没有写在class里面的类,是不是很强大。
字节码能大大的提高java的动态性,他还有什么优势呢:
- 比反射开销小,性能高
- JAVAasist性能高于反射,低于ASM
javaasist和ASM是常见的字节码操作类库,常见的字节码操作类库有:
BCEL和ASM是指令级操作,所以它们的性能很高,但同时,它们学习起来的难度很大。
CGLIB是基于ASM的,javassist是代码级的,它们的性能没有上面两个高,但是javassist学习起来简单,所以一般用javassist来操作字节码。
要使用javassist库,需要去它的官网下载(要翻墙)百度搜一下也都可以。
使用
创建一个类
把javassist依赖库装好之后,就可以使用了。
首先要获得类池,用类池创建一个编译时的类:
//获得类池
ClassPool cp = ClassPool.getDefault();
//创建一个类 CtClass 编译时的类
CtClass cc = cp.makeClass("testJavassist.emp");
testJavassist.emp就是等会把这个类文件存放在一个文件夹中的结构
然后后我们就可以创造类中的属性,方法和构造器了,用法其实和放射类似:
//创建属性
CtField cf1 = CtField.make("private int id;", cc);
CtField cf2 = CtField.make("private String school;", cc);
//把属性加到类里面
cc.addField(cf1);
cc.addField(cf2);
//创建方法
CtMethod cm1 = CtMethod.make("public int getId(){return id;}", cc);
CtMethod cm2 = CtMethod.make("public void setId(int id){this.id = id;}", cc);
//把方法加到类中
cc.addMethod(cm1);
cc.addMethod(cm2);
//创建构造器
CtConstructor ccon = new CtConstructor(new CtClass[]{CtClass.intType,cp.get("java.lang.String")},cc);
ccon.setBody("{this.id = id;this.school = school;}");
cc.addConstructor(ccon);
每创建好属性方法构造器,就将其加入类中,这样就将一个类创建好了,之后我们把它写入文件中:
cc.writeFile("d:/myjava");
写进文件之后,在d盘的myjava/testJavassist文件夹下会有这样的文件:
这是class文件,打开后不能看我们创建的代码:
需要下载反编译软件 下载
下载解压后打开exe文件:
打开后找到我们的class文件,就可以看到我们创建好的类了:
常用的API
获取类的信息:
public static void test01() throws Exception {
ClassPool cp = ClassPool.getDefault();
//获得指定类
CtClass cc = cp.get("testJavasisst.emp");
//获得字节码
byte[] data = cc.toBytecode();
System.out.println(Arrays.toString(data));
System.out.println(cc.getName());//类名
System.out.println(cc.getSimpleName());//简要类名
System.out.println(cc.getSuperclass());//获取父类
System.out.println(cc.getInterfaces());//获取接口
}
创建新的方法:
public static void test02() throws Exception {
ClassPool cp = ClassPool.getDefault();
//获得指定类
CtClass cc = cp.get("testJavasisst.emp");
//一种方式产生新方法
CtMethod cm = CtNewMethod.make("public int add(int a,int b){return a + b;}", cc);
//另一种方式 返回值 方法名 参数值 操作的类
CtMethod cm2 = new CtMethod(CtClass.intType,"sub",new CtClass[] {CtClass.intType,CtClass.intType},cc);
cm2.setModifiers(Modifier.PUBLIC);//设置修饰符
cm2.setBody("return $1 - $2;");//$1 $2是占位符,表示两个形参 $0表示this关键字
cc.addMethod(cm2);
//反射调用生成的对象
Class clazz = cc.toClass();
Object obj = clazz.newInstance();//调用无参构造器,生成新的对象
Method method = clazz.getDeclaredMethod("sub", int.class,int.class);
Object result = method.invoke(obj, 200,300);
System.out.println(result);
}
关于代码中的占位符:因为这段代码第二种方式创建新的方法的时候,只是定义了参数的类型,没有说明参数的名字,所以要使用占位符在方法主体中使用参数,占位符有以下几种:
修改已有的方法信息
public static void test03() throws Exception {
ClassPool cp = ClassPool.getDefault();
//获得指定类
CtClass cc = cp.get("testJavasisst.emp");
CtMethod cm =cc.getDeclaredMethod("sayHello",new CtClass[] {CtClass.intType});
cm.insertBefore("System.out.println(\"start\");");
cm.insertAfter("System.out.println(\"end\");");
cm.insertAt(20,"System.out.println(\"我在那\");");//20行添加这样一个代码,原本的代码向下平移
//反射调用生成的对象
Class clazz = cc.toClass();
Object obj = clazz.newInstance();// 调用无参构造器,生成新的对象
Method method = clazz.getDeclaredMethod("sayHello", int.class);
method.invoke(obj, 200);//执行方法
}
属性信息:
public static void test04() throws Exception {
ClassPool cp = ClassPool.getDefault();
//获得指定类
CtClass cc = cp.get("testJavasisst.emp");
// CtField cf1 = CtField.make("private int id;", cc);
//类型,名称,类
CtField cf1 = new CtField(CtClass.intType,"test",cc);
cf1.setModifiers(Modifier.PRIVATE);
cc.addField(cf1,"10");//默认值
//get&set
cc.addMethod(CtNewMethod.getter("getTest", cf1));
cc.addMethod(CtNewMethod.setter("setTest", cf1));
}
构造器和注解的获取跟上面的相同:
cc.getConstructors();
cc.getAnnotations();
局限性
尽管javassist简单易学,但是有他的局限性:
- JDK5.0新语法不支持(包括泛型,枚举),不支持注解修改,但可以通过底层的javassist类来解决,具体参考:javassist.bytecode.annotation
- 不支持数组的初始化,如String[]{“1”,“2”},除非数组的容量为1
- 不支持内部类和匿名类
- 不支持continue和break表达式
- 对于继承关系,有些不支持,例如
-
- class A{}
-
- class B extends A{}
-
- class C extends B{}