第16次文章:Java字节码
- 2019 年 10 月 8 日
- 筆記
在上一期讲解java的动态性的时候,我们主要提到了java中的反射机制,可以在java代码运行的时候,改变类的结构,属性等信息,而这一节我们通过另一种实现方式来讲解java的动态性,主要就是java的字节码操作。
一、了解一下字节码:
1、背景
在我们日常编程时,我们在IDE中编写好源代码之后,点击“run”,程序直接就运行了。但是点击“run”按钮之后,计算机是如何操作的呢?其实,计算机并不是直接使用我们程序员编写好的源代码进行执行,而是在我们点击“run”按钮之后,计算机首先是对源代码(.java)文件进行编译操作,将我们写好的源代码.java文件编译成为字节码.class文件,然后把.class文件传送给JVM进行运行。所以说,我们的java虚拟机执行的是字节码文件。并且,不论该字节码文件来自于哪里,也不论字节码文件使用的是哪一种编辑器,只要其符合java虚拟机的要求,都可以被执行。
2、简介
(1)编译器将java源码编译成符合java虚拟机规范的字节码文件
(2)字节码内部不包含任何分隔符区分段落
(3)一组8位字节单位的字节流组成了一个完整的字节码文件
3、操作字节码的几个功能
在前面的反射中,我们已经提到了,反射的可以动态的生成新的类,并且可以改变某个类的结构。而操作字节码实现的几个功能,也主要是这两条。但是两者是有区别的,操作字节码相比于反射,其开销比反射小,性能比反射高。两者在大多数是需要相辅相成同时存在的。
4、常见的java字节码操作类库
(1)BCEL (Byte Code Engineering Library):属于java classworking广泛使用的一种框架,它可以让您深入JVM汇编语言进行类操作的细节,主要在实际的JVM指令层次上进行操作(BCEL拥有丰富的JVM指令级支持)。
(2)ASM:是一个轻量级java字节码操作框架,直接涉及到JVM底层的操作和指令。
(3)CGLIB(code generation library):是一个强大的,高性能,高质量的code生成类库,基于ASM实现。
(4)javassist:是一个开源的分析、编辑和创建java字节码的类库。性能较ASM差,跟CGLIB差不多,但是使用简单。很多开源框架都在使用它。主要在源代码级别上进行工作。javassist性能高于反射,低于ASM。
二、javassist类库
1、简介
javassist主要是基于源代码级别的类库,所以其API与JAVA的反射机制中包含的API十分相似。javassist的最外层主要是由CtClass,CtMethod,以及CtField几个类组成,用来执行类似于反射API中的java.lang.Class,java.lang.reflect.Method,java.lang.reflect.Method.Field相同的操作。
2、测试javassist生成一个新的类
源代码如下:
package com.peng.test; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import javassist.CtField; import javassist.CtMethod; /** * 测试使用javassist生成一个新的类 */ public class Demo01 { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("com.peng.bean.Emp"); //创建属性 CtField f1 = CtField.make("private int empno;", cc); CtField f2 = CtField.make("private String ename;", cc); cc.addField(f1); cc.addField(f2); //创建方法 CtMethod m1 = CtMethod.make("public void setEmpno(int empno){this.empno = empno;}", cc); CtMethod m2 = CtMethod.make("public int getEmpno(){return this.empno;}", cc); cc.addMethod(m1); cc.addMethod(m2); //创建构造器 CtConstructor constructor = new CtConstructor(new CtClass[] {CtClass.intType,pool.get("java.lang.String")}, cc); constructor.setBody("{this.empno = empno;this.ename = ename;}"); cc.addConstructor(constructor); cc.writeFile("G:/java学习/test");//将上面构造好的类写入到G:/java学习/test System.out.println("生成类,成功!"); } }
tips:
(1)由上面的代码也可以看出使用javassist操作字节码的方式:首先获取一个类池“ClassPool”,通过类池,我们创建编译过程中的新类“CtClass”,创建的过程中,需要对这个新的编译类进行命名,Ct的含义就是“compile time”。然后我们在对象"cc"中创建新的属性值,并将属性值加入到新的对象“cc”中。最后创建构造器的方法也是一样的,只不过在创建构造器的时候,需要将构造器的声明和构造器的内部结构分开编写。最后,我们将写好的构造器加入到新对象“cc”中。并且将建立的.Class文件写出到指定的目录中。
结果如下所示:

(2)如果我们直接去查看这个字节码文件,那么我们打开得到的将会是一堆乱码,无法查阅。这里我们使用反编译软件“XJad”进行查看:

这就是我们编写好的字节码文件反编译后的源代码。
3、测试javassist的相关API
使用5个test从不同的方面进行测试API功能:
/** * 测试javassist的API */ public class Demo02 { /** * 测试处理类的基本用法 * @throws Exception * @throws IOException */ public static void test01() throws IOException, Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("com.peng.test.Emp"); byte[] bytes = cc.toBytecode(); System.out.println(Arrays.toString(bytes)); System.out.println(cc.getName());//获取全部名称 System.out.println(cc.getPackageName());//获取包名 System.out.println(cc.getSimpleName());//仅获取名称 System.out.println(cc.getSuperclass());//获取父类 System.out.println(cc.getInterfaces());//获取接口 } /** * 测试产生新的方法 * @throws Exception */ public static void test02() throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("com.peng.test.Emp"); //方法一: // CtMethod m = CtNewMethod.make("public int add(int a,int b){return a+b;}",cc); //方法二: CtMethod m = new CtMethod(CtClass.intType, "add", new CtClass[] {CtClass.intType,CtClass.intType}, cc); m.setModifiers(Modifier.PUBLIC); m.setBody("{return $1+$2;}"); cc.addMethod(m); //通过反射调用新生成的方法 Class clazz = cc.toClass(); Object obj = clazz.newInstance();//通过调用Emp无参构造器,创建新的Emp对象 Method method = clazz.getDeclaredMethod("add", int.class ,int.class); Object result = method.invoke(obj, 200,300); System.out.println(result); } /** * 修改已有方法的信息,修改方法的内容 * @throws Exception */ public static void test03() throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("com.peng.test.Emp"); CtMethod cm = cc.getDeclaredMethod("SayHello",new CtClass[] {CtClass.intType}); //动态修改方法体 cm.insertBefore("System.out.println($1);System.out.println("start!!!!!");"); cm.insertAfter("System.out.println("end!!!");"); //通过反射调用新的方法 Class clazz = cc.toClass(); Object obj = clazz.newInstance();//通过调用无参构造器创建新的对象 Method method = clazz.getDeclaredMethod("SayHello", int.class); method.invoke(obj, 300); } /** * 属性的操作 * @throws Exception */ public static void test04() throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("com.peng.test.Emp"); //增加属性 //方法一: // CtField f1 = CtField.make("private int salary;",cc); //方法二: CtField f1 = new CtField(CtClass.intType, "salary",cc); f1.setModifiers(Modifier.PRIVATE); cc.addField(f1); //获取属性值 System.out.println(cc.getField("salary"));//获取指定的属性值 //增减相应的set和get方法 cc.addMethod(CtNewMethod.getter("getSalary", f1));//增加get方法 cc.addMethod(CtNewMethod.setter("setSalary", f1));//增加set方法 } /** * 对构造器的操作 * @throws Exception */ public static void test05() throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("com.peng.test.Emp"); CtConstructor[] cs = cc.getConstructors(); for(CtConstructor c:cs) { System.out.println(c.getLongName()); } } }
tips:
(1)关于test01,主要测试一些基本的API用法,获取名称等等:运行得到的结果如下图所示:

(2)关于test02,主要是利用javassist产生一个加法方法,然后加入到新产生的对象cc中。在上述代码中,我们关于产生新的方法,给出了两种产生形式。这里需要提一个点,方法一源码我们编写时,直接给出了形参“int a ;int b”,所以在我们使用返回值return的时候,可以直接利用形参相加。但是在方法二中,我们仅仅指定了两个形参的类型,而并没有给定形参名称,所以在使用返回值的时候,我们使用的是“return $1+$2”,其中“$1”和“$2”分别代表第一个和第二个形参。将写好的方法加法加入到对象cc中之后,我们使用反射的方法,可以调用产生的新方法。测试结果如下所示:

(3)关于test03,主要是对已经存在的类进行一些修改操作,所以我们先自己定义一个Javabean——Emp类。在Emp类中,我们为了便于测试,新增了一个SayHello方法,源码如下所示:
public void SayHello(int a) { System.out.println("Say Hello"+a); }
对象cc直接获取该类的字节码。我们为了修改该类的信息,首先获取cc的“SayHello”方法,然后我们分别使用方法“insertBefore”和方法“insertAfter”,在这个方法的前面分别插入相应的代码。结果如下图所示:

(4)关于test04,是对类的属性值的操作。我们同样是利用上面已经写好的类,对其增加一个salary属性值。我们也给出了两种产生方法。由于在产生过程中,我们没有对属性“salary”赋予初始值,所以最后的输出结果如下图所示:

(5)关于test05,是对构造器的操作。我们的Emp中有一个无参构造和带参构造,将所有的构造器获取并打印,所得到结果如下所示:
