任何足够先进的科技,都与魔法无异

相信你对Java编译以后的class字节码无论是在文件格式以及元数据方面已经有了很多的认识和了解,接下来我们不停留在理论的基础上,动手去操作和控制class字节码,这个介于在Java编译器和Java虚拟机之间的产物。


class 文件已经不是由你的 Java 源码编译而来,而是由程序动态生成。 能够做这件事的,有JDK中的动态代理API, 还有一个叫做 cglib 的开源库。 这两个库都是偏重于动态代理的, 也就是以动态生成 class 的方式来支持代理的动态创建


有一个叫做 ASM 的库, 能够直接生成class文件,它的 api 对于动态代理的 API 来说更加原生, 每个api都和 class 文件格式中的特定部分相吻合, 也就是说, 如果对 class 文件的格式比较熟练, 使用这套 API 就会相对简单。 下面我们通过一个实例来讲解 ASM 的使用, 并且在使用的过程中, 会对应 class 文件中的各个部分来说明。除此之外, 还有一个叫做 ASM 的库, 能够直接生成class文件,它的 api 对于动态代理的 API 来说更加原生, 每个api都和 class 文件格式中的特定部分相吻合, 也就是说, 如果对 class 文件的格式比较熟练, 使用这套 API 就会相对简单。下面我们通过一个实例来讲解 ASM 的使用, 并且在使用的过程中, 会对应 class 文件中的各个部分来说明。


ASM 库的介绍和使用


ASM 库是一款基于 Java 字节码层面的代码分析和修改工具,那 ASM 和访问者模式有什么关系呢?访问者模式主要用于修改和操作一些数据结构比较稳定的数据,通过前面的学习,我们知道 .class 文件的结构是固定的,主要有常量池、字段表、方法表、属性表等内容,通过使用访问者模式在扫描 .class 文件中各个表的内容时,就可以修改这些内容了。


ASM 可以直接生产二进制的 .class 文件,也可以在类被加载入 JVM 之前动态修改类行为。下文将通过两个例子,分别介绍如何生成一个 class 文件和修改 Java 类中方法的字节码。


ASM 会比较困难,ASM 官方也提供了一个帮助工具 ASMifier,我们可以先写出目标代码,然后通过 javac 编译成 .class 文件,然后通过 ASMifier 分析此 .class 文件就可以得到需要插入的代码对应的 ASM 代码了。

ASM 生成 class 文件

package work;public class Example {public static void main(String[] var0) {System.out.println("createExampleClass");}
}

这个 Example 类很简单,只有简单的包名,加上一个静态 main 方法,打印输出 createExampleClass 。
下面开始介绍如何使用 ASM 动态生成上述源码对应的字节码。

public class Main extends ClassLoader {// 此处记得替换成自己的文件地址public static final String PATH = "/Users/xxx/IdeaProjects/untitled/src/work/";public static void main(String[] args) {createExampleClass();}private static void createExampleClass() {ClassWriter cw = new ClassWriter(0);// 定义一个叫做Example的类,并且这个类是在 work 目录下面cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "work/Example", null, "java/lang/Object", null);// 生成默认的构造方法MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);// 生成构造方法的字节码指令mv.visitVarInsn(ALOAD, 0);mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);mv.visitInsn(RETURN);mv.visitMaxs(1, 1);//构造函数访问结束mv.visitEnd();// 生成main方法中的字节码指令mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);mv.visitCode();// 获取该方法mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");// 加载字符串参数mv.visitLdcInsn("createExampleClass");// 调用该方法mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);mv.visitInsn(RETURN);mv.visitMaxs(2, 1);mv.visitEnd();// 获取生成的class文件对应的二进制流byte[] code = cw.toByteArray();// 将二进制流写到本地磁盘上FileOutputStream fos = null;try {fos = new FileOutputStream(PATH + "Example.class");fos.write(code);System.out.println(fos.getFD());fos.close();} catch (Exception e) {System.out.print(" FileOutputStream error " + e.getMessage());e.printStackTrace();}loadclass("Example.class", "work.Example");}private static void loadclass(String className, String packageNamePath) {//通过反射调用main方法MyClassLoader myClassLoader = new MyClassLoader(PATH + className);// 类的全称,对应包名try {// 加载class文件Class<?> Log = myClassLoader.loadClass(packageNamePath);System.out.println("类加载器是:" + Log.getClassLoader());// 利用反射获取main方法Method method = Log.getDeclaredMethod("main", String[].class);String[] arg = {"ad"};method.invoke(null, (Object) arg);} catch (Exception e) {e.printStackTrace();}}
}

