javap反编译和ASM的基础分析和应用介绍

本文是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 指令的运行时解析过程大致分为如下几个步骤:

  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.

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

  }
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值