本文是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.
使用 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 属性了,自然就不可能获取本地变量表了。
jvm1.7字节码说明 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.3
Java8 字节码新增MethodParameters属性来存储方法的参数名,所以Java8中反射可以直接支持。
此时我们还想获取怎么办?同样类似 mybatis 那样做,为 @Param 注解的参数提供一个值,显式保存参数名到注解中。参数名与需要映射的字段名或对象名相同。
ASM 提供了两种 API 模型用来操作 Class 文件。一种是核心API,基于事件来表示类,另一种是树 API,以基于对象的形式表示类。
这两个 API 可以类比于 Java 中两种处理 XML 的框架:SAX 和 DOM,基于事件的类似于 SAX,基于对象的类似于DOM。
我们只讨论基于事件的 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
}