下面是自定义的一个 class 加载类:

public class MyClassLoader extends ClassLoader {// 指定路径private String path;public MyClassLoader(String classPath) {path = classPath;}/*** 重写findClass方法** @param name 是我们这个类的全路径* @return* @throws ClassNotFoundException*/@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {Class log = null;// 获取该class文件字节码数组byte[] classData = getData();if (classData != null) {// 将class的字节码数组转换成Class类的实例log = defineClass(name, classData, 0, classData.length);}return log;}/*** 将class文件转化为字节码数组** @return*/private byte[] getData() {File file = new File(path);if (file.exists()) {FileInputStream in = null;ByteArrayOutputStream out = null;try {in = new FileInputStream(file);out = new ByteArrayOutputStream();byte[] buffer = new byte[1024];int size = 0;while ((size = in.read(buffer)) != -1) {out.write(buffer, 0, size);}} catch (IOException e) {e.printStackTrace();} finally {try {in.close();} catch (IOException e) {e.printStackTrace();}}return out.toByteArray();} else {return null;}}
}

下面详细介绍生成class的过程:

首先定义一个类,相关代码片段如下:

 ClassWriter cw = new ClassWriter(0);

// 定义一个叫做Example的类,并且这个类是在 work 目录下面

cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "work/Example", null, "java/lang/Object", null);

ClassWriter 类是 ASM 中的核心 API , 用于生成一个类的字节码。 ClassWriter 的 visit 方法定义一个类。


  • 第一个参数 V1_8 是生成的 class 的版本号, 对应class文件中的主版本号和次版本号, 即 minor_version 和 major_version 。
  • 第二个参数ACC_PUBLIC表示该类的访问标识。这是一个public的类。 对应class文件中的access_flags 。
  • 第三个参数是生成的类的类名。 需要注意,这里是类的全限定名。 如果生成的class带有包名, 如com.jg.xxx.Example, 那么这里传入的参数必须是com/jg/xxx/Example 。对应 class 文件中的 this_class 。
  • 第四个参数是和泛型相关的, 这里我们不关新, 传入null表示这不是一个泛型类。这个参数对应class文件中的Signature属性(attribute)
  • 第五个参数是当前类的父类的全限定名。 该类直接继承Object。 这个参数对应class文件中的super_class 。
  • 第六个参数是 String[] 类型的, 传入当前要生成的类的直接实现的接口。 这里这个类没实现任何接口, 所以传入null 。 这个参数对应class文件中的interfaces 。

定义默认构造方法, 并生成默认构造方法的字节码指令
相关代码片段如下:

       // 生成默认的构造方法MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);// 生成构造方法的字节码指令mv.visitVarInsn(ALOAD, 0);mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);mv.visitInsn(RETURN);mv.visitMaxs(1, 1);// 构造函数访问结束mv.visitEnd();

