本文是ASM与CGLIB的简单使用的后续,更多的是提供思路和总结自己觉得比较有用的内容
javap
是Java class文件分解器,可以反编译(即对javac
编译的文件进行反编译),也可以查看java编译器生成的字节码。用于分解class文件。
先看看javap
都有哪些参数(java 8):
参数 | 说明 |
---|---|
-help –help -? | 输出此用法消息 |
-version | 版本信息 |
-v -verbose | 输出附加信息 |
-l | 输出行号和本地变量表 |
-public | 仅显示公共类和成员 |
-protected | 显示受保护的/公共类和成员 |
-package | 显示程序包/受保护的/公共类和成员 (默认) |
-p -private | 显示所有类和成员 |
-c | 对代码进行反汇编 |
-s | 输出内部类型签名 |
-sysinfo | 显示正在处理的类的系统信息 (路径、大小、日期,、MD5 散列) |
-constants | 显示静态最终常量 |
-classpath | 指定查找用户类文件的位置 |
-bootclasspath | 覆盖引导类文件的位置 |
反编译 Java 文件
命令 javap -c
Java 类:
public class JavapJavaSpec {
private static final int _P_1 = 1;
public static final int _P_2 = 2;
public String method1() {
return "hello";
}
public String method2() {
return "world";
}
public static String methodStatic() {
return "hello world";
}
}
编译后生成一个字节码文件JavapJavaSpec.class
,反编译字节码文件:
//javap -c JavapJavaSpec
public class JavapJavaSpec {
public static final int _P_2;
public JavapJavaSpec();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public java.lang.String method1();
Code:
0: ldc #2 // String hello
2: areturn
public java.lang.String method2();
Code:
0: ldc #3 // String world
2: areturn
public static java.lang.String methodStatic();
Code:
0: ldc #4 // String hello world
2: areturn
}
反编译 Scala 文件
Scala 类:
class JavapScalaSpec {
def method1 = "hello"
def method2 = "world"
}
object JavapScalaSpec {
private val _P_1 = 1
val _P_2 = 2
def methodStatic = "hello world"
}
编译生成两个字节码文件JavapScalaSpec$.class
、JavapScalaSpec.class
分别反编译两个字节码文件:
//javap -c JavapScalaSpec
Compiled from "JavapScalaSpec.scala"
public class JavapScalaSpec {
public static java.lang.String methodStatic();
Code:
0: getstatic #16 // Field JavapScalaSpec$.MODULE$:LJavapScalaSpec$;
3: invokevirtual #18 // Method JavapScalaSpec$.methodStatic:()Ljava/lang/String;
6: areturn
public static int _P_2();
Code:
0: getstatic #16 // Field JavapScalaSpec$.MODULE$:LJavapScalaSpec$;
3: invokevirtual #22 // Method JavapScalaSpec$._P_2:()I
6: ireturn
public java.lang.String method1();
Code:
0: ldc #25 // String hello
2: areturn
public java.lang.String method2();
Code:
0: ldc #30 // String world
2: areturn
public JavapScalaSpec();
Code:
0: aload_0
1: invokespecial #34 // Method java/lang/Object."<init>":()V
4: return
}
//javap -c JavapScalaSpec$
public final class JavapScalaSpec$ {
public static JavapScalaSpec$ MODULE$;
public static {};
Code:
0: new #2 // class JavapScalaSpec$
3: invokespecial #15 // Method "<init>":()V
6: return
public int _P_2();
Code:
0: aload_0
1: getfield #21 // Field _P_2:I
4: ireturn
public java.lang.String methodStatic();
Code:
0: ldc #25 // String hello world
2: areturn
}
Scala 静态方法实现
在 Scala 中,JavapScalaSpec
自己伴生对象中的方法就是静态方法,编译后保存在JavapScalaSpec$.class
中。 如反编译JavapScalaSpec.class
所显示,JavapScalaSpec.class
中的methodStatic
本质是通过持有JavapScalaSpec$
的静态字MODULE$
(这个字段是JavapScalaSpec$
的引用)调用JavapScalaSpec$
的methodStatic
方法(这个方法是没有static
修饰的,说明它就是一个简单的实例方法),JavapScalaSpec.class
并没有静态方法本身的实现。 静态字段 _P_2 同样类似,但是需要注意的是,JavapScalaSpec
伴生对象的字段虽然是起到了Java静态字段的作用,实际在字节码中却是一个静态方法,且静态方法本质是去调用JavapScalaSpec$
的实例方法 _P_2。
心细的人应该想到了,其实Java在调用Scala代码时,就是这样使用的:
public static final int _P_3 = JavapScalaSpec._P_2();
// 这里,_P_2 虽然是 JavapScalaSpec 伴生对象的字段
// 但是 Java 使用时 JavapScalaSpec 就是当做方法的,所以 Java 调用 Scala 代码有时候感觉怪怪的,而 Scala 调用Java 代码 就没有这些问题了。
// 这是因为 Scala 在编译期间经常通过合成类的方式来实现兼容 Java 或实现 Scala 的特殊功能。
从字节码中也可以看到,调用方式是使用了invokevirtual
指令:
MODULE$:LJavapScalaSpec$;
3: invokevirtual #18 // Method JavapScalaSpec$.methodStatic:()Ljava/
JVM 方法调用指令
从字节码层面来看,Java 中的所有方法调用,最终无外乎转换为如下几条调用指令:
指令 | 说明 |
---|---|
invokestatic | 调用静态方法 |
invokespecial | 调用实例构造器 |
invokevirtual | 调用所有的虚方法 |
invokeinterface | 调用接口方法,会在运行时再确定一个实现此接口的对象 |
invokedynamic | 调用动态方法。JDK 7引入的,主要是为了支持动态语言的方法调用 |
JVM提供了上述5条方法调用指令,所以不妨从字节码层面来一窥Java多态机制的执行过程。
虚方法和非虚方法
上述5条方法调用指令中的invokevirtual
负责调用所有的虚方法。那么什么是虚方法?什么是非虚方法呢? 从Java语言层面来看,static
,private
,final
修饰的方法,父类方法以及实例构造器,这些方法称为非虚方法。与之相反,其他所有的方法称为虚方法。字节码指令层面来讲,invokestatic
和invokespecial
调用的方法都是非虚方法。 然后invokevirtual
指令刚好是 Java 实现动态多分配(多态)的指令。
Java中多态机制的实现过程
invokevirtual
指令的运行时解析过程大致分为如下几个步骤:
- 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作 C
- 如果在类型 C 中找到常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回
java.lang.IllegalAccessError
异常 - 否则,按照继承关系从下往上依次对 C 的各个父类进行第2步的搜索和验证过程
- 如果始终没找到合适的方法,则抛出
java.lang.AbstractMethodError
异常
正是由于invokevirtual
指令是这样的一个执行过程,所以这就解释了为什么Java语言里面实现多态需要如下三个条件:
- 父类引用指向子类对象
- 有继承的存在
- 子类重写父类方法
- 由于父类引用指向子类对象,所以JVM会去首先去查找该子类对象对应的类型。
- 又由于有继承的存在,所以子类的方法不可能比父类少,这就保证了,只要该引用变量能调用的方法,子类中一定存在。所以第二步一定能在子类的类型中查找到调用的方法。
- 方法找到后就可以执行了,至于方法执行后能不能产生不同的效果(多态),得看子类是否重写了这个方法。所以要想产生多态,子类得重写父类方法。(以上所说的方法均是指的虚方法)
使用本地变量表
javap -l
这里仅以Java代码为例,Scala类似,为例看到本地变量表,我们新增一个有参的方法:
public static String method3(String hello, String world) {
return hello + world;
}
反编译字节码文件:
//javap -l JavapJavaSpec
Compiled from "JavapJavaSpec.java"
public class JavapJavaSpec {
public static final int _P_2;
public static final int _P_3;
public JavapJavaSpec();
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LJavapJavaSpec;
public java.lang.String method1();
LineNumberTable:
line 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this LJavapJavaSpec;
public java.lang.String method2();
LineNumberTable:
line 16: 0
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this LJavapJavaSpec;
public static java.lang.String methodStatic();
LineNumberTable:
line 20: 0
public static java.lang.String method3(java.lang.String);
LineNumberTable:
line 24: 0
LocalVariableTable:
Start Length Slot Name Signature
0 2 0 hello Ljava/lang/String;
public static java.lang.String method3(java.lang.String, java.lang.String);
LineNumberTable:
line 28: 0
LocalVariableTable:
Start Length Slot Name Signature
0 19 0 hello Ljava/lang/String;
0 19 1 world Ljava/lang/String;
static {};
LineNumberTable:
line 9: 0
}
可以看到LineNumberTable
属性表存放方法的行号信息,LocalVariableTable
属性表中存放方法的局部变量信息。 method3
方法的参数名称是存在LocalVariableTable
中的。这里就引入一个问题,LocalVariableTable
有什么用呢?运行时如何获取类的方法的形参名字呢?
其实这种方法在写框架的时候经常使用,比如 Spring的LocalVariableTableParameterNameDiscoverer。
如果是 Java 8 以上,则通过编译器参数javac -parameters
与反射就能实现:使用Parameter
对象的getName
方法。但是如果不是Java 8以上,就要借助字节码框架了。Spring 中使用的是 ASM。
源码中有一段注释:Uses ObjectWeb’s ASM library for analyzing class files.
ASM 框架执行流程
ASM提供了两组API,Core和Tree:
- Core是基于访问者模式来操作类的
- Tree是基于树节点来操作类的
下文使用的也是ASM的CoreAPI。
ASM内部采用访问者模式将.class
类文件的内容从头到尾扫描一遍,每次扫描到类文件相应的内容时,都会调用ClassVisitor
内部相应的方法。比如:
- 扫描到类文件时,会回调
ClassVisitor
的visit()
方法; - 扫描到类注解时,会回调
ClassVisitor
的visitAnnotation()
方法; - 扫描到类成员时,会回调
ClassVisitor
的visitField()
方法; - 扫描到类方法时,会回调
ClassVisitor
的visitMethod()
方法。
扫描到相应结构内容时,会回调相应方法,该方法会返回一个对应的字节码操作对象(比如,visitMethod()
返回MethodVisitor
实例)。 通过修改这个对象,就可以修改class文件相应结构部分内容,最后将这个ClassVisitor
字节码内容覆盖原来.class
文件就实现了类文件的代码切入。
ASM中提供一个ClassReader
类,这个类可以直接由字节数组或者class文件间接的获得字节码数据。它会调用accept()
方法,接受一个实现了抽象类ClassVisitor
的对象实例作为参数,然后依次调用ClassVisitor
的各个方法。字节码空间上的偏移被转成各种visitXXX方法。 使用者只需要在对应的的方法上进行需求操作即可,无需考虑字节偏移。这个过程中ClassReader
可以看作是一个事件生产者,ClassWriter
继承自ClassVisitor
抽象类,负责将对象化的class文件内容重构成一个二进制格式的class字节码文件,ClassWriter
可以看作是一个事件的消费者。
asm-commons包中提供了一个类
AdviceAdapter
,使用该类可以更加方便的让我们在方法前后注入代码,因为其提供了方法onMethodEnter()
和onMethodExit()
。
使用 ClassReader 获取私有字段
基于上面的,ASM 确实可以用于获取方法的参数名称(重写MethodVisitor
的visitLocalVariable
方法),但是无法获取接口的方法的名称(其实是抽象方法都无法获取)。 为了测试,我们为JavapJavaSpec
新增一个抽象方法来看看为什么:
public abstract String method4(String hello, String world);
我们再次反编译字节码文件:
Compiled from "JavapJavaSpec.java"
public abstract class JavapJavaSpec {
public static final int _P_2;
public static final int _P_3;
public JavapJavaSpec();
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LJavapJavaSpec;
public java.lang.String method1();
LineNumberTable:
line 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this LJavapJavaSpec;
public java.lang.String method2();
LineNumberTable:
line 16: 0
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this LJavapJavaSpec;
public static java.lang.String methodStatic();
LineNumberTable:
line 20: 0
public static java.lang.String method3(java.lang.String);
LineNumberTable:
line 24: 0
LocalVariableTable:
Start Length Slot Name Signature
0 2 0 hello Ljava/lang/String;
public static java.lang.String method3(java.lang.String, java.lang.String);
LineNumberTable:
line 28: 0
LocalVariableTable:
Start Length Slot Name Signature
0 19 0 hello Ljava/lang/String;
0 19 1 world Ljava/lang/String;
public abstract java.lang.String method4(java.lang.String, java.lang.String);
static {};
LineNumberTable:
line 9: 0
}
看看method4
方法,现在就比较清楚了,既然方法参数是通过本地变量表LocalVariableTable
来获取,那么抽象方法都没有LocalVariableTable
属性了,自然就不可能获取本地变量表了。
此时我们还想获取怎么办?同样类似mybatis那样做,为@Param
注解的参数提供一个值,显示保存参数名,这样也能解决获取的参数名都是var0
这种。
下面我们只讨论基于事件的ASM API,它使用了访问者设计模式,提供了读写字节码的功能:ClassReader
和ClassWriter
。
我们使用ClassReader
读取ActiveUsersQueryRequest
类的静态内部类的私有字段:
public class ActiveUsersQueryRequest implements GraphQLOperationRequest {
private static final GraphQLOperation OPERATION_TYPE = GraphQLOperation.QUERY;
private static final String OPERATION_NAME = "activeUsers";
private Map<String, Object> input = new LinkedHashMap<>();
public ActiveUsersQueryRequest() {
}
public void setTimeRange(String timeRange) {
this.input.put("timeRange", timeRange);
}
public void setOffset(Integer offset) {
this.input.put("offset", offset);
}
public void setLimit(Integer limit) {
this.input.put("limit", limit);
}
@Override
public GraphQLOperation getOperationType() {
return OPERATION_TYPE;
}
@Override
public String getOperationName() {
return OPERATION_NAME;
}
@Override
public Map<String, Object> getInput() {
return input;
}
@Override
public String toString() {
return Objects.toString(input);
}
public static class Builder {
// 使用ASM获取这些字段
private String timeRange;
private Integer offset;
private Integer limit;
public Builder() {
}
public Builder setTimeRange(String timeRange) {
this.timeRange = timeRange;
return this;
}
public Builder setOffset(Integer offset) {
this.offset = offset;
return this;
}
public Builder setLimit(Integer limit) {
this.limit = limit;
return this;
}
public ActiveUsersQueryRequest build() {
ActiveUsersQueryRequest obj = new ActiveUsersQueryRequest();
obj.setTimeRange(timeRange);
obj.setOffset(offset);
obj.setLimit(limit);
return obj;
}
}
}
下面是实现逻辑,使用Scala编写:
def getRequestBuilderFields(clazz: Class[_]): Seq[String] = {
// 获取外部类 ActiveUsersQueryRequest 的全类名
val cr = new ClassReader(clazz.getName)
val innerClass = ListBuffer[String]()
val innerClassFields = ListBuffer[String]()
// 传入 类 访问者对象,描述如何访问类字节码
cr.accept(new ClassVisitor(Opcodes.ASM8) {
override def visitInnerClass(name: String, outerName: String, innerName: String, access: Int): Unit = {
// 重新 visitInnerClass 方法,表示我们需要访问 clazz 的内部类
// 因为全类名是 / 分割,所以我们需要替换为 . ,然后我们比较外部类名,然后保存内部类的全类名。
val outName = outerName.replace(File.separator, ".")
if (clazz.getName == outName) {
innerClass.append(name)
}
}
}, 0)
// 获取内部类后,我们继续访问内部类 Builder
if (innerClass.nonEmpty) {
val cr = new ClassReader(innerClass.head)
cr.accept(new ClassVisitor(Opcodes.ASM8) {
// 实现 visitField 访问字段
override def visitField(access: Int, name: String, descriptor: String, signature: String, value: Any): FieldVisitor = {
// 无任何多余操作,只保存所有字段。
innerClassFields.append(name)
super.visitField(access, name, descriptor, signature, value)
}
}, 0)
}
// 返回 Builder 类的所有字段
innerClassFields
}
文档信息
- 本文作者:梦境迷离
- 本文链接:https://blog.dreamylost.cn/%E5%85%B6%E4%BB%96/%E5%85%B6%E4%BB%96-javap%E5%92%8CASM.html
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)