前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >第16次文章:Java字节码

第16次文章:Java字节码

作者头像
鹏-程-万-里
发布2019-09-27 12:12:21
5670
发布2019-09-27 12:12:21
举报

在上一期讲解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生成一个新的类

源代码如下:

代码语言:javascript
复制
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功能:

代码语言:javascript
复制
/**
 * 测试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方法,源码如下所示:

代码语言:javascript
复制
  public void SayHello(int a) {
    System.out.println("Say Hello"+a);
  }

对象cc直接获取该类的字节码。我们为了修改该类的信息,首先获取cc的“SayHello”方法,然后我们分别使用方法“insertBefore”和方法“insertAfter”,在这个方法的前面分别插入相应的代码。结果如下图所示:

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

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


本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-04-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java小白成长之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档