字节码操控框架ASM - 初识

写在前面:

ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

简单点说,通过 javac 将 .java 文件编译成 .class 文件,.class 文件中的内容虽然不同,但是它们都具有相同的格式,ASM 通过使用访问者(visitor)模式,按照 .class 文件特有的格式从头到尾扫描一遍 .class 文件中的内容,在扫描的过程中,就可以对 .class 文件做一些操作了

 

一,java字节码

提到 Java 字节码,可能很多人都不是很熟悉,大概都知道使用 javac 可以将 .java 文件编译成 .class 文件,.class 文件中存放的就是该 .java 文件对应的字节码内容,比如如下一段 DemoClass.java 代码很简单:

package com.equaker.demo.asm;

public class DemoClass {

    private int m;
    private static final String userName = "@EQuaker";

    private Integer age = 25;

    public int inc(){
        return ++m;
    }

    public static final String getUserName(){
        return userName;
    }
    public Integer getAge(){
        return age;
    }


    private String hi(String name){
        return "hi" + name;
    }

    public static final void hello(){
        System.out.println("hello");
        System.out.println("hello2");
    }

    public String dd(){
        return "dd";
    }


}

通过 javac 编译生成对应的 Demo.class 文件,使用纯文本文件打开 Demo.class,其中的内容是以 8 位字节为基础单位的二进制流,表面来看就是由十六进制符号组成的,这一段十六进制符号组成的长串是遵守 Java 虚拟机规范的。字符这里就不贴了。我们使用 javap -verbose DemoClass.class来看些我们勉强能看懂的吧。

F:\demo>javap -verbose DemoClass.class
Classfile /F:/demo/DemoClass.class
  Last modified 2021-1-14; size 1155 bytes
  MD5 checksum 46133fe2e9d345b28f020039cdfb039a
  Compiled from "DemoClass.java"
public class com.equaker.demo.asm.DemoClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #17.#41        // java/lang/Object."<init>":()V
   #2 = Methodref          #42.#43        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   #3 = Fieldref           #5.#44         // com/equaker/demo/asm/DemoClass.age:Ljava/lang/Integer;
   #4 = Fieldref           #5.#45         // com/equaker/demo/asm/DemoClass.m:I
   #5 = Class              #46            // com/equaker/demo/asm/DemoClass
   #6 = String             #47            // @EQuaker
   #7 = Class              #48            // java/lang/StringBuilder
   #8 = Methodref          #7.#41         // java/lang/StringBuilder."<init>":()V
   #9 = String             #35            // hi
  #10 = Methodref          #7.#49         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #11 = Methodref          #7.#50         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #12 = Fieldref           #51.#52        // java/lang/System.out:Ljava/io/PrintStream;
  #13 = String             #37            // hello
  #14 = Methodref          #53.#54        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #15 = String             #55            // hello2
  #16 = String             #38            // dd
  #17 = Class              #56            // java/lang/Object
  #18 = Utf8               m
  #19 = Utf8               I
  #20 = Utf8               userName
  #21 = Utf8               Ljava/lang/String;
  #22 = Utf8               ConstantValue
  #23 = Utf8               age
  #24 = Utf8               Ljava/lang/Integer;
  #25 = Utf8               <init>
  #26 = Utf8               ()V
  #27 = Utf8               Code
  #28 = Utf8               LineNumberTable
  #29 = Utf8               inc
  #30 = Utf8               ()I
  #31 = Utf8               getUserName
  #32 = Utf8               ()Ljava/lang/String;
  #33 = Utf8               getAge
  #34 = Utf8               ()Ljava/lang/Integer;
  #35 = Utf8               hi
  #36 = Utf8               (Ljava/lang/String;)Ljava/lang/String;
  #37 = Utf8               hello
  #38 = Utf8               dd
  #39 = Utf8               SourceFile
  #40 = Utf8               DemoClass.java
  #41 = NameAndType        #25:#26        // "<init>":()V
  #42 = Class              #57            // java/lang/Integer
  #43 = NameAndType        #58:#59        // valueOf:(I)Ljava/lang/Integer;
  #44 = NameAndType        #23:#24        // age:Ljava/lang/Integer;
  #45 = NameAndType        #18:#19        // m:I
  #46 = Utf8               com/equaker/demo/asm/DemoClass
  #47 = Utf8               @EQuaker
  #48 = Utf8               java/lang/StringBuilder
  #49 = NameAndType        #60:#61        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #50 = NameAndType        #62:#32        // toString:()Ljava/lang/String;
  #51 = Class              #63            // java/lang/System
  #52 = NameAndType        #64:#65        // out:Ljava/io/PrintStream;
  #53 = Class              #66            // java/io/PrintStream
  #54 = NameAndType        #67:#68        // println:(Ljava/lang/String;)V
  #55 = Utf8               hello2
  #56 = Utf8               java/lang/Object
  #57 = Utf8               java/lang/Integer
  #58 = Utf8               valueOf
  #59 = Utf8               (I)Ljava/lang/Integer;
  #60 = Utf8               append
  #61 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #62 = Utf8               toString
  #63 = Utf8               java/lang/System
  #64 = Utf8               out
  #65 = Utf8               Ljava/io/PrintStream;
  #66 = Utf8               java/io/PrintStream
  #67 = Utf8               println
  #68 = Utf8               (Ljava/lang/String;)V
{
  public com.equaker.demo.asm.DemoClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: bipush        25
         7: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        10: putfield      #3                  // Field age:Ljava/lang/Integer;
        13: return
      LineNumberTable:
        line 3: 0
        line 8: 4

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #4                  // Field m:I
         5: iconst_1
         6: iadd
         7: dup_x1
         8: putfield      #4                  // Field m:I
        11: ireturn
      LineNumberTable:
        line 11: 0

  public static final java.lang.String getUserName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #6                  // String @EQuaker
         2: areturn
      LineNumberTable:
        line 15: 0

  public java.lang.Integer getAge();
    descriptor: ()Ljava/lang/Integer;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #3                  // Field age:Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 18: 0

  public static final void hello();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #13                 // String hello
         5: invokevirtual #14                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        11: ldc           #15                 // String hello2
        13: invokevirtual #14                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        16: return
      LineNumberTable:
        line 27: 0
        line 28: 8
        line 29: 16

  public java.lang.String dd();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: ldc           #16                 // String dd
         2: areturn
      LineNumberTable:
        line 32: 0
}
SourceFile: "DemoClass.java"

从上图中,我们可以看到,.class 文件中主要有常量池、字段表、方法表和属性表等内容。访问控制权限(ACC_PUBLIC,ACC_PRIVATR...)。方法形参与返回值,例如String getName(String name);他的descriptor应该为(Ljava/lang/String;)Ljava/lang/String;方法内的code部分有点像汇编,就是需要加载数据进栈,返回这样的操作。具体可查询别人的文章《认识 .class 文件的字节码结构》。多看看就明白啦。

二,访问者模式 & ASM

ASM 库是一款基于 Java 字节码层面的代码分析和修改工具,那 ASM 和访问者模式有什么关系呢?访问者模式主要用于修改和操作一些数据结构比较稳定的数据,通过前面的学习,我们知道 .class 文件的结构是固定的,主要有常量池、字段表、方法表、属性表等内容,通过使用访问者模式在扫描 .class 文件中各个表的内容时,就可以修改这些内容了

ASM 库是 Visitor 模式的典型应用。

2.1,ASM中的几个重要类

在 ASM 库中存在以下几个重要的类:

  • ClassReader:它将字节数组或者 class 文件读入到内存当中,并以树的数据结构表示,树中的一个节点代表着 class 文件中的某个区域。可以将 ClassReader 看作是 Visitor 模式中的访问者的实现类
  • ClassVisitor(抽象类):ClassReader 对象创建之后,调用 ClassReader#accept() 方法,传入一个 ClassVisitor 对象。在 ClassReader 中遍历树结构的不同节点时会调用 ClassVisitor 对象中不同的 visit() 方法,从而实现对字节码的修改。在 ClassVisitor 中的一些访问会产生子过程,比如 visitMethod 会产生 MethodVisitor 的调用,visitField 会产生对 FieldVisitor 的调用,用户也可以对这些 Visitor 进行自己的实现,从而达到对这些子节点的字节码的访问和修改。
    在 ASM 的访问者模式中,用户还可以提供多种不同操作的 ClassVisitor 的实现,并以责任链的模式提供给 ClassReader 来使用,而 ClassReader 只需要 accept 责任链中的头节点处的 ClassVisitor。
  • ClassWriter:ClassWriter 是 ClassVisitor 的实现类,它是生成字节码的工具类,它一般是责任链中的最后一个节点,其之前的每一个 ClassVisitor 都是致力于对原始字节码做修改,而 ClassWriter 的操作则是老实得把每一个节点修改后的字节码输出为字节数组。

classWrite下面的几个方法:

  • void visitCode():表示 ASM 开始扫描这个方法
  • void onMethodEnter():进入这个方法
  • void onMethodExit():即将从这个方法出去
  • void onVisitEnd():表示方法扫码完毕

 

2.2,ASM的工作过程

ASM 大致的工作流程是:

  1. ClassReader 读取字节码到内存中,生成用于表示该字节码的内部表示的树,ClassReader 对应于访问者模式中的元素
  2. 组装 ClassVisitor 责任链,这一系列 ClassVisitor 完成了对字节码一系列不同的字节码修改工作,对应于访问者模式中的访问者 Visitor
  3. 然后调用 ClassReader#accept() 方法,传入 ClassVisitor 对象,此 ClassVisitor 是责任链的头结点,经过责任链中每一个 ClassVisitor 的对已加载进内存的字节码的树结构上的每个节点的访问和修改
  4. 最后,在责任链的末端,调用 ClassWriter 这个 visitor 进行修改后的字节码的输出工作

 

三,实战

讲太多心累,看的迷迷糊糊,那就上代码吧。

3.1,初一级

我们先来个简单点的吧,查看某个类的所有属性与方法。你可能第一个想到的是java.lang.reflect下面的反射工具。但这里我们用的是ASM哦。

首先我们肯定需要一个ClassReader 去读取某个class文件。然后再通过我们的观察者ClassVistor 去 。我们这里还是使用DemoClass.class 为例。

Asm_Test.java:

package com.equaker.demo.asm;

import org.objectweb.asm.*;

import java.io.FileInputStream;
import java.io.IOException;

public class Asm_Test {

    public static void main(String[] args) throws IOException {

        // 读取class文件
        ClassReader classReader = new ClassReader(new FileInputStream("F:\\demo\\DemoClass.class"));
        ClassVisitor classVisitor = new MyClassVisitor(Opcodes.ASM4);

        classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);

        System.out.println(classReader.getAccess());

    }

}

class MyClassVisitor extends ClassVisitor{


    public MyClassVisitor(int i) {
        super(i);
    }

// 重写了visitMethod方法。
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
        System.out.println("visitMethod- methodname: " + name + "; desc: " + desc);
        return methodVisitor;
    }
// 重写了visitField方法。
    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        FieldVisitor fieldVisitor = super.visitField(access, name, descriptor, signature, value);
        System.out.println("visitField- fieldname: " + name + "; descriptor: " + descriptor);
        return fieldVisitor;
    }
}

运行结果如下:

visitField- fieldname: m; descriptor: I
visitField- fieldname: userName; descriptor: Ljava/lang/String;
visitField- fieldname: age; descriptor: Ljava/lang/Integer;
visitMethod- methodname: <init>; desc: ()V
visitMethod- methodname: inc; desc: ()I
visitMethod- methodname: getUserName; desc: ()Ljava/lang/String;
visitMethod- methodname: getAge; desc: ()Ljava/lang/Integer;
visitMethod- methodname: hi; desc: (Ljava/lang/String;)Ljava/lang/String;
visitMethod- methodname: hello; desc: ()V
visitMethod- methodname: dd; desc: ()Ljava/lang/String;
33

3.2,初二级

既然我们使用classReader和classVisitor的观察者模式可以读取方法,变量等。而且当我们重写visitField和visitMethod时,发现他是需要返回的。这也就意味着我们可以改变这个类。

现在我们一次干两件事。修改m的控制权限为public,删除inc()方法。这里我们需要用到的就是ClassAdapter。这个类实现了ClassVisitor接口。功能预期差不多,尽管在ASM 4系列就已经去掉了ClassAdapter,把其功能融合到ClassVisitor里了。但是我们就写写啦,这些都不重要。

EditParamClassAdapter.java(修改属性控制权限):
package com.equaker.demo.asm;

import com.sun.xml.internal.ws.org.objectweb.asm.ClassAdapter;
import com.sun.xml.internal.ws.org.objectweb.asm.ClassVisitor;
import com.sun.xml.internal.ws.org.objectweb.asm.FieldVisitor;
import com.sun.xml.internal.ws.org.objectweb.asm.Opcodes;

/**
 * 将 m 的访问权限 改为 public
 *
 */
public class EditParamClassAdapter extends ClassAdapter {

    public EditParamClassAdapter(ClassVisitor cv) {
        super(cv);
    }
    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        System.out.println("visitField- fieldname: " + name + "; desc: " + desc);
        //如果属性名称为m,则控制昂问权限为public。
        if("m".equalsIgnoreCase(name)){
            access = Opcodes.ACC_PUBLIC;
        }

        FieldVisitor fieldVisitor = super.visitField(access, name, desc, signature, value);
        return fieldVisitor;
    }
}
DeleteMethodClassAdapter.java(删除inc方法):
package com.equaker.demo.asm;

import com.sun.xml.internal.ws.org.objectweb.asm.ClassAdapter;
import com.sun.xml.internal.ws.org.objectweb.asm.ClassVisitor;
import com.sun.xml.internal.ws.org.objectweb.asm.MethodVisitor;

/**
 *
 * 删除 inc() 方法
 *
 */
public class DeleteMethodClassAdapter extends ClassAdapter {
    public DeleteMethodClassAdapter(ClassVisitor cv) {
        super(cv);
    }


    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        System.out.println("visitMethod- methodname: " + name + "; desc: " + desc);
        //直接返回null就行了。
        if("inc".equalsIgnoreCase(name)){
            return null;
        }

        return super.visitMethod(access, name, desc, signature, exceptions);
    }
}

这里我们主要完成了在字节码文件的遍历过程中,操作字节码。下面我们试着生成新的class文件。

WriteAsm_Test.java:
package com.equaker.demo.asm;

import com.sun.xml.internal.ws.org.objectweb.asm.*;

import java.io.*;
import java.lang.reflect.Method;


public class WriteAsm_Test implements Opcodes {

    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {

        ClassReader classReader = new ClassReader(new FileInputStream("F:\\demo\\DemoClass.class"));

        //写class文件
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        ClassAdapter editParamClassAdapter = new EditParamClassAdapter(classWriter);
        ClassAdapter deleteMethodClassAdapter = new DeleteMethodClassAdapter(editParamClassAdapter);

        classReader.accept(deleteMethodClassAdapter, ClassReader.SKIP_DEBUG);

        byte[] bs = classWriter.toByteArray();

        OutputStream outputStream = new FileOutputStream("F:\\demo\\DemoClass2.class");
        outputStream.write(bs);
        outputStream.flush();
    }
}

这里有个东西需要注意一下。我么声明了两个Adapter去设计字节码的修改。这里面存在一个引用链的问题。读者可以仔细看一下我的声明顺序以及利用构造器进行依赖传递。

运行结果:我们把生成的class文件放在idea里面 就可以看到结果了。不出意外的话应该没问题。

3.3,初三级

你有没有想过从无到有生成一个Class?希望你想过。。。

我们先来看看我们的目标类吧。

TestClass.java:

package com.equaker.demo.asm;

public class TestClass {
    public static final String userName = "@EQuaker";
    public Integer age;

    public static final String getUserName() {
        System.out.println("this is getUserName() method");
        System.out.println("this is hello World");
        return "这是静态将变量(在类加载解析阶段由符号引用转为直接引用,所以不牵扯 getfield操作)";
    }

    public Integer getAge() {
        return this.age;
    }
}

当我写到这里的时候,我犹豫了半天,该咋写,因为东西实在是太多了。大家可以看到我定义的两个成员变量类型的区别。其实光是这里都大有文章。慢慢来,慢慢来,慢慢来。。。

我们利用ASM框架生成字节码class的时候主要用的就是ClassWrite的visitMethod 和visitFiled。这个可以帮助我们去构建。代码里面细说。

WriteAsm_Test.java:
package com.equaker.demo.asm;

import com.sun.xml.internal.ws.org.objectweb.asm.*;

import java.io.*;
import java.lang.reflect.Method;