使用上面创建的 ClassWriter 对象, 调用该对象的 visitMethod 方法, 得到一个 MethodVisitor 对象, 这个对象定义一个方法。 对应 class 文件中的一个 method_info 。


  • 第一个参数是 ACC_PUBLIC , 指定要生成的方法的访问标志。 这个参数对应 method_info 中的 access_flags 。
  • 第二个参数是方法的方法名。 对于构造方法来说, 方法名为 。 这个参数对应 method_info 中的 name_index , name_index 引用常量池中的方法名字符串。
  • 第三个参数是方法描述符, 在这里要生成的构造方法无参数, 无返回值, 所以方法描述符为 ()V 。 这个参数对应 method_info 中的descriptor_index 。
  • 第四个参数是和泛型相关的,这里传入null表示该方法不是泛型方法。这个参数对应 method_info 中的 Signature 属性。
  • 第五个参数指定方法声明可能抛出的异常。 这里无异常声明抛出, 传入 null 。 这个参数对应 method_info 中的 Exceptions 属性。

  • 接下来调用 MethodVisitor 中的多个方法, 生成当前构造方法的字节码。 对应 method_info 中的 Code 属性。
  • 调用 visitVarInsn 方法,生成 aload 指令, 将第 0 个本地变量(也就是 this)压入操作数栈。
  • 调用 visitMethodInsn方法, 生成 invokespecial 指令, 调用父类(也就是 Object)的构造方法。
  • 调用 visitInsn 方法,生成 return 指令, 方法返回。
  • 调用 visitMaxs 方法, 指定当前要生成的方法的最大局部变量和最大操作数栈。 对应 Code 属性中的 max_stack 和 max_locals
  • 最后调用 visitEnd 方法, 表示当前要生成的构造方法已经创建完成。

生成class数据, 保存到磁盘中, 加载class数据

生成 ASM 代码

javac Example.java  // 生成 Example class 文件
java -classpath asm-all-6.0_ALPHA.jar org.objectweb.asm.util.ASMifier Example.class  // 利用 ASMifier 将class 文件转为 asm 代码
import java.util.*;
import org.objectweb.asm.*;
public class ExampleDump implements Opcodes {public static byte[] dump () throws Exception {ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "Example", null, "java/lang/Object", null);{mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("createExampleClass");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
}
cw.visitEnd();return cw.toByteArray();
}
}

利用 ASM 修改方法

还是在原来的代码基础上,Main 类下面新增一个方法 modifyMethod 方法,具体代码如下:

