JVM——Class文件

简介

C或者C++传统语言写的程序通常首先被编译,然后被连接成单独的、专门支持特定硬件平台和操作系统的二进制文件,因此一个平台上的二进制可执行文件往往不能在其他平台上工作,而Java 程序是先被编译成Class文件,然后通过Classloader装载进入JVM运行时环境的方法区中,直到被字节码执行引擎执行。

写完程序点击运行,编译器会自动将代码编译成class文件然后交给JVM去执行,但是JVM只认Class文件,毫不关心这个Class是如何生成的,只要符合JVM规范中定义的Class文件的结构即可,所以其他语言也可以通过JVM来执行,这就是JVM的语言无关性。

通常我们会认为只有与Java类似的Kotlin、Groovy、Jython等语言能直接编译成Class文件
在这里插入图片描述
但是时至今日,GraalVM的出现已经可以让更多的语言来享受JVM的托管环境,并且能支持的语言也越来越多。
在这里插入图片描述

文件结构

Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,如果遇到需要占用8个字节以上空间的数据项时,会按照高位在前的方式分割成若干个8个字节进行存储。高位在前就是Big-Endian,即按高位字节在地址最低位,最低字节在地址最高位来存储数据。

JVM规范文档中严格规定了Class文件结构,只有满足规范的Class二进制流文件才会被装载到方法区,否则在ClassLoader连接的验证阶段就会被剔除。
JVM中规定的Class文件结构
先看左边一列

  • u1、u2、u4、u8
    这些是无符号数,u1、 u2、 u4、 u8分别代表1个字节、 2个字节、 4个字节和8个字节的无符号数
  • _info结尾
    这些是表,表用于描述有层次关系的复合结构的数据, 整个Class文件本质上也可以视作是一张表

再看右边一列

magic

魔数:很多文件格式标准中都有使用魔数来进行身份识别的习惯, 譬如图片格式, 如GIF或者JPEG等在文件头中都存有魔数,Class文件也不例外,Class文件的魔数是0xCAFEBABE,不能修改,它用来确定这个文件是否为一个能被虚拟机接受的Class文件。

minor_version 和 major_version

minor_version 是副版本号:Java 2出现前被短暂使用过,后来一直被弃用(值固定为0),直到JDK12出现,由于JDK提供的功能集已经非常庞大, 有一些复杂的新特性需要以“公测”的形式放出, 所以设计者重新启用了副版本号, 将它用于标识“技术预览版”功能特性的支持。 如果Class文件中使用了该版本JDK尚未列入正式特性清单中的预览功能, 则必须把次版本号标识为65535, 以便Java虚拟机在加载类文件时能够区分出来。

major_version是主版本号:JDK版本是向下兼容的,它不能执行高于自身版本的Class文件

在这里插入图片描述

constant_pool_count 和 constant_pool[constant_pool_count-1]

constant_pool_count表示常量池里面常量个数,它的值等于常量池中成员数加1
constant_pool[constant_pool_count-1]是常量池,它包含Class文件结构及其子结构中所有字符串常量、类名、接口名、字段名和其他常量

access_flags

访问标志:是一种由标志所构成的掩码,用于表示某个类或者接口的访问权限及属性

this_class

类索引:它的值是常量池中某项的索引

super_class

父类索引,要么是0,要么也是常量池中某项的索引

intertaces_count 和 interfaces[intertaces_count]

intertaces_count是当前类或接口的直接超接口数量
interfaces[intertaces_count]接口表

fields_count 和 fields[fields_count]

fields_count 表示当前class文件fields表的成员个数

methods_count 和 methods[methods_count]

methods_count表示当前class文件methods表的成员个数

attributes_count 和 attributes[attributes_count]

attributes_count 表示当前class文件attributes表的成员个数

了解Class文件具体结构可以看官方文档,或者查看8.0中文文档 提取码:v1fz

实战

首先写一个类

public class Hello{
	private static String message = "Hello World!";
	
	static{
		System.out.println("class has been loaded");
	}
	
	private int add(int a, int b){
		return a + b;
	}
	
	public static void main(String[] args){
		System.out.println("Dean say " + message);
	} 
}

通过javac -g:vars Hello.java 编译成class文件,然后使用javap -verbose Hello,就可以简单查看class文件翻译后的样子

Classfile /C:/Users/LWH/Desktop/Hello.class
  Last modified 2021-3-26; size 839 bytes
  MD5 checksum c9a28db4d04a7b597d25b698820b4bbf
