第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中有一个无参构造和带参构造,将所有的构造器获取并打印,所得到结果如下所示: