前言

对于Java字节码,它是在Java类的编译过程产生的,即由.java源文件到.class二进制字节码文件的过程。而Java类的加载又是通过类的名字获取二进制字节流,然后在内存中将字节流生成类对象。所以动态修改类的时机在于修改.class文件,只要通过修改.class文件的字节码,即可达到修改类的目的。修改字节码可以通过ASM这个开源框架实现,ASM是一个Java字节码引擎的库,具体可以查看官网,它可以通过操作字节码实现修改类或者生成类。

介绍

Java字节码的执行操作主要是在虚拟机的栈执行,这个栈主要有局部变量表,操作数栈等几个部分。

(一)局部变量表

主要用来保存方法中的局部变量,基本的存储单位为slot(32位的存储空间),所以long double的数据类型需要两个slot, 当方法被调用时,参数会传递从0开始的局部变量表的索引位置上,所以局部变量最大的大小是在编译期就决定的,特别需要注意的是如果调用的是实例方法,局部变量第0个位置是实例对象的引用

(二)操作数栈

主要用来当作字节码指令操作的出栈入栈的容器,例如变量的出栈入栈都是在操作数栈里面进行的。

(三)指令

指令主要是由操作码+操作数组成的,指令包括加载和存储指令,运算指令和类型转换指令,方法调用指令等等。指令所需要的操作,调用方法,赋值等,都是在操作数栈进行的。

过程

首先是导包,包的版本关系可以查看发布版本,这里我导入的是implementation "org.ow2.asm:asm:6.2"。修改字节码主要需要以下这几个类:ClassReader, ClassWriter, ClassVisitor, MethodVisitor。各个类的作用如下:

  1. ClassReader: 读取类文件
  2. ClassWriter: 继承ClassVisitor 主要用来生成修改类之后的字节
  3. ClassVisitor: 用于访问修改类
  4. MethodVisitor: 用于访问修改类的方法

一般用法如下:

    try {String classPath = "asmdemo/ModifyInstanceClass";ClassReader classReader = new ClassReader(classPath);ClassWriter classWriter = new ClassWriter(classReader, 0);ClassVisitor classVisitor = new ClassVisitorDemo(classWriter);classReader.accept(classVisitor, 0);File file = new File(ROOT_SUFFIX + "ClassDynamicLoader/ASMProject/build/classes/java/main/asmdemo/ModifyInstanceClass.class");FileOutputStream output = new FileOutputStream(file);output.write(classWriter.toByteArray());output.close();} catch (IOException e) {e.printStackTrace();}private static class ClassVisitorDemo extends ClassVisitor {ClassVisitorDemo(ClassVisitor classVisitor) {super(Opcodes.ASM5, classVisitor);}@Overridepublic void visitEnd() {cv.visitField(Opcodes.ACC_PRIVATE, "timer", Type.getDescriptor(long.class), null, null);MethodVisitor methodVisitor = cv.visitMethod(Opcodes.ACC_PUBLIC, "newFunc","()V", null,null);methodVisitor.visitInsn(Opcodes.RETURN);methodVisitor.visitMaxs(0,1);super.visitEnd();}@Overridepublic MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);if (name.equals("print") && desc.equals("()V")) {methodVisitor = new MethodVisitorHub.FirstMethodVisitor(methodVisitor);} else if(name.equals("print") && desc.equals("(Ljava/lang/String;)V")) {methodVisitor = new MethodVisitorHub.SecondMethodVisitor(methodVisitor);} else if (name.equals("connectStr")) {methodVisitor = new MethodVisitorHub.ThirdMethodVisitor(methodVisitor);}return methodVisitor;}}

先利用ClassReader读取待修改的类文件,然后基于Reader创建了对应的ClassWriter,再基于ClassWriter创建了对应的ClassVisitor, 再接着ClassReader委托ClassVisitor去读取修改类,最后,创建文件输出流,利用ClassWriter生成的字节,将重新生成的字节码写回build目录生成的class文件,替换编译生成的class文件,这样就可以达到修改类的目的。

Created with Raphaël 2.1.2开始Reader读取类MethodVisitor修改类获取Writer修改之后的字节码文件输出流将字节码写回类生成的build路径,替换

