简介
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连接的验证阶段就会被剔除。
先看左边一列
- 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计算方法运行时间的示例:
- 工具准备
需要两个jar包:asm-9.1.jar和asm-util-9.1.jarASM jar包下载地址
这可以帮助开发者快速生成ASM代码
使用文本对比工具来方便查看ASM代码的变化 - 准备一个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();
}
}
- 写好想要修改后的类,再使用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();
}
}
- 修改的是方法,所以需要一个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);
}
}
- 通过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;
}
}
- 开始修改
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文件修改完毕");
}
}
- 最后编写一个测试类运行
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文件的修改完成了,其余更多使用方法可以看官网