本文是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 覆盖引导类文件的位置命令 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 类:
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 中,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/从字节码层面来看,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 确实可以用于获取方法的参数名称(重写 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 }