用法

   对于类的修改,主要关注ClassVisitor和MethodVisitor这两个类即可,ClassVistor可以实现成员变量和方法的增加,MethodVisitor用于修改类方法的实现。在修改类方法的时候,我是先通过把原先的方法修改为预期的方法,然后通过javap命令对预期的方法产生的类文件进行反编译,查看编译器产生的字节码。命令如下:javap -v .class文件路径。 通过反编译之后可以得到修改后的类的操作数栈和局部变量表的最大大小,还有具体的字节码指令。下面开始看具体的使用。

   MethodVIsitor一般通过实现visitCode visitInsan visitMaxs方法来实现类的修改。visitCode是方法的访问开始;visitInsn可以访问方法的操作指令,一般应用于在return指令之前插入代码;vistiMax则用于复写操作数栈和局部变量表的大小,因为类被修改,所以所需的栈和变量表大小可能会增加。下面是几个具体的例子:

1. 在print()空方法中插入一行输出 System.out.print("Hello World");

首先利用javap -v 编译修改前的print方法,如下
![这里写图片描述](https://img-blog.csdn.net/20180906151907501?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3Rhb3N6dQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
接着在print()方法增加 `    System.out.print("Hello World");`再执行javap -c反编译


可以发现多了三个指令,并且stack即操作数栈增加了2。所以代码如下:

  public static class FirstMethodVisitor extends MethodVisitor {public FirstMethodVisitor(MethodVisitor mv) {super(Opcodes.ASM5, mv);}/*** 进入方法 插入System.out.print("hello world")这行代码*/@Overridepublic void visitCode() {super.visitCode();mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitLdcInsn("hello world");mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "print", "(Ljava/lang/String;)V", false);}@Overridepublic void visitMaxs(int maxStack, int maxLocals) {mv.visitMaxs(2,1);}}

  上面的代码主要覆写了visitMaxs,stack local数值是通过反编译得到的,visitCode则是添加了三个指令。分析System.out.print可知,其实是通过System这个类获取out这个变量,然后通过out调用print这个方法输出“hello world”这个变量。
  所以首先要获取out,out是一个静态变量,第一个指令是visitFieldInsn,顾名思义就是访问成员的指令,第一个参数是操作码,第二个参数是调用成员的类,第三个参数是成员的名称,第四个参数是成员的类型,对号入座第一个指令,操作码是获取静态变量,调用类是“java/lang/System”, 成员名是“out”, 类型通过反编译可知是“Ljava/io/PrintStream;”。所以得出结论,第一个指令是通过System这个类获取out这个静态变量并且把变量入栈。
  接着第二个指令visitLdcInsn是把常量推到操作数栈,这里是把“hello world”入栈,
  最后就是第三个指令visitMethodInsn,还是顾名思义是访问方法的指令,第一个参数是操作码,第二个参数是调用方法等的类,第三个参数是方法名,第四个参数是方法的返回类型和参数类型,第五个参数是调用方法的类是否是接口,对号入座,Opcodes.INVOKEVIRTUAL指的是调用的是实例方法,调用的类是out即“java/io/PrintStream”这个类,方法名是print,返回值是void对应“V”,参数是String对应“Ljava/lang/String; ”, 这些参数的对应类型都可以从反编译得到。第三个指令需要两个操作数,一个是执行方法的主体即out,第二个是参数即“hello world”,使用visitMethodInsn指令的时候,out “”hello world“依次从操作数栈出栈,刚刚好对应指令调用的参数顺序。
拦截方法的入口在ClassVisitor,如下:

      private static class ClassVisitorDemo extends ClassVisitor {ClassVisitorDemo(ClassVisitor classVisitor) {super(Opcodes.ASM5, classVisitor);}@Overridepublic void visitEnd() {cv.visitField(Opcodes.ACC_PRIVATE, "timer", Type.getDescriptor(long.class), null, null);MethodVisitor methodVisitor = cv.visitMethod(Opcodes.ACC_PUBLIC, "newFunc","()V", null,null);methodVisitor.visitInsn(Opcodes.RETURN);methodVisitor.visitMaxs(0,1);super.visitEnd();}@Overridepublic MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);if (name.equals("print") && desc.equals("()V")) {methodVisitor = new MethodVisitorHub.FirstMethodVisitor(methodVisitor);} else if(name.equals("print") && desc.equals("(Ljava/lang/String;)V")) {methodVisitor = new MethodVisitorHub.SecondMethodVisitor(methodVisitor);} else if (name.equals("connectStr")) {methodVisitor = new MethodVisitorHub.ThirdMethodVisitor(methodVisitor);}return methodVisitor;}}

在visitMethod中判断方法名为print,则进行拦截注入自己创建的MethodVisitor即可。
  到这里已经分析完成,可以自信满满地运行代码了,但是要切记,不能在修改之前使用该类,如果使用了之后,类已经被加载,那么修改之后的类不会被再次加载,也就无法发挥作用了。