public class WriteAsm_Test implements Opcodes {

    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {

        ClassReader classReader = new ClassReader(new FileInputStream("F:\\demo\\DemoClass.class"));

        //声明一个ClassWriter
        ClassWriter classWriter2 = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        //classWriter2.newClass("TestClass");
        //classWriter2.newMethod("TestClass", "testMethod", "()V", false);

        //参数依次为:JDK版本52(1.8),类控制访问权限为public,继承Object, 没有泛型, 不是接口
        classWriter2.visit(52, Opcodes.ACC_PUBLIC|Opcodes.ACC_SUPER, "com/equaker/demo/asm/TestClass", null, "java/lang/Object", null);

        //新增一个构造器 <init> ()V 表示狗构造器
//        MethodVisitor constructMethodVisitor = classWriter2.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
//        constructMethodVisitor.visitCode();
//        constructMethodVisitor.visitEnd();

        //新增一个public static final String userName = "@EQuaker" 字段
        FieldVisitor nameFieldVisitor = classWriter2.visitField(Opcodes.ACC_PUBLIC|Opcodes.ACC_STATIC|Opcodes.ACC_FINAL, "userName", "Ljava/lang/String;", null, "@EQuaker");
        nameFieldVisitor.visitEnd();
        //新增一个public Integer age 字段
        FieldVisitor ageFieldVisitor = classWriter2.visitField(Opcodes.ACC_PUBLIC, "age", "Ljava/lang/Integer;", null, 25);
        ageFieldVisitor.visitEnd();

        //新增一个static final string getUserName方法
        MethodVisitor testMethodVisitor = classWriter2.visitMethod(Opcodes.ACC_PUBLIC|Opcodes.ACC_STATIC|Opcodes.ACC_FINAL, "getUserName", "()Ljava/lang/String;", null, null);
        testMethodVisitor.visitCode();
        //新增方法内代码 System.out.Println("this is getUserName() method")
        testMethodVisitor.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System", "out","Ljava/io/PrintStream;");
        testMethodVisitor.visitLdcInsn("this is getUserName() method");
        testMethodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");

        //新增方法内代码 System.out.Println("this is hello World")
        testMethodVisitor.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System", "out","Ljava/io/PrintStream;");
        testMethodVisitor.visitLdcInsn("this is hello World");
        testMethodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");

         //新增方法内代码 return userName;
        //由于usernam 是 静态最终型的,所以再编译期间已经由符号引用转为直接引用
        // 这一点需要和实例变量区分
        testMethodVisitor.visitLdcInsn("这是静态将变量(在类加载解析阶段由符号引用转为直接引用,所以不牵扯 getfield操作)");
        testMethodVisitor.visitInsn(Opcodes.ARETURN);

//        testMethodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
//        testMethodVisitor.visitFieldInsn(Opcodes.GETFIELD, "com/equaker/demo/asm/TestClass", "userName", "Ljava/lang/String;");
        
        //参数一次为: 最大栈大小, 最大本地变量个数。想都不用想 1》2
        testMethodVisitor.visitMaxs(2,1);
        testMethodVisitor.visitEnd();


        // 新增 getAge()方法
        MethodVisitor agetMethodVisitor = classWriter2.visitMethod(Opcodes.ACC_PUBLIC, "getAge", "()Ljava/lang/Integer;", null, null);
        agetMethodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
        agetMethodVisitor.visitFieldInsn(Opcodes.GETFIELD, "com/equaker/demo/asm/TestClass", "age", "Ljava/lang/Integer;");
        //testMethodVisitor.visitLdcInsn(22);
        agetMethodVisitor.visitInsn(Opcodes.ARETURN);
        agetMethodVisitor.visitMaxs(2, 1);
        agetMethodVisitor.visitEnd();

        byte[] bs2 = classWriter2.toByteArray();
        OutputStream outputStream2 = new FileOutputStream("F:\\soyuan_workspace_study\\equaker-demo\\src\\test\\java\\com\\equaker\\demo\\asm\\TestClass.class");
        outputStream2.write(bs2);
        outputStream2.flush();

    }

}

大家看到这里,不知道有没有一丝眼熟,有没有想起我开篇介绍的通过javap -verbose DemoClass.class文件结构。现在可以去看看两者之间的关系了。无论是声明变量还是声明方法。我们使用的都是全路径。这里我们挑几个说说吧。

加入方法是public static final String getName(String name){}。那我们的access应该就是:Opcodes.ACC_PUBLIC|Opcodes.ACC_STATIC|Opcodes.ACC_FINAL。方法名:getName。方法描述descriptor: 

(Ljava/lang/String;)Ljava/lang/String;。。。。。应该不难理解。