public class Hello
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #13.#32        // java/lang/Object."<init>":()V
   #2 = Fieldref           #33.#34        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Class              #35            // java/lang/StringBuilder
   #4 = Methodref          #3.#32         // java/lang/StringBuilder."<init>":()V
   #5 = String             #36            // Dean say
   #6 = Methodref          #3.#37         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #7 = Fieldref           #12.#38        // Hello.message:Ljava/lang/String;
   #8 = Methodref          #3.#39         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #9 = Methodref          #40.#41        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #10 = String             #42            // Hello World!
  #11 = String             #43            // class has been loaded
  #12 = Class              #44            // Hello
  #13 = Class              #45            // java/lang/Object
  #14 = Utf8               message
  #15 = Utf8               Ljava/lang/String;
  #16 = Utf8               <init>
  #17 = Utf8               ()V
  #18 = Utf8               Code
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               LHello;
  #22 = Utf8               add
  #23 = Utf8               (II)I
  #24 = Utf8               a
  #25 = Utf8               I
  #26 = Utf8               b
  #27 = Utf8               main
  #28 = Utf8               ([Ljava/lang/String;)V
  #29 = Utf8               args
  #30 = Utf8               [Ljava/lang/String;
  #31 = Utf8               <clinit>
  #32 = NameAndType        #16:#17        // "<init>":()V
  #33 = Class              #46            // java/lang/System
  #34 = NameAndType        #47:#48        // out:Ljava/io/PrintStream;
  #35 = Utf8               java/lang/StringBuilder
  #36 = Utf8               Dean say
  #37 = NameAndType        #49:#50        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #38 = NameAndType        #14:#15        // message:Ljava/lang/String;
  #39 = NameAndType        #51:#52        // toString:()Ljava/lang/String;
  #40 = Class              #53            // java/io/PrintStream
  #41 = NameAndType        #54:#55        // println:(Ljava/lang/String;)V
  #42 = Utf8               Hello World!
  #43 = Utf8               class has been loaded
  #44 = Utf8               Hello
  #45 = Utf8               java/lang/Object
  #46 = Utf8               java/lang/System
  #47 = Utf8               out
  #48 = Utf8               Ljava/io/PrintStream;
  #49 = Utf8               append
  #50 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #51 = Utf8               toString
  #52 = Utf8               ()Ljava/lang/String;
  #53 = Utf8               java/io/PrintStream
  #54 = Utf8               println
  #55 = Utf8               (Ljava/lang/String;)V
{
  public Hello();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LHello;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        10: ldc           #5                  // String Dean say
        12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: getstatic     #7                  // Field message:Ljava/lang/String;
        18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        24: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        27: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      28     0  args   [Ljava/lang/String;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: ldc           #10                 // String Hello World!
         2: putstatic     #7                  // Field message:Ljava/lang/String;
         5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #11                 // String class has been loaded
        10: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: return
}

class源文件查看是通过winhex.exe
在这里插入图片描述
下面阐述时位置以二维坐标定位,并且查看时需要将它与Javap工具生成的文件对照起来看
class文件是二进制,这边以16进制打开,一个字节就用两位表示

  • 魔数
    JVM规定class文件的开头是4个字节的魔数,所以[00000000,0]到[00000000, 3]是0xCAFEBABE,说明该文件可以被ClassLoad加载。
  • 主副版本号
    [00000000,4]、[00000000,5]是副版本号,因为使用的是JDK1.8来生成的Class文件,所以副版本号用不到是0。
    [00000000,6]、[00000000,7]是主版本号,0x0034对应的十进制数字是52,在表中可知指JDK1.8。
  • 常量池
    [00000000,8]、[00000000,9]是常量池数量,一共56个,在javap查看的文件中Constant pool下可以看到这些常量,由于Constant pool是从1开始的,所以会看到55个。
    紧跟常量池数量后面的是cp_info,在JVM规范文档中可以看到它的描述:
    在这里插入图片描述
    可以看到这些常量,每个都由一个tag和一个info组成,tag占8个字节,所以后面**[00000000,A]就是第一个常量**的tag:
    在这里插入图片描述
    0x0A对应的就是方法:CONSTANT_Methodref
    在这里插入图片描述
    由此可知tag后面是class_index:[00000000,B],[00000000,C] 0x0D是13,在Javap 工具中可以看到13是一个class指向45,45是一个常量“java/lang/Object”
#13 = Class              #45            // java/lang/Object
#45 = Utf8               java/lang/Object

[00000000,D],[00000000,E] 是name_and_type_index,值为32,在常量池表里面可以看到它是NameAndType,分别指向#16:#17,也就是

 #16 = Utf8               <init>
 #17 = Utf8               ()V
 #32 = NameAndType        #16:#17        // "<init>":()V

第一个常量就看完了,它完整起来就是表述一个Object的init方法
接下来看第二个常量:[00000000,F]对应的tag是CONSTANCT_Fieldref,它和第一个常量结构类似,后面[00000010,0]、[00000010,1]代表class_index是33

#33 = Class              #46            // java/lang/System
#46 = Utf8               java/lang/System

[00000010,2]、[00000010,3]代表name_and_type_index是34

#34 = NameAndType        #47:#48        // out:Ljava/io/PrintStream;
#47 = Utf8               out
#48 = Utf8               Ljava/io/PrintStream;

第三个常量TAG[00000010,4]是7 CONSTANCT_Class:
在这里插入图片描述
所以[00000010,5]、[00000010,6]是他的name_index,值是35

#35 = Utf8               java/lang/StringBuilder

以此类推后面常量就对照着看,直到[00000240,B]结束。

  • 访问标志
    [00000240,C]、[00000240,D]是0x0021在表中对应0x0020和0x0001分别是ACC_SUPER和ACC_PUBLIC,表示该类可以被包外访问,这个在javap下也可以看出来:
flags: ACC_PUBLIC, ACC_SUPER
  • 类索引
    [00000240,E]、[00000240,F]是0x000C,十进制12,在常量池中对应的就是类名Hello
#12 = Class              #44            // Hello
#44 = Utf8               Hello
  • 父类索引
    [00000250,0]、[00000250,1]是0x000D,十进制13,在常量池中对应的是Hello父类类名Object
#13 = Class              #45            // java/lang/Object
#45 = Utf8               java/lang/Object
  • 接口
    [00000250,2]、[00000250,3]是接口数量,这边没有所以是0,后面的interfaces[intertaces_count]自然也就没有了。
  • 字段
    [00000250,4]、[00000250,5]是字段数量,代表有一个字段,那么看下字段field_info的组成
field_info {
	u2				access_flags
	u2				name_index
	u2				descriptor_index
	u2				attributes_count
	attribute_info	attributes[attributes_count]
}

[00000250,6]、[00000250,7]是access_flags,值是0x000A,对应表里就是0x0002和0x0008,private static
[00000250,8]、[00000250,9]是name_index,值是0x000E,对应常量池14,就是字段名message

#14 = Utf8               message

[00000250,A]、[00000250,B]是descriptor_index,值是0x000F,对应常量池里15,表示该字段类型是String

#15 = Utf8               Ljava/lang/String;

[00000250,C]、[00000250,D]是attributes_count,值是0x0000,没有辅助信息,后面attribute_info也没有了。

  • 方法
    [00000250,E]、[00000250,F]是methods_count方法数量,值是0x0004,一共四个方法
    下面是method_info的组成
method_info{
	u2				access_flags
	u2				name_index
	u2				descriptor_index
	u2				attributes_count
	attribute_info	attributes[attributes_count]
}

同样前两位[00000260,0]、[00000260,1]表示access_flags,值为0x0001,是一个public方法
在这里插入图片描述
[00000260,2]、[00000260,3]表示name_index,值为0x0010,对应常量池16

 #16 = Utf8               <init>

[00000260,4]、[00000260,5]表示descriptor_index,值为0x0011,对应常量池17

#17 = Utf8               ()V

[00000260,6]、[00000260,7]表示attributes_count,值为0x0001,说明有一个属性,属性格式是

attribute_info{
	u2 attribute_name_index
	u4 attribute_length
	u1 info[attribute_length]
}

[00000260,8]、[00000260,9]是属性名,值为0x0012,对应常量池18

#18 = Utf8               Code

是Code,[00000260,A]到[00000260,D]四个字节表示Code的长度,一共35,Code的内容在JVM规范中可以详细查看Code属性的含义,也可以通过javap查看

Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LHello;

略过这35位Code的描述,第一个方法就结束了,直接到[00000290,1]、[00000290,2]值是0x0002,是第二个方法的access_flags,是一个private方法,第二个方法的name_index是[00000290,3]、[00000290,4],值为0x0016,对应常量池表中22

#22 = Utf8               add

第二个方法的descriptor_index是[00000290,5]、[00000290,6],值为0x0017,对应常量池表中23,是表示一个有效方法的描述符,这边表示有两个int参数并且返回值为int

#23 = Utf8               (II)I

第二个方法的attributes_count是[00000290,7]、[00000290,8],值为0x0001,有一个属性,属性名attribute_name_index是[00000290,9]、[00000290,A],在常量池中对应18

#18 = Utf8               Code

也是code,长度是54,方法就以此类推查看
第三个方法描述符是[000002D0,5]、[000002D0,6],值为0x0009,由表可知是public static,结合源码可以猜出是main方法到了。
第三个方法的名称是[000002D0,7]、[000002D0,8],值为0x001B,对应常量池中的27

#27 = Utf8               main

就是main方法,再看参数和返回值[000002D0,9]、[000002D0,10],值为0x001C,对应常量池中的28

#28 = Utf8               ([Ljava/lang/String;)V

Class文件中“【”代表数组,"L"代表一个对象,由此可知参数是一个String数组,返回值是Void,main同样由Code属性

  • 属性
    方法看完最后两位[00000340,5]、[00000340,6]是attributes_count,值为0x0000没有属性

自此一个Class文件就读完了,其余部分可以对照着JVM文档查看

使用ASM修改Class文件

ASM是一个Java字节码操作框架,可用于动态生成类或者增强既有类的功能。
ASM可以直接产生二进制Class文件,也可以在类被加载入JVM之前动态改变类行为,ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能根据要求生成新类。
其原理是将class文件载入,然后构建成一棵树。然后根据用户自定义的修改类对该树进行加工,加工完成后即可得到修改后的class文件。
ASM是主要使用的是访问者设置模式,最常用的visitor是FieldVisitor和MethodVisitor。下面是使用ASM计算方法运行时间的示例:

  1. 工具准备
    需要两个jar包:asm-9.1.jar和asm-util-9.1.jarASM jar包下载地址
    这可以帮助开发者快速生成ASM代码
    使用文本对比工具来方便查看ASM代码的变化
  2. 准备一个Hello类,然后用java -cp .;asm-9.1.jar;asm-util-9.1.jar org.objectweb.asm.util.ASMifier Hello生成ASM代码
java类:
public class Hello{
	public void add(int a, int b){
		System.out.println(a + b);
	}
}

使用ASMifier生成的代码:
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ConstantDynamic;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.RecordComponentVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.TypePath;
public class HelloDump implements Opcodes {

public static byte[] dump () throws Exception {

ClassWriter classWriter = new ClassWriter(0);
FieldVisitor fieldVisitor;
RecordComponentVisitor recordComponentVisitor;
MethodVisitor methodVisitor;
AnnotationVisitor annotationVisitor0;

classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "Hello", null, "java/lang/Object", null);

classWriter.visitSource("Hello.java", null);

{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(1, label0);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
}
{
methodVisitor = classWriter.visitMethod(ACC_PRIVATE, "add", "(II)V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(3, label0);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitVarInsn(ILOAD, 1);
methodVisitor.visitVarInsn(ILOAD, 2);
methodVisitor.visitInsn(IADD);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLineNumber(4, label1);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(3, 3);
methodVisitor.visitEnd();
}
classWriter.visitEnd();

return classWriter.toByteArray();
}
}
  1. 写好想要修改后的类,再使用ASMifier生成ASM代码
想要修改后的类:
public class Hello{
	public void add(int a, int b){
		long start = System.currentTimeMillis();
		System.out.println(a + b);
		long end = System.currentTimeMillis();
		System.out.println("time = " + (end - start));
	}
}

ASM代码:
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ConstantDynamic;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.RecordComponentVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.TypePath;
public class HelloDump implements Opcodes {

public static byte[] dump () throws Exception {

ClassWriter classWriter = new ClassWriter(0);
FieldVisitor fieldVisitor;
RecordComponentVisitor recordComponentVisitor;
MethodVisitor methodVisitor;
AnnotationVisitor annotationVisitor0;

classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "Hello", null, "java/lang/Object", null);

classWriter.visitSource("Hello.java", null);

{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(1, label0);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
}
{
methodVisitor = classWriter.visitMethod(ACC_PRIVATE, "add", "(II)V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(3, label0);
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
methodVisitor.visitVarInsn(LSTORE, 3);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLineNumber(4, label1);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitVarInsn(ILOAD, 1);
methodVisitor.visitVarInsn(ILOAD, 2);
methodVisitor.visitInsn(IADD);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
Label label2 = new Label();
methodVisitor.visitLabel(label2);
methodVisitor.visitLineNumber(5, label2);
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
methodVisitor.visitVarInsn(LSTORE, 5);
Label label3 = new Label();
methodVisitor.visitLabel(label3);
methodVisitor.visitLineNumber(6, label3);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
methodVisitor.visitLdcInsn("time = ");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
methodVisitor.visitVarInsn(LLOAD, 5);
methodVisitor.visitVarInsn(LLOAD, 3);
methodVisitor.visitInsn(LSUB);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label label4 = new Label();
methodVisitor.visitLabel(label4);
methodVisitor.visitLineNumber(7, label4);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(6, 7);
methodVisitor.visitEnd();
}
classWriter.visitEnd();

return classWriter.toByteArray();
}
}
  1. 修改的是方法,所以需要一个MethodVisitor来修改方法,将前后ASM代码的差异直接填进去,这是对比结果
    在这里插入图片描述
/**
     * 访问方法
     */
    class ASMMethodVisitor extends MethodVisitor{

        public ASMMethodVisitor(MethodVisitor methodVisitor) {
            //methodVisitor会被赋值给变量mv,以便后续使用
            super(Opcodes.ASM9, methodVisitor);
        }

        /**
         * 方法开始
         */
        @Override
        public void visitCode() {
            mv.visitCode();

            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(Opcodes.LSTORE, 3);
            Label label1 = new Label();
            mv.visitLabel(label1);
            mv.visitLineNumber(4, label1);
        }

        /**
         * 方法结束
         */
        @Override
        public void visitInsn(int opcode) {
            //当方法到结尾或者抛出异常时执行
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
                mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                mv.visitVarInsn(Opcodes.LSTORE, 5);
                Label label3 = new Label();
                mv.visitLabel(label3);
                mv.visitLineNumber(6, label3);
                mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
                mv.visitInsn(Opcodes.DUP);
                mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
                mv.visitLdcInsn("time = ");
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
                mv.visitVarInsn(Opcodes.LLOAD, 5);
                mv.visitVarInsn(Opcodes.LLOAD, 3);
                mv.visitInsn(Opcodes.LSUB);
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                Label label4 = new Label();
                mv.visitLabel(label4);
                mv.visitLineNumber(7, label4);
            }

            mv.visitInsn(opcode);
        }
    }
  1. 通过ClassVisitor找到需要修改的方法
/**
 * 访问类
 */
public class ASMClassVisitor extends ClassVisitor {
    public ASMClassVisitor(ClassVisitor classVisitor) {
        //Opcodes.ASM9 是使用的ASM版本, classVisitor会被赋值给变量cv,以便后续使用
        super(Opcodes.ASM9, classVisitor);
    }


    /**
     * 开始方法
     * @param version       版本
     * @param access        权限
     * @param name          类名
     * @param signature     签名
     * @param superName     父类名
     * @param interfaces    接口
     */
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        cv.visit(version, access, name, signature, superName, interfaces);
    }

    /**
     * 访问方法
     * @param access        权限
     * @param name          方法名
     * @param descriptor    描述
     * @param signature     签名
     * @param exceptions    异常
     * @return
     */
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        //拿到原始方法
        MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
        //排除init方法
        if (mv != null && !name.equals("<init>")){
            //为方法增加记录方法执行时间功能
            mv = new ASMMethodVisitor(mv);
        }
        return mv;
    }
}
  1. 开始修改
public class ASMClassTest {

    public static void main(String[] args)  throws Exception{
        InputStream inputStream = new FileInputStream("C://Users/Dean/Desktop/Hello.class");
        ClassReader cr = new ClassReader(inputStream);
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        ClassVisitor cv = new ASMClassVisitor(cw);

        cr.accept(cv, ClassReader.SKIP_DEBUG);
        byte[] data = cw.toByteArray();

        File f = new File("C://Users/Dean/Desktop/Hello.class");
        FileOutputStream outputStream = new FileOutputStream(f);
        outputStream.write(data);
        outputStream.close();

        System.out.println("class文件修改完毕");
    }

}
  1. 最后编写一个测试类运行
public class Test{
	public static void main(String[] args){
		Hello hello = new Hello();
		hello.add(1, 2);
	}
}

通过javac Test.java, java Test得到运行结果:
3
time = 1

这就证明Class文件的修改完成了,其余更多使用方法可以看官网


版权声明:本文为qq_37658380原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。