2. 在print(String s )空方法中插入一行输出 System.out.print(s)
   分析的方法和上面的一样,这里的关键是读取参数的值,反编译之后可以发现使用了ALOAD这个指令,这个指令的作用是从局部变量表读取变量入栈,指令代码如下:

 mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitVarInsn(Opcodes.ALOAD, 1);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "print", "(Ljava/lang/String;)V", false);

   visitVarInsn是读取参数的指令,操作码是ALOAD,后面的参数是指变量表的索引,上面也提到,如果是实例方法,局部变量表的0索引是实例对象,所以这里取了索引1。

3. 在connectStr()空方法打印执行消耗时间
修改前代码如下:

  public void connectStr() {String s = "";for (int i = 0; i < 10000; i ++) {s += i;}}

修改后代码如下:

  public void connectStr() {this.timer = -System.currentTimeMillis();String s = "";for(int i = 0; i < 10000; ++i) {s = s + i;}this.timer += System.currentTimeMillis();System.out.println(this.timer);}

   这里的关键是在return前插入代码, 还有增加变量timer。具体的反编译过程就不展示了,直接上代码:

  public static class ThirdMethodVisitor extends MethodVisitor {public ThirdMethodVisitor(MethodVisitor mv) {super(Opcodes.ASM5, mv);}/*** 进入方法*/@Overridepublic void visitCode() {super.visitCode();mv.visitVarInsn(Opcodes.ALOAD, 0);mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);mv.visitInsn(Opcodes.LNEG);mv.visitFieldInsn(Opcodes.PUTFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");}/*** return前插入代码*/@Overridepublic void visitInsn(int opcode) {if (opcode == Opcodes.RETURN) {mv.visitVarInsn(Opcodes.ALOAD, 0);mv.visitInsn(Opcodes.DUP);mv.visitFieldInsn(Opcodes.GETFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);mv.visitInsn(Opcodes.LADD);mv.visitFieldInsn(Opcodes.PUTFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitVarInsn(Opcodes.ALOAD, 0);mv.visitFieldInsn(Opcodes.GETFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);}super.visitInsn(opcode);}@Overridepublic void visitMaxs(int maxStack, int maxLocals) {mv.visitMaxs(5, 3);}}

   首先看visitCode方法, 做的事情就是this.timer = System.currentTimeMillis(),对这行代码进行拆分,就是获取时间戳赋值给timer,对应底下的指令mv.visitVarInsn(Opcodes.ALOAD, 0) 先将实例对象入栈即我们用的变量this,接着访问方法获取系统时间戳然后执行LNEG取反入栈,最后在执行访问方法的指令PUTFIELD把值赋给timer,需要的参数是时间戳和this变量,this变量用于访问timer,时间戳则是赋值的变量。
   接着看visitInsn方法,visitInsn可以拦截方法执行的指令做一些插入操作,在这里我们需要做的事在return之前插入时间戳的计算和打印, 代码比较长如下:

    if (opcode == Opcodes.RETURN) {mv.visitVarInsn(Opcodes.ALOAD, 0);mv.visitInsn(Opcodes.DUP);mv.visitFieldInsn(Opcodes.GETFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);mv.visitInsn(Opcodes.LADD);mv.visitFieldInsn(Opcodes.PUTFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitVarInsn(Opcodes.ALOAD, 0);mv.visitFieldInsn(Opcodes.GETFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);}

   老规矩,拆解代码 this.timer += System.currentTimeMillis(), 需要取出timer的值,获取时间戳,进行加法操作,然后结果赋值到timer,这里需要用到两个this变量,因为要访问timer两次,所以可以看到一个新的指令,DUP,DUP的意思就是复制栈顶变量然后入栈,也就是说拷贝多一份this变量,底下的指令已经分析过了,就不再赘述。
   到这里,还没完成,因为timer变量还没生成呢,类变量的生成就要依赖ClassVisitor了, 拦截ClassVisitor的visitEnd方法,动态增加变量,如下:

   @Overridepublic void visitEnd() {cv.visitField(Opcodes.ACC_PRIVATE, "timer", Type.getDescriptor(long.class), null, null);super.visitEnd();}

代码已经上传 AsmDemo

总结

   这里介绍的只是动态修改类的冰山一角,动态生成类的应用场景很多,像市面上的路由框架,热修复框架,很多都是利用了动态修改类的方式进行代码的注入,所以路还很长,还需更加努力。

参考链接

动态生成类的一些错误
字节码指令介绍
字节码原理

Java字节码介绍及动态修改类相关推荐

  1. JVM学习笔记(Ⅰ):Class类文件结构解析(带你读懂Java字节码,这一篇就够了)

    JVM学习笔记(Ⅰ):Class类文件结构解析,带你读懂Java字节码 前言:本文属于博主个人的学习笔记,博主也是小白.如果有不对的地方希望各位帮忙指出.本文主要还是我的学习总结,因为网上的一些知识分 ...

  2. CJBE-Continued Java Bytecode Editor (JAVA字节码编辑器推介)

    CJBE-Continued Java Bytecode Editor (续JAVA字节码编辑器) CJBE-Continued Java Bytecode Editor(续java字节码编辑器),一 ...

  3. java修改字节码技术,Javassist修改class,ASM修改class

    背景: 项目使用的Logback 1.1.11版本的类ch.qos.logback.core.rolling.helper.RollingCalendar的periodBarriersCrossed方 ...

  4. 【JVM】字节码与ASM字节码增强、Instrument实现类的动态重加载

    目录 字节码与ASM字节码增强 什么是字节码? 字节码结构 操作数栈与字节码 字节码增强 ASM 运行时类加载 Instrument JPDA与JVMTI instrument实现热加载的过程 字节码 ...

  5. 牛散村:Java字节码编程javassist的详细介绍

    本篇文章将和大家分享一下关于Java字节码编程中一个非常之好用的技术javassist,下面将详细为大家介绍一下javassist技术,以及具体实例代码讲解. 一.Javassist入门 (一)Jav ...

  6. Java Agent与ASM字节码介绍

    Java Agent Java Agent是jdk1.5以后引入的,也叫做Java代理. javaAgent是运行方法之前的拦截器.我们利用javaAgent和ASM字节码技术,在JVM加载class ...

  7. 使用JBE(Java Bytecode Editor)修改Java字节码

    JBE JBE(Java Bytecode Editor)是一个Java字节码编辑工具,而且是开源的,该项目是基于jclasslib ej-technologies( https://github.c ...

  8. 用JBE修改Java字节码

    2019独角兽企业重金招聘Python工程师标准>>> 用JBE修改Java字节码 注意:我们在每一章都有一个实战环节,大家在阅读一些基本知识的时候可以同时参考实战,带着实际应用的需 ...

  9. 《Java平台体系》——第二章 JVM——实战:用JBE修改Java字节码

    说了那么多,大家可能晕晕的,我自己也晕晕的.下面我们来动动手吧! 注意:我们在每一章都有一个实战环节,大家在阅读一些基本知识的时候可以同时参考实战,带着实际应用的需要去学习基本知识是一件很愉快的事情. ...

最新文章

  1. 敏捷结果30天之第六天:周五回顾,找到三件做的好以及三件需要改善的事情...
  2. 二进制补码求值用c语言,C语言程序设计第2章数据类型.运算符与表达式.ppt
  3. Python uuid 介绍
  4. java构造函数_JAVA的构造函数是怎么写的。万分感谢。路过的请不要嘲笑%_%
  5. 1岁确诊脑瘫,“轮椅博士”,毕业了!
  6. 一维数组去重处理法一(C语言)
  7. Python 3.9.0a6 已可用于测试
  8. 第3章 一切基于pom
  9. zabbix:unable to create mutex for log file
  10. mac系统的UTF-8 BOM编码
  11. 用java写图形验证码,超级简单
  12. 碰撞触发器Trigger
  13. vsftpd参数cmds_allowed
  14. FFmpeg 在 Python 中的安装使用方法详解
  15. Hi3519AV100开启UART串口
  16. 视频合并软件怎么把多个视频合并为一个视频
  17. SpringCloud入门 —— SSO 单点登录
  18. TMC2208-LA步进电机驱动芯片
  19. 项目经理如何写年终总结,范文+PPT模板
  20. Citrix 服务器虚拟化之三十二 XenConvert

热门文章

  1. Paper Writting2 - 写作框架
  2. 手把手教会你-棋盘覆盖算法以及代码实现!!
  3. vue+vant实现按月计算周
  4. golang fmt.Sprintf fmt.Fprintf fmt.Printf
  5. html转义字符 js方法,js解析html转义字符 用 unescape() 函数
  6. 分享一款特别好用的js解密网站
  7. 货运APP开发面临市场的考验
  8. 腾讯云 centOS 下安装 chrome 浏览器
  9. 微信小程序项目中JSON文件的作用详解
  10. Nowcoder 27. 二叉树的镜像