private static void modifyMethod() {byte[] code = null;try {// 需要注意把 . 变成 /, 比如 com.example.a.class 变成 com/example/a.classInputStream inputStream = new FileInputStream(PATH + "Example.class");ClassReader reader = new ClassReader(inputStream);// 1. 创建 ClassReader 读入 .class 文件到内存中ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);// 2. 创建 ClassWriter 对象,将操作之后的字节码的字节数组回写ClassVisitor change = new ChangeVisitor(writer);// 3. 创建自定义的 ClassVisitor 对象reader.accept(change, ClassReader.EXPAND_FRAMES);code = writer.toByteArray();System.out.println(code);FileOutputStream fos = new FileOutputStream(PATH + "Example.class");fos.write(code);fos.close();} catch (Exception e) {System.out.println("FileInputStream " + e.getMessage());e.printStackTrace();}try {if (code != null) {System.out.println(code);FileOutputStream fos = new FileOutputStream(PATH + "Example.class");fos.write(code);fos.close();}} catch (Exception e) {System.out.println("FileOutputStream ");e.printStackTrace();}loadclass("Example.class", "work.Example");}

新建一个 adapter,继承自 AdviceAdapter,AdviceAdapter 本质也是一个 MethodVisitor,但是里面对很多对方法的操作逻辑进行了封装,使得我们不用关心 ASM 内部的访问逻辑,只需要在对应的方法下面添加代码逻辑即可。

public class ChangeAdapter extends AdviceAdapter {private String methodName = null;ChangeAdapter(int api, MethodVisitor mv, int access, String name, String desc) {super(api, mv, access, name, desc);methodName = name;}@Overrideprotected void onMethodEnter() {super.onMethodEnter();Label l0 = new Label();Label l1 = new Label();Label l2 = new Label();mv.visitTryCatchBlock(l0, l1, l2, "java/lang/InterruptedException");mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);// 把当前的时间戳存起来mv.visitVarInsn(LSTORE, 1);mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitLdcInsn("ChangeAdapter onMethodEnter ");mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);mv.visitLabel(l0);mv.visitLdcInsn(new Long(100L));mv.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V", false);mv.visitLabel(l1);Label l3 = new Label();mv.visitJumpInsn(GOTO, l3);mv.visitLabel(l2);mv.visitFrame(Opcodes.F_FULL, 2, new Object[] {"[Ljava/lang/String;", Opcodes.LONG}, 1, new Object[] {"java/lang/InterruptedException"});mv.visitVarInsn(ASTORE, 3);mv.visitVarInsn(ALOAD, 3);mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/InterruptedException", "printStackTrace", "()V", false);mv.visitLabel(l3);mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);// 把当前的时间戳存起来mv.visitVarInsn(LSTORE, 3);}@Overrideprotected void onMethodExit(int opcode) {super.onMethodExit(opcode);mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");// 把之前存储的时间戳取出来mv.visitVarInsn(LLOAD, 3);mv.visitVarInsn(LLOAD, 1);mv.visitInsn(LSUB);mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);}@Overridepublic void visitMaxs(int i, int i1) {super.visitMaxs(i, i1);}

在 adapter 中,有两个非常重要的方法:

  • onMethodEnter:表示正在进入一个方法,在执行方法里的内容前会调用。因此,此处是对一个方法添加相关处理逻辑的很好的办法。
  • onMethodExit:表示正在退出一个方法,在执行 return 之前。如果一个方法存在返回值,只能再该方法添加静态方法。

上面的代码是为了计算某个方法的耗时,我们先是在方法开始前记录了当前的时间戳,同时为了避免程序执行过快,还让该线程睡了100ms。在方法结束前,将之前的时间戳取出来,同时获取当前的时间戳,两者相减,就是方法运行耗时。

public class ChangeVisitor extends ClassVisitor {ChangeVisitor(ClassVisitor classVisitor) {super(Opcodes.ASM5, classVisitor);}@Overridepublic MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);System.out.print(name);if (name.equals("main")) {return new ChangeAdapter(Opcodes.ASM4, methodVisitor, access, name, desc);}return methodVisitor;}
}

ChangeVisitor 主要就是对 ASM 访问 class 文件方法的时候,做个拦截。如果发现方法名是 main,就让其走前面写好的 ChangeAdapter,这样,我们就可以改写 class 文件的方法了。

如果方法带有返回值

前面修改的 main 是没有返回值的,那么如果存在返回值?这么写还合适吗?

如果你添加了非静态方法的调用,去看生成的 class 文件也许可能是对的,但是在调用的时候就会报错。示例如下:

protected void onMethodExit(int opcode) {mv.visitVarInsn(LLOAD, longT);mv.visitInsn(LSUB);mv.visitVarInsn(LSTORE, longT);mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitLdcInsn("work2 createExampleClass");mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);mv.visitVarInsn(LLOAD, longT);}

从class 文件来看,生成的 class 文件是没有问题的,结果在反射调用的时候报了异常:

【JVM技术专题】针对于ASM库生成和修改class文件开发指南 「 入门篇」相关推荐

  1. 【JVM技术专题】较为深入分析线程池基本原理及实现机制「 入门篇」

    基本原理 线程池基本常识 线程池(Thread Pool)是一种基于池化思想管理线程的工具.线程频繁的创建.销毁会产生大量的系统内核调用,消耗CPU资源.用线程池来维护多个线程的生命周期,一方面可以避 ...

  2. 计算机毕业设计ssm民族地区文化调研与数字化保护技术研究---青海平弦乐库的建设及播放平台开发l3479系统+

    计算机毕业设计ssm民族地区文化调研与数字化保护技术研究---青海平弦乐库的建设及播放平台开发l3479系统+ 计算机毕业设计ssm民族地区文化调研与数字化保护技术研究---青海平弦乐库的建设及播放平 ...

  3. 计算机毕业设计ssm民族地区文化调研与数字化保护技术研究---青海平弦乐库的建设及播放平台开发l3479系统+程

    计算机毕业设计ssm民族地区文化调研与数字化保护技术研究---青海平弦乐库的建设及播放平台开发l3479系统+程 计算机毕业设计ssm民族地区文化调研与数字化保护技术研究---青海平弦乐库的建设及播放 ...

  4. ssm毕设项目民族地区文化调研与数字化保护技术研究---青海平弦乐库的建设及播放平台开发l3479(java+VUE+Mybatis+Maven+Mysql+sprnig)

    ssm毕设项目民族地区文化调研与数字化保护技术研究---青海平弦乐库的建设及播放平台开发l3479(java+VUE+Mybatis+Maven+Mysql+sprnig) 项目运行 环境配置: Jd ...

  5. Flutter开发指南之理论篇:Dart语法04(库,异步,正则表达式)

    总目录 Flutter开发指南之理论篇:Dart语法01(数据类型,变量,函数) Flutter开发指南之理论篇:Dart语法02(运算符,循环,异常) Flutter开发指南之理论篇:Dart语法0 ...

  6. 【JVM技术专题】 深入学习JIT编译器实现机制「 原理篇」

    前提概要 解释器 Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为"热点代码"(hots ...

  7. 精华推荐 |【JVM技术专题】深入学习JIT编译器实现机制「核心剖析篇」

    前提概要 我们都知道开发语言整体分为两类,一类是编译型语言,一类是解释型语言.那么你知道二者有何区别吗?编译器和解释器又有什么区别? 这是为了兼顾启动效率和运行效率两个方面.Java程序最初是通过解释 ...

  8. 【JVM技术专题】深入分析CG管理和原理查缺补漏「番外篇」

    前提概要 本文主要针对 Hotspot VM 中"CMS + ParNew" 组合的一些使用场景进行总结. 自 Sun 发布 Java 语言以来,开始使用GC技术来进行内存自动管理 ...

  9. 【JVM技术专题】「源码专题」深入剖析JVM的Mutex锁的运行原理及源码实现(底层原理-防面试)

    并行编程之条件变量(posix condition variables) 在整理Java LockSupport.park()的东东,看到了个"Spurious wakeup",重 ...

最新文章

  1. 最近做了一个小小的系统,收获挺大的....我想总结一下
  2. shell脚本中的case语句使用要点
  3. 浅谈lastIndexOf
  4. C++学习笔记:(七)C语言实现面向对象编程
  5. 创建一个追踪摄像机(2)
  6. 使用IntelliJ书签
  7. 列表元素的几种统计方法总结(嵌套列表)
  8. 每天打卡心情好(洛谷P1664题题解,Java语言描述)
  9. linux echo 变量 字符串,echo命令 – 输出字符串或提取Shell变量的值 – 运维那些事...
  10. (193)FPGA上电后IO的默认状态(Vivado软件默认为z)
  11. python中文转拼音不用第三方库_又一个奇葩要求,Python是如何将“中文”转“拼音”的?...
  12. 华为 5G 折叠屏手机获入网许可;苹果召回部分 MacBook Pro;Oracle Linux 8.0 发布 | 极客头条...
  13. Android官方开发文档Training系列课程中文版:线程执行操作之定义线程执行代码...
  14. 【十五分钟Talkshow】如何理解并优化.NET应用程序对内存的使用
  15. Java基础语法总结(全)
  16. 用python写生日快乐说说_生日快乐的说说(精选50句)
  17. 移动通信网络规划:多址技术
  18. 103000大写加零吗_关于支票金额大写规范写零的问题,比如1008712元,100万后要不要加零...
  19. php匹配地址中的省市区,php 正则匹配省市区
  20. 数据库系列7:事务与锁的实现原理

热门文章

  1. 【python】输出30以内的质数并输出、统计个数、以及从大到小排列。
  2. 视网膜屏的contentscalefactor设置
  3. 微信小程序设置cookie
  4. tzc 2922 棋盘问题
  5. Python异常「1」(异常的概念、异常捕获、异常的传递、自定义异常)
  6. assasin谈设计模式
  7. oracle coherence介绍及使用
  8. 如何注册谷歌账号,遇到“此电话号码无法用于进行验证”怎么办
  9. Python学习之 a == b 和 a is b 的区别
  10. owl文件导入Neo4j