专栏首页Java小白成长之路第16次文章:Java字节码

第16次文章:Java字节码

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


本文分享自微信公众号 - Java小白成长之路(Java_xiaobai),作者:鹏程万里

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-04-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 第29次文章:事务机制

    -连接到数据库上,并执行一条DML语句(insert、update或delete)。

    鹏-程-万-里
  • 第32次文章:手写SORM框架(二)

    在上周,我们将整个SORM的框架结构梳理了一下,本周开始对整个框架的每个细节步骤进行相关的填充。目前还没有把整个框架全部搭建起来,只完成了一小部分,这周我们就对...

    鹏-程-万-里
  • 当我们没有加减乘除之后

    当我们看到无法使用加法和减法的时候,我们的第一印象应该就是想着转化思维,去思考计算机的底层到底是什么运算呢?

    鹏-程-万-里
  • OpenSSL与yaSSL性能对比

    摘要 MySQL可以使用OpenSSL或yaSSL/wolfSSL进行编译,这两者都支持基于OpenSSL API的加密连接。在5.7版本,我们知道默认情况下...

    腾讯数据库技术
  • Cocos Creator中使用动作系统(官方文档摘录)

    Cocos Creator 提供的动作系统源自 Cocos2d-x,API 和使用方法均一脉相承。动作系统可以在一定时间内对节点完成位移,缩放,旋转等各种动作。

    bering
  • C++对于大型图片的加载缩放尝试

    Qt对于图片的操作主要集中在这几个类 QImage ,QImageReader ,QPixmap 其中QImage这个类对图片的缩放有几个很不错的技巧,不过对于...

    Gxjun
  • 黑客3次攻破微软员工邮箱

    微软作为全球最大的软件公司,在Twitter官方账号被攻破后,“叙利亚电子军”日前攻破了微软的员工邮箱系统,公开了多封盗取的邮件。 微软证实了这...

    安恒信息
  • 关于MFC与OpenGL结合绘图区域用鼠标来控制图形的移动总结

    原文链接:https://www.cnblogs.com/DOMLX/p/11773171.html

    徐飞机
  • MySQL:Innodb Handler_read_*变量解释

    实际上这些变量都是MySQL层定义出来的,因为MySQL可以包含多个存储引擎。因此这些值如何增加需要在引擎层的接口中自行实现,也就是说各个引擎都有自己的实现,在...

    用户1278550
  • 为什么选择Mapabc

    目前网络上有众多的在线电子地图服务,诸如Mapabc、Google Maps、Yahoo Maps、Mapbar、Microsoft Virtual Earth...

    大江小浪

扫码关注云+社区

领取腾讯云代金券