还有关于业务代码的问题比如System.out.println("");看似简单一行,实际上我们需要先声明一个java/io/PrintStream类型的变量,然后在创建变量(即打印内容),最后在执行反射执行代码。意会。。。

我们再来看一个问题,大家再仔细看看我上面DemoClass.class 通过javap贴出来的内容。为了方便,我再贴一遍。

F:\demo>javap -verbose DemoClass.class
Classfile /F:/demo/DemoClass.class
  Last modified 2021-1-14; size 1155 bytes
  MD5 checksum 46133fe2e9d345b28f020039cdfb039a
  Compiled from "DemoClass.java"
public class com.equaker.demo.asm.DemoClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #17.#41        // java/lang/Object."<init>":()V
   #2 = Methodref          #42.#43        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   #3 = Fieldref           #5.#44         // com/equaker/demo/asm/DemoClass.age:Ljava/lang/Integer;
   #4 = Fieldref           #5.#45         // com/equaker/demo/asm/DemoClass.m:I
   #5 = Class              #46            // com/equaker/demo/asm/DemoClass
   #6 = String             #47            // @EQuaker
   #7 = Class              #48            // java/lang/StringBuilder
   #8 = Methodref          #7.#41         // java/lang/StringBuilder."<init>":()V
   #9 = String             #35            // hi
  #10 = Methodref          #7.#49         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #11 = Methodref          #7.#50         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #12 = Fieldref           #51.#52        // java/lang/System.out:Ljava/io/PrintStream;
  #13 = String             #37            // hello
  #14 = Methodref          #53.#54        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #15 = String             #55            // hello2
  #16 = String             #38            // dd
  #17 = Class              #56            // java/lang/Object
  #18 = Utf8               m
  #19 = Utf8               I
  #20 = Utf8               userName
  #21 = Utf8               Ljava/lang/String;
  #22 = Utf8               ConstantValue
  #23 = Utf8               age
  #24 = Utf8               Ljava/lang/Integer;
  #25 = Utf8               <init>
  #26 = Utf8               ()V
  #27 = Utf8               Code
  #28 = Utf8               LineNumberTable
  #29 = Utf8               inc
  #30 = Utf8               ()I
  #31 = Utf8               getUserName
  #32 = Utf8               ()Ljava/lang/String;
  #33 = Utf8               getAge
  #34 = Utf8               ()Ljava/lang/Integer;
  #35 = Utf8               hi
  #36 = Utf8               (Ljava/lang/String;)Ljava/lang/String;
  #37 = Utf8               hello
  #38 = Utf8               dd
  #39 = Utf8               SourceFile
  #40 = Utf8               DemoClass.java
  #41 = NameAndType        #25:#26        // "<init>":()V
  #42 = Class              #57            // java/lang/Integer
  #43 = NameAndType        #58:#59        // valueOf:(I)Ljava/lang/Integer;
  #44 = NameAndType        #23:#24        // age:Ljava/lang/Integer;
  #45 = NameAndType        #18:#19        // m:I
  #46 = Utf8               com/equaker/demo/asm/DemoClass
  #47 = Utf8               @EQuaker
  #48 = Utf8               java/lang/StringBuilder
  #49 = NameAndType        #60:#61        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #50 = NameAndType        #62:#32        // toString:()Ljava/lang/String;
  #51 = Class              #63            // java/lang/System
  #52 = NameAndType        #64:#65        // out:Ljava/io/PrintStream;
  #53 = Class              #66            // java/io/PrintStream
  #54 = NameAndType        #67:#68        // println:(Ljava/lang/String;)V
  #55 = Utf8               hello2
  #56 = Utf8               java/lang/Object
  #57 = Utf8               java/lang/Integer
  #58 = Utf8               valueOf
  #59 = Utf8               (I)Ljava/lang/Integer;
  #60 = Utf8               append
  #61 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #62 = Utf8               toString
  #63 = Utf8               java/lang/System
  #64 = Utf8               out
  #65 = Utf8               Ljava/io/PrintStream;
  #66 = Utf8               java/io/PrintStream
  #67 = Utf8               println
  #68 = Utf8               (Ljava/lang/String;)V
{
  public com.equaker.demo.asm.DemoClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: bipush        25
         7: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        10: putfield      #3                  // Field age:Ljava/lang/Integer;
        13: return
      LineNumberTable:
        line 3: 0
        line 8: 4

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #4                  // Field m:I
         5: iconst_1
         6: iadd
         7: dup_x1
         8: putfield      #4                  // Field m:I
        11: ireturn
      LineNumberTable:
        line 11: 0

  public static final java.lang.String getUserName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #6                  // String @EQuaker
         2: areturn
      LineNumberTable:
        line 15: 0

  public java.lang.Integer getAge();
    descriptor: ()Ljava/lang/Integer;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #3                  // Field age:Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 18: 0

  public static final void hello();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #13                 // String hello
         5: invokevirtual #14                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        11: ldc           #15                 // String hello2
        13: invokevirtual #14                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        16: return
      LineNumberTable:
        line 27: 0
        line 28: 8
        line 29: 16

  public java.lang.String dd();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: ldc           #16                 // String dd
         2: areturn
      LineNumberTable:
        line 32: 0
}
SourceFile: "DemoClass.java"

你有没有发现关于getUsername和getAge在code部分的代码不太一样。在getAge里面我们先Aload一个栈空间,然后通过getField操作获取age变量,最后执行返回。而在getUserName里面缺没有涉及aload和getfield操作,而是直接声明了一个本地栈变量(ldc)。如果发现了,说明你很棒哦,,,

其实这里面的知识点是关于类加载方面的。我们都知道类加载器的过程主要有五步:加载,验证,准备,解析,初始化。而在初始化的时候进行的操作就是符号引用转变为直接应用。由于是静态最终型,所以在方法体内直接引入内容@EQuaker 而不是地址引用。具体参考我的另一篇文章《Java类加载,垃圾收集》

 

3.4,初三留级-复读

既然都有字节码了,你有没有想过去执行这个class里面的某个方法???你可能一脸懵逼,我TM连代码都没有。先抛出我的答案:类加载器+反射

大家可能都知道类加载器是用来加载类的,可是真的需要在什么时候使用呢?这不,机会来了。。。

MyClassLoader.java:

package com.equaker.demo.asm;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;

public class MyCLassLoader extends ClassLoader {

    public MyCLassLoader(){
        super();
    }
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return super.loadClass(name);
    }
     //全路径名需要换成自己的,我这里省事了。。。
    public Class<?> defineMyClass(byte[] b, int off, int len){
        return super.defineClass("com.equaker.demo.asm.TestClass", b, off, len);
    }

}

反射代码我们直接在上一步的WriteAsm_Test.java 追加。

package com.equaker.demo.asm;

import com.sun.xml.internal.ws.org.objectweb.asm.*;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;


public class WriteAsm_Test implements Opcodes {

    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException {
        //。。。代码在上面
        MyCLassLoader classLoader = new MyCLassLoader();
        Class clazz = classLoader.defineMyClass(bs2, 0, bs2.length);


        Method[] methods = clazz.getDeclaredMethods();
        for(Method method : methods){
            System.out.println("方法名称: "+method.getName());

            if("getUserName".equalsIgnoreCase(method.getName())){
                Object ret = method.invoke(clazz, null);
                System.out.println("ret:" + ret);
            }

        }

    }


}

as you see:

visitField- fieldname: m; desc: I
visitField- fieldname: userName; desc: Ljava/lang/String;
visitField- fieldname: age; desc: Ljava/lang/Integer;
visitMethod- methodname: <init>; desc: ()V
visitMethod- methodname: inc; desc: ()I
visitMethod- methodname: getUserName; desc: ()Ljava/lang/String;
visitMethod- methodname: getAge; desc: ()Ljava/lang/Integer;
visitMethod- methodname: hi; desc: (Ljava/lang/String;)Ljava/lang/String;
visitMethod- methodname: hello; desc: ()V
visitMethod- methodname: dd; desc: ()Ljava/lang/String;
方法名称: getAge
方法名称: getUserName
this is getUserName() method
this is hello World
ret:这是静态将变量(在类加载解析阶段由符号引用转为直接引用,所以不牵扯 getfield操作)

讲完这些,初中该学的东西差不多了,大家感到困惑的点可能就是 javap里面的那个代码该咋写。推荐个神器 ASMifer。 idea可以安装插件 bytecode outline。

截两张图给大家。

图一,

图二,

你没看错,他帮你写好代码了。实际开发中我们参考api就可以了。

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值