javap和ASM

2020/09/03 其他 共 10223 字,约 30 分钟
梦境迷离

本文是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$.classJavapScalaSpec.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语言层面来看,staticprivatefinal修饰的方法,父类方法以及实例构造器,这些方法称为非虚方法。与之相反,其他所有的方法称为虚方法。字节码指令层面来讲,invokestaticinvokespecial调用的方法都是非虚方法。 然后invokevirtual指令刚好是 Java 实现动态多分配(多态)的指令。

Java中多态机制的实现过程

invokevirtual指令的运行时解析过程大致分为如下几个步骤:

  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作 C
  2. 如果在类型 C 中找到常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常
  3. 否则,按照继承关系从下往上依次对 C 的各个父类进行第2步的搜索和验证过程
  4. 如果始终没找到合适的方法,则抛出java.lang.AbstractMethodError异常

正是由于invokevirtual指令是这样的一个执行过程,所以这就解释了为什么Java语言里面实现多态需要如下三个条件:

  • 父类引用指向子类对象
  • 有继承的存在
  • 子类重写父类方法
  1. 由于父类引用指向子类对象,所以JVM会去首先去查找该子类对象对应的类型。
  2. 又由于有继承的存在,所以子类的方法不可能比父类少,这就保证了,只要该引用变量能调用的方法,子类中一定存在。所以第二步一定能在子类的类型中查找到调用的方法。
  3. 方法找到后就可以执行了,至于方法执行后能不能产生不同的效果(多态),得看子类是否重写了这个方法。所以要想产生多态,子类得重写父类方法。(以上所说的方法均是指的虚方法)

使用本地变量表

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内部相应的方法。比如:

  • 扫描到类文件时,会回调ClassVisitorvisit()方法;
  • 扫描到类注解时,会回调ClassVisitorvisitAnnotation()方法;
  • 扫描到类成员时,会回调ClassVisitorvisitField()方法;
  • 扫描到类方法时,会回调ClassVisitorvisitMethod()方法。

扫描到相应结构内容时,会回调相应方法,该方法会返回一个对应的字节码操作对象(比如,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 确实可以用于获取方法的参数名称(重写MethodVisitorvisitLocalVariable方法),但是无法获取接口的方法的名称(其实是抽象方法都无法获取)。 为了测试,我们为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,它使用了访问者设计模式,提供了读写字节码的功能:ClassReaderClassWriter

我们使用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

  }

文档信息

Search

    Table of Contents