ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
简单点说,通过 javac 将 .java 文件编译成 .class 文件,.class 文件中的内容虽然不同,但是它们都具有相同的格式,ASM 通过使用访问者(visitor)模式,按照 .class 文件特有的格式从头到尾扫描一遍 .class 文件中的内容,在扫描的过程中,就可以对 .class 文件做一些操作了
提到 Java 字节码,可能很多人都不是很熟悉,大概都知道使用 javac 可以将 .java 文件编译成 .class 文件,.class 文件中存放的就是该 .java 文件对应的字节码内容,比如如下一段 DemoClass.java 代码很简单:
package com.equaker.demo.asm;
public class DemoClass {
private int m;
private static final String userName = "@EQuaker";
private Integer age = 25;
public int inc(){
return ++m;
}
public static final String getUserName(){
return userName;
}
public Integer getAge(){
return age;
}
private String hi(String name){
return "hi" + name;
}
public static final void hello(){
System.out.println("hello");
System.out.println("hello2");
}
public String dd(){
return "dd";
}
}
通过 javac 编译生成对应的 Demo.class 文件,使用纯文本文件打开 Demo.class,其中的内容是以 8 位字节为基础单位的二进制流,表面来看就是由十六进制符号组成的,这一段十六进制符号组成的长串是遵守 Java 虚拟机规范的。字符这里就不贴了。我们使用 javap -verbose DemoClass.class来看些我们勉强能看懂的吧。
F:\demo>javap -verbose DemoClass.class
Classfile /F:/demo/DemoClass.class
Last modified 2021-1-14; size 1155 bytes
MD5 checksum 46133fe2e9d345b28f020039cdfb039a
Compiled from "DemoClass.java"
public class com.equaker.demo.asm.DemoClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #17.#41 // java/lang/Object."<init>":()V
#2 = Methodref #42.#43 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#3 = Fieldref #5.#44 // com/equaker/demo/asm/DemoClass.age:Ljava/lang/Integer;
#4 = Fieldref #5.#45 // com/equaker/demo/asm/DemoClass.m:I
#5 = Class #46 // com/equaker/demo/asm/DemoClass
#6 = String #47 // @EQuaker
#7 = Class #48 // java/lang/StringBuilder
#8 = Methodref #7.#41 // java/lang/StringBuilder."<init>":()V
#9 = String #35 // hi
#10 = Methodref #7.#49 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#11 = Methodref #7.#50 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#12 = Fieldref #51.#52 // java/lang/System.out:Ljava/io/PrintStream;
#13 = String #37 // hello
#14 = Methodref #53.#54 // java/io/PrintStream.println:(Ljava/lang/String;)V
#15 = String #55 // hello2
#16 = String #38 // dd
#17 = Class #56 // java/lang/Object
#18 = Utf8 m
#19 = Utf8 I
#20 = Utf8 userName
#21 = Utf8 Ljava/lang/String;
#22 = Utf8 ConstantValue
#23 = Utf8 age
#24 = Utf8 Ljava/lang/Integer;
#25 = Utf8 <init>
#26 = Utf8 ()V
#27 = Utf8 Code
#28 = Utf8 LineNumberTable
#29 = Utf8 inc
#30 = Utf8 ()I
#31 = Utf8 getUserName
#32 = Utf8 ()Ljava/lang/String;
#33 = Utf8 getAge
#34 = Utf8 ()Ljava/lang/Integer;
#35 = Utf8 hi
#36 = Utf8 (Ljava/lang/String;)Ljava/lang/String;
#37 = Utf8 hello
#38 = Utf8 dd
#39 = Utf8 SourceFile
#40 = Utf8 DemoClass.java
#41 = NameAndType #25:#26 // "<init>":()V
#42 = Class #57 // java/lang/Integer
#43 = NameAndType #58:#59 // valueOf:(I)Ljava/lang/Integer;
#44 = NameAndType #23:#24 // age:Ljava/lang/Integer;
#45 = NameAndType #18:#19 // m:I
#46 = Utf8 com/equaker/demo/asm/DemoClass
#47 = Utf8 @EQuaker
#48 = Utf8 java/lang/StringBuilder
#49 = NameAndType #60:#61 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#50 = NameAndType #62:#32 // toString:()Ljava/lang/String;
#51 = Class #63 // java/lang/System
#52 = NameAndType #64:#65 // out:Ljava/io/PrintStream;
#53 = Class #66 // java/io/PrintStream
#54 = NameAndType #67:#68 // println:(Ljava/lang/String;)V
#55 = Utf8 hello2
#56 = Utf8 java/lang/Object
#57 = Utf8 java/lang/Integer
#58 = Utf8 valueOf
#59 = Utf8 (I)Ljava/lang/Integer;
#60 = Utf8 append
#61 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#62 = Utf8 toString
#63 = Utf8 java/lang/System
#64 = Utf8 out
#65 = Utf8 Ljava/io/PrintStream;
#66 = Utf8 java/io/PrintStream
#67 = Utf8 println
#68 = Utf8 (Ljava/lang/String;)V
{
public com.equaker.demo.asm.DemoClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 25
7: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
10: putfield #3 // Field age:Ljava/lang/Integer;
13: return
LineNumberTable:
line 3: 0
line 8: 4
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #4 // Field m:I
5: iconst_1
6: iadd
7: dup_x1
8: putfield #4 // Field m:I
11: ireturn
LineNumberTable:
line 11: 0
public static final java.lang.String getUserName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
Code:
stack=1, locals=0, args_size=0
0: ldc #6 // String @EQuaker
2: areturn
LineNumberTable:
line 15: 0
public java.lang.Integer getAge();
descriptor: ()Ljava/lang/Integer;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #3 // Field age:Ljava/lang/Integer;
4: areturn
LineNumberTable:
line 18: 0
public static final void hello();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
Code:
stack=2, locals=0, args_size=0
0: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String hello
5: invokevirtual #14 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #15 // String hello2
13: invokevirtual #14 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: return
LineNumberTable:
line 27: 0
line 28: 8
line 29: 16
public java.lang.String dd();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: ldc #16 // String dd
2: areturn
LineNumberTable:
line 32: 0
}
SourceFile: "DemoClass.java"
从上图中,我们可以看到,.class 文件中主要有常量池、字段表、方法表和属性表等内容。访问控制权限(ACC_PUBLIC,ACC_PRIVATR...)。方法形参与返回值,例如String getName(String name);他的descriptor应该为(Ljava/lang/String;)Ljava/lang/String;方法内的code部分有点像汇编,就是需要加载数据进栈,返回这样的操作。具体可查询别人的文章。多看看就明白啦。
ASM 库是一款基于 Java 字节码层面的代码分析和修改工具,那 ASM 和访问者模式有什么关系呢?访问者模式主要用于修改和操作一些数据结构比较稳定的数据,通过前面的学习,我们知道 .class 文件的结构是固定的,主要有常量池、字段表、方法表、属性表等内容,通过使用访问者模式在扫描 .class 文件中各个表的内容时,就可以修改这些内容了
ASM 库是 Visitor 模式的典型应用。
在 ASM 库中存在以下几个重要的类:
classWrite下面的几个方法:
ASM 大致的工作流程是:
讲太多心累,看的迷迷糊糊,那就上代码吧。
我们先来个简单点的吧,查看某个类的所有属性与方法。你可能第一个想到的是java.lang.reflect下面的反射工具。但这里我们用的是ASM哦。
首先我们肯定需要一个ClassReader 去读取某个class文件。然后再通过我们的观察者ClassVistor 去 。我们这里还是使用DemoClass.class 为例。
Asm_Test.java:
package com.equaker.demo.asm;
import org.objectweb.asm.*;
import java.io.FileInputStream;
import java.io.IOException;
public class Asm_Test {
public static void main(String[] args) throws IOException {
// 读取class文件
ClassReader classReader = new ClassReader(new FileInputStream("F:\\demo\\DemoClass.class"));
ClassVisitor classVisitor = new MyClassVisitor(Opcodes.ASM4);
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
System.out.println(classReader.getAccess());
}
}
class MyClassVisitor extends ClassVisitor{
public MyClassVisitor(int i) {
super(i);
}
// 重写了visitMethod方法。
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
System.out.println("visitMethod- methodname: " + name + "; desc: " + desc);
return methodVisitor;
}
// 重写了visitField方法。
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
FieldVisitor fieldVisitor = super.visitField(access, name, descriptor, signature, value);
System.out.println("visitField- fieldname: " + name + "; descriptor: " + descriptor);
return fieldVisitor;
}
}
运行结果如下:
既然我们使用classReader和classVisitor的观察者模式可以读取方法,变量等。而且当我们重写visitField和visitMethod时,发现他是需要返回的。这也就意味着我们可以改变这个类。
现在我们一次干两件事。修改m的控制权限为public,删除inc()方法。这里我们需要用到的就是ClassAdapter。这个类实现了ClassVisitor接口。功能预期差不多,尽管在ASM 4系列就已经去掉了ClassAdapter,把其功能融合到ClassVisitor里了。但是我们就写写啦,这些都不重要。
EditParamClassAdapter.java(修改属性控制权限):
package com.equaker.demo.asm;
import com.sun.xml.internal.ws.org.objectweb.asm.ClassAdapter;
import com.sun.xml.internal.ws.org.objectweb.asm.ClassVisitor;
import com.sun.xml.internal.ws.org.objectweb.asm.FieldVisitor;
import com.sun.xml.internal.ws.org.objectweb.asm.Opcodes;
/**
* 将 m 的访问权限 改为 public
*
*/
public class EditParamClassAdapter extends ClassAdapter {
public EditParamClassAdapter(ClassVisitor cv) {
super(cv);
}
@Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
System.out.println("visitField- fieldname: " + name + "; desc: " + desc);
//如果属性名称为m,则控制昂问权限为public。
if("m".equalsIgnoreCase(name)){
access = Opcodes.ACC_PUBLIC;
}
FieldVisitor fieldVisitor = super.visitField(access, name, desc, signature, value);
return fieldVisitor;
}
}
DeleteMethodClassAdapter.java(删除inc方法):
package com.equaker.demo.asm;
import com.sun.xml.internal.ws.org.objectweb.asm.ClassAdapter;
import com.sun.xml.internal.ws.org.objectweb.asm.ClassVisitor;
import com.sun.xml.internal.ws.org.objectweb.asm.MethodVisitor;
/**
*
* 删除 inc() 方法
*
*/
public class DeleteMethodClassAdapter extends ClassAdapter {
public DeleteMethodClassAdapter(ClassVisitor cv) {
super(cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println("visitMethod- methodname: " + name + "; desc: " + desc);
//直接返回null就行了。
if("inc".equalsIgnoreCase(name)){
return null;
}
return super.visitMethod(access, name, desc, signature, exceptions);
}
}
这里我们主要完成了在字节码文件的遍历过程中,操作字节码。下面我们试着生成新的class文件。
WriteAsm_Test.java:
package com.equaker.demo.asm;
import com.sun.xml.internal.ws.org.objectweb.asm.*;
import java.io.*;
import java.lang.reflect.Method;
public class WriteAsm_Test implements Opcodes {
public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassReader classReader = new ClassReader(new FileInputStream("F:\\demo\\DemoClass.class"));
//写class文件
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassAdapter editParamClassAdapter = new EditParamClassAdapter(classWriter);
ClassAdapter deleteMethodClassAdapter = new DeleteMethodClassAdapter(editParamClassAdapter);
classReader.accept(deleteMethodClassAdapter, ClassReader.SKIP_DEBUG);
byte[] bs = classWriter.toByteArray();
OutputStream outputStream = new FileOutputStream("F:\\demo\\DemoClass2.class");
outputStream.write(bs);
outputStream.flush();
}
}
这里有个东西需要注意一下。我么声明了两个Adapter去设计字节码的修改。这里面存在一个引用链的问题。读者可以仔细看一下我的声明顺序以及利用构造器进行依赖传递。
运行结果:我们把生成的class文件放在idea里面 就可以看到结果了。不出意外的话应该没问题。
你有没有想过从无到有生成一个Class?希望你想过。。。
我们先来看看我们的目标类吧。
TestClass.java:
package com.equaker.demo.asm;
public class TestClass {
public static final String userName = "@EQuaker";
public Integer age;
public static final String getUserName() {
System.out.println("this is getUserName() method");
System.out.println("this is hello World");
return "这是静态将变量(在类加载解析阶段由符号引用转为直接引用,所以不牵扯 getfield操作)";
}
public Integer getAge() {
return this.age;
}
}
当我写到这里的时候,我犹豫了半天,该咋写,因为东西实在是太多了。大家可以看到我定义的两个成员变量类型的区别。其实光是这里都大有文章。慢慢来,慢慢来,慢慢来。。。
我们利用ASM框架生成字节码class的时候主要用的就是ClassWrite的visitMethod 和visitFiled。这个可以帮助我们去构建。代码里面细说。
WriteAsm_Test.java:
package com.equaker.demo.asm;
import com.sun.xml.internal.ws.org.objectweb.asm.*;
import java.io.*;
import java.lang.reflect.Method;
public class WriteAsm_Test implements Opcodes {
public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassReader classReader = new ClassReader(new FileInputStream("F:\\demo\\DemoClass.class"));
//声明一个ClassWriter
ClassWriter classWriter2 = new ClassWriter(ClassWriter.COMPUTE_MAXS);
//classWriter2.newClass("TestClass");
//classWriter2.newMethod("TestClass", "testMethod", "()V", false);
//参数依次为:JDK版本52(1.8),类控制访问权限为public,继承Object, 没有泛型, 不是接口
classWriter2.visit(52, Opcodes.ACC_PUBLIC|Opcodes.ACC_SUPER, "com/equaker/demo/asm/TestClass", null, "java/lang/Object", null);
//新增一个构造器 <init> ()V 表示狗构造器
// MethodVisitor constructMethodVisitor = classWriter2.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
// constructMethodVisitor.visitCode();
// constructMethodVisitor.visitEnd();
//新增一个public static final String userName = "@EQuaker" 字段
FieldVisitor nameFieldVisitor = classWriter2.visitField(Opcodes.ACC_PUBLIC|Opcodes.ACC_STATIC|Opcodes.ACC_FINAL, "userName", "Ljava/lang/String;", null, "@EQuaker");
nameFieldVisitor.visitEnd();
//新增一个public Integer age 字段
FieldVisitor ageFieldVisitor = classWriter2.visitField(Opcodes.ACC_PUBLIC, "age", "Ljava/lang/Integer;", null, 25);
ageFieldVisitor.visitEnd();
//新增一个static final string getUserName方法
MethodVisitor testMethodVisitor = classWriter2.visitMethod(Opcodes.ACC_PUBLIC|Opcodes.ACC_STATIC|Opcodes.ACC_FINAL, "getUserName", "()Ljava/lang/String;", null, null);
testMethodVisitor.visitCode();
//新增方法内代码 System.out.Println("this is getUserName() method")
testMethodVisitor.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System", "out","Ljava/io/PrintStream;");
testMethodVisitor.visitLdcInsn("this is getUserName() method");
testMethodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
//新增方法内代码 System.out.Println("this is hello World")
testMethodVisitor.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System", "out","Ljava/io/PrintStream;");
testMethodVisitor.visitLdcInsn("this is hello World");
testMethodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
//新增方法内代码 return userName;
//由于usernam 是 静态最终型的,所以再编译期间已经由符号引用转为直接引用
// 这一点需要和实例变量区分
testMethodVisitor.visitLdcInsn("这是静态将变量(在类加载解析阶段由符号引用转为直接引用,所以不牵扯 getfield操作)");
testMethodVisitor.visitInsn(Opcodes.ARETURN);
// testMethodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
// testMethodVisitor.visitFieldInsn(Opcodes.GETFIELD, "com/equaker/demo/asm/TestClass", "userName", "Ljava/lang/String;");
//参数一次为: 最大栈大小, 最大本地变量个数。想都不用想 1》2
testMethodVisitor.visitMaxs(2,1);
testMethodVisitor.visitEnd();
// 新增 getAge()方法
MethodVisitor agetMethodVisitor = classWriter2.visitMethod(Opcodes.ACC_PUBLIC, "getAge", "()Ljava/lang/Integer;", null, null);
agetMethodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
agetMethodVisitor.visitFieldInsn(Opcodes.GETFIELD, "com/equaker/demo/asm/TestClass", "age", "Ljava/lang/Integer;");
//testMethodVisitor.visitLdcInsn(22);
agetMethodVisitor.visitInsn(Opcodes.ARETURN);
agetMethodVisitor.visitMaxs(2, 1);
agetMethodVisitor.visitEnd();
byte[] bs2 = classWriter2.toByteArray();
OutputStream outputStream2 = new FileOutputStream("F:\\soyuan_workspace_study\\equaker-demo\\src\\test\\java\\com\\equaker\\demo\\asm\\TestClass.class");
outputStream2.write(bs2);
outputStream2.flush();
}
}
大家看到这里,不知道有没有一丝眼熟,有没有想起我开篇介绍的通过javap -verbose DemoClass.class文件结构。现在可以去看看两者之间的关系了。无论是声明变量还是声明方法。我们使用的都是全路径。这里我们挑几个说说吧。
加入方法是public static final String getName(String name){}。那我们的access应该就是:Opcodes.ACC_PUBLIC|Opcodes.ACC_STATIC|Opcodes.ACC_FINAL。方法名:getName。方法描述descriptor:
(Ljava/lang/String;)Ljava/lang/String;。。。。。应该不难理解。
还有关于业务代码的问题比如System.out.println("");看似简单一行,实际上我们需要先声明一个java/io/PrintStream类型的变量,然后在创建变量(即打印内容),最后在执行反射执行代码。意会。。。
我们再来看一个问题,大家再仔细看看我上面DemoClass.class 通过javap贴出来的内容。为了方便,我再贴一遍。
F:\demo>javap -verbose DemoClass.class
Classfile /F:/demo/DemoClass.class
Last modified 2021-1-14; size 1155 bytes
MD5 checksum 46133fe2e9d345b28f020039cdfb039a
Compiled from "DemoClass.java"
public class com.equaker.demo.asm.DemoClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #17.#41 // java/lang/Object."<init>":()V
#2 = Methodref #42.#43 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#3 = Fieldref #5.#44 // com/equaker/demo/asm/DemoClass.age:Ljava/lang/Integer;
#4 = Fieldref #5.#45 // com/equaker/demo/asm/DemoClass.m:I
#5 = Class #46 // com/equaker/demo/asm/DemoClass
#6 = String #47 // @EQuaker
#7 = Class #48 // java/lang/StringBuilder
#8 = Methodref #7.#41 // java/lang/StringBuilder."<init>":()V
#9 = String #35 // hi
#10 = Methodref #7.#49 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#11 = Methodref #7.#50 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#12 = Fieldref #51.#52 // java/lang/System.out:Ljava/io/PrintStream;
#13 = String #37 // hello
#14 = Methodref #53.#54 // java/io/PrintStream.println:(Ljava/lang/String;)V
#15 = String #55 // hello2
#16 = String #38 // dd
#17 = Class #56 // java/lang/Object
#18 = Utf8 m
#19 = Utf8 I
#20 = Utf8 userName
#21 = Utf8 Ljava/lang/String;
#22 = Utf8 ConstantValue
#23 = Utf8 age
#24 = Utf8 Ljava/lang/Integer;
#25 = Utf8 <init>
#26 = Utf8 ()V
#27 = Utf8 Code
#28 = Utf8 LineNumberTable
#29 = Utf8 inc
#30 = Utf8 ()I
#31 = Utf8 getUserName
#32 = Utf8 ()Ljava/lang/String;
#33 = Utf8 getAge
#34 = Utf8 ()Ljava/lang/Integer;
#35 = Utf8 hi
#36 = Utf8 (Ljava/lang/String;)Ljava/lang/String;
#37 = Utf8 hello
#38 = Utf8 dd
#39 = Utf8 SourceFile
#40 = Utf8 DemoClass.java
#41 = NameAndType #25:#26 // "<init>":()V
#42 = Class #57 // java/lang/Integer
#43 = NameAndType #58:#59 // valueOf:(I)Ljava/lang/Integer;
#44 = NameAndType #23:#24 // age:Ljava/lang/Integer;
#45 = NameAndType #18:#19 // m:I
#46 = Utf8 com/equaker/demo/asm/DemoClass
#47 = Utf8 @EQuaker
#48 = Utf8 java/lang/StringBuilder
#49 = NameAndType #60:#61 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#50 = NameAndType #62:#32 // toString:()Ljava/lang/String;
#51 = Class #63 // java/lang/System
#52 = NameAndType #64:#65 // out:Ljava/io/PrintStream;
#53 = Class #66 // java/io/PrintStream
#54 = NameAndType #67:#68 // println:(Ljava/lang/String;)V
#55 = Utf8 hello2
#56 = Utf8 java/lang/Object
#57 = Utf8 java/lang/Integer
#58 = Utf8 valueOf
#59 = Utf8 (I)Ljava/lang/Integer;
#60 = Utf8 append
#61 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#62 = Utf8 toString
#63 = Utf8 java/lang/System
#64 = Utf8 out
#65 = Utf8 Ljava/io/PrintStream;
#66 = Utf8 java/io/PrintStream
#67 = Utf8 println
#68 = Utf8 (Ljava/lang/String;)V
{
public com.equaker.demo.asm.DemoClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 25
7: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
10: putfield #3 // Field age:Ljava/lang/Integer;
13: return
LineNumberTable:
line 3: 0
line 8: 4
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #4 // Field m:I
5: iconst_1
6: iadd
7: dup_x1
8: putfield #4 // Field m:I
11: ireturn
LineNumberTable:
line 11: 0
public static final java.lang.String getUserName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
Code:
stack=1, locals=0, args_size=0
0: ldc #6 // String @EQuaker
2: areturn
LineNumberTable:
line 15: 0
public java.lang.Integer getAge();
descriptor: ()Ljava/lang/Integer;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #3 // Field age:Ljava/lang/Integer;
4: areturn
LineNumberTable:
line 18: 0
public static final void hello();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
Code:
stack=2, locals=0, args_size=0
0: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String hello
5: invokevirtual #14 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #15 // String hello2
13: invokevirtual #14 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: return
LineNumberTable:
line 27: 0
line 28: 8
line 29: 16
public java.lang.String dd();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: ldc #16 // String dd
2: areturn
LineNumberTable:
line 32: 0
}
SourceFile: "DemoClass.java"
你有没有发现关于getUsername和getAge在code部分的代码不太一样。在getAge里面我们先Aload一个栈空间,然后通过getField操作获取age变量,最后执行返回。而在getUserName里面缺没有涉及aload和getfield操作,而是直接声明了一个本地栈变量(ldc)。如果发现了,说明你很棒哦,,,
既然都有字节码了,你有没有想过去执行这个class里面的某个方法???你可能一脸懵逼,我TM连代码都没有。先抛出我的答案:类加载器+反射。
大家可能都知道类加载器是用来加载类的,可是真的需要在什么时候使用呢?这不,机会来了。。。
MyClassLoader.java:
package com.equaker.demo.asm;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
public class MyCLassLoader extends ClassLoader {
public MyCLassLoader(){
super();
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return super.loadClass(name);
}
//全路径名需要换成自己的,我这里省事了。。。
public Class<?> defineMyClass(byte[] b, int off, int len){
return super.defineClass("com.equaker.demo.asm.TestClass", b, off, len);
}
}
反射代码我们直接在上一步的WriteAsm_Test.java 追加。
package com.equaker.demo.asm;
import com.sun.xml.internal.ws.org.objectweb.asm.*;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class WriteAsm_Test implements Opcodes {
public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException {
//。。。代码在上面
MyCLassLoader classLoader = new MyCLassLoader();
Class clazz = classLoader.defineMyClass(bs2, 0, bs2.length);
Method[] methods = clazz.getDeclaredMethods();
for(Method method : methods){
System.out.println("方法名称: "+method.getName());
if("getUserName".equalsIgnoreCase(method.getName())){
Object ret = method.invoke(clazz, null);
System.out.println("ret:" + ret);
}
}
}
}
as you see:
visitField- fieldname: m; desc: I
visitField- fieldname: userName; desc: Ljava/lang/String;
visitField- fieldname: age; desc: Ljava/lang/Integer;
visitMethod- methodname: <init>; desc: ()V
visitMethod- methodname: inc; desc: ()I
visitMethod- methodname: getUserName; desc: ()Ljava/lang/String;
visitMethod- methodname: getAge; desc: ()Ljava/lang/Integer;
visitMethod- methodname: hi; desc: (Ljava/lang/String;)Ljava/lang/String;
visitMethod- methodname: hello; desc: ()V
visitMethod- methodname: dd; desc: ()Ljava/lang/String;
方法名称: getAge
方法名称: getUserName
this is getUserName() method
this is hello World
ret:这是静态将变量(在类加载解析阶段由符号引用转为直接引用,所以不牵扯 getfield操作)
讲完这些,初中该学的东西差不多了,大家感到困惑的点可能就是 javap里面的那个代码该咋写。推荐个神器 ASMifer。 idea可以安装插件 bytecode outline。
截两张图给大家。
图一,
图二,
你没看错,他帮你写好代码了。实际开发中我们参考api就可以了。
因篇幅问题不能全部显示,请点此查看更多更全内容