ASM

2022/07/16

ASM简介

简介

通用的Java字节码操作和分析框架,指令层次上操作字节码,相对于Javassist更好的性能以及更高的灵活性。

ASM处理字节码数据的思路是这样的:第一步,将.class文件拆分成多个部分;第二步,对某一个部分的信息进行修改;第三步,将多个部分重新组织成一个新的.class文件

更多参考官网:ASM (ow2.io)

ASM与Agent

Java ASM是一个操作字节码的工具(tool),而Java Agent提供了修改字节码的机会(opportunity)。 想像这样一个场景: 有一个JVM正在运行,突然Java Agent在JVM上打开一扇大门,Java ASM通过大门冲进JVM里面,就要开始修改字节码了。

.class ---> Java ASM ---> Java Agent ---> JVM

ASM组成

从组成结构上来说,Java ASM有Core API和Tree API两部分组成。

Core API是基础,而Tree API是在Core API的这个基础上构建起来的

                                   ┌─── asm.jar
                                   
            ┌─── Core API ─────────┼─── asm-util.jar
                                  
                                  └─── asm-commons.jar
Java ASM ───┤
            
                                  ┌─── asm-tree.jar
            └─── Tree API ─────────┤
                                   └─── asm-analysis.jar

ASM修改类的两种方法就是基于事件触发的Core API和基于对象的Tree API

可以对比XML文件解析的方式:SAX模式和DOM模式;每种模式都有自己的优缺点:

想要了解更多可以阅读:asm4-guide

Core API

访问者模式

ASM对字节码的操作和分析都是基于访问者模式来实现的;

访问者模式建议将新行为放入一个名为访问者的独立类中, 而不是试图将其整合到已有类中。

更多参考:访问者设计模式

源码学习

这里使用asm-all的`5.1版本。(IDEA有问题,所以下载的jar包,用Jd-gui)

<!-- https://mvnrepository.com/artifact/org.ow2.asm/asm-all -->
<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm-all</artifactId>
    <version>5.1</version>
</dependency>

image-20230313195546140

图中可以看到有以下几部分:

Core API最重要的就是ClassReader、ClassVisitor和ClassWriter这三个类

image-20230313191332466

ClassReader

读取.class文件,转换成 ClassVisitor 能访问的结构。父类是Object类。

public class ClassReader {
    //第1组,真实的数据部分
    final byte[] classFileBuffer;
    ...
    //第2组,数据的索引信息
    private final int[] cpInfoOffsets;
    public final int header;
}

这3个字段能够体现出ClassReader类处理.class文件的整体思路:

拿到classFileBuffer字段后,一个主要目的就是对它的内容进行修改,来实现一个新的功能。它处理的大体思路是这样的:

.class文件 --> ClassReader --> byte[] --> 经过各种转换 --> ClassWriter --> byte[] --> .class文件
  1. 一个.class文件存储于磁盘的某个位置;
  2. 使用ClassReader类将这个.class文件的内容读取,以byte[]的形式存储到classFileBuffer字段中;
  3. 增加某些功能,就对这些原始内容(byte[])进行转换;
  4. 转换都完成之后,再交给ClassWriter类处理,调用它的toByteArray()方法,从而得到新的内容(byte[]);
  5. 将新生成的内容(byte[])存储到一个具体的.class文件中,那么这个新的.class文件就具备了一些新的功能。

它有四个构造函数,分别支持 byte[]InputStreamFile Path 三种输入方式,这三种输入类型的构造方法如下:

其中较为重要的方法有getXxx()方法,来读取 Class 文件信息

还有accept() 方法,该方法中接收一个 ClassVisitor 对象,它是ClassReader和ClassVisitor进行连接的“桥梁”。accept()方法的代码逻辑就是按照一定的顺序来调用ClassVisitor当中的visitXxx()方法。

ClassVisitor类

是一个抽象类,使用时必须继承,两个比较常见的子类就是ClassWriter类(Core API)和ClassNode类(Tree API)

ClassVisitor类定义的字段有如下两个:

public abstract class ClassVisitor {
    protected final int api;
    protected ClassVisitor cv;
}

image-20230313205123774

ClassVisitor类有两个构造方法:

public ClassVisitor(final int api) {
        this(api, null);
    }

    public ClassVisitor(final int api, final ClassVisitor classVisitor) {
        this.api = api;
        this.cv = classVisitor;
    }

ASM中使用了Visitor Pattern(访问者模式),所以ClassVisitor当中许多的visitXxx()方法。我们需要关注的有4个:visit()visitField()visitMethod()visitEnd()

上面多个visitXxx()方法遵循一定的调用顺序。这个调用顺序,是参考自ClassVisitor类的API文档。简化后为以下顺序:

visit()--->visitField()visitMethod()--->visitEnd()

visitXxx()方法目的就是为了生成一个合法的.class文件,所以这些visitXxx()方法与ClassFile的结构密切相关。例如visit()方法

public void visit(
    final int version,
    final int access,
    final String name,
    final String signature,
    final String superName,
    final String[] interfaces);

image-20230313210538824

剩下的查看官方文档

ClassWriter

ClassWriter的父类是ClassVisitor,因此ClassWriter类继承了visitxxx()方法

public class ClassWriter extends ClassVisitor {
}

ClassWriter定义的字段有很多,也是与.class文件结构密切相关

ClassWriter定义的构造方法有两个,这里只关注其中一个,也就是只接收一个int类型参数的构造方法。在使用new关键字创建ClassWriter对象时,推荐使用COMPUTE_FRAMES参数

public class ClassWriter extends ClassVisitor {

    public static final int COMPUTE_MAXS = 1;
    public static final int COMPUTE_FRAMES = 2;
   
    public ClassWriter(final int flags) {
        this(null, flags);
    }
}

ClassWriter提供的方法,同样重点关注visitXxx()方法的调用。另一个方法就是toByteArray()方法,这个方法的作用是将“所有的努力”(对visitXxx()的调用)转换成byte[],而这些byte[]的内容就遵循ClassFile结构。

使用ClassWriter生成一个Class文件,可以大致分成三个步骤:

public class HelloWorldGenerateCore {
    public static byte[] dump () throws Exception {
        // (1) 创建ClassWriter对象
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        // (2) 调用visitXxx()方法
        cw.visit();
        cw.visitField();
        cw.visitMethod();
        cw.visitEnd();       // 注意,最后要调用visitEnd()方法

        // (3) 调用toByteArray()方法
        byte[] bytes = cw.toByteArray();
        return bytes;
    }
}

Transformation(对类进行转换)

使用上面三个类进行转换的思路时这样的,

ClassReader --> ClassVisitor(1) --> ... --> ClassVisitor(N) --> ClassWriter

ClassReader类负责“读”,ClassWriter负责“写”,而ClassVisitor则负责进行“转换”(Transformation)

image-20230314144317659

借用别人的一张图来描述这个过程很形象。

两个问题:

  1. 三个ClassReader、ClassVisitor和ClassWriter三个类的实例之间,是如何建立联系的?
  2. 在执行代码的时候(程序开始运行),类内部visitXxx()方法的调用顺序是什么样的?

进行Class Transformation操作时,一般是这样写代码的,很容易理解代码是怎么串起来的

public class HelloWorldTransformCore {
    public static void main(String[] args) {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);
        byte[] bytes1 = FileUtils.readBytes(filepath);

        //(1)构建ClassReader
        ClassReader cr = new ClassReader(bytes1);

        //(2)构建ClassWriter
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        //(3)串连ClassVisitor
        int api = Opcodes.ASM9;
        ClassVisitor cv1 = new ClassVisitor1(api, cw);
        ClassVisitor cv2 = new ClassVisitor2(api, cv1);
        // ...
        ClassVisitor cvn = new ClassVisitorN(api, cvm);

        //(4)结合ClassReader和ClassVisitor
        int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
        cr.accept(cvn, parsingOptions);

        //(5)生成byte[]
        byte[] bytes2 = cw.toByteArray();

        FileUtils.writeBytes(filepath, bytes2);
    }
}

ClassReader类与ClassVisitor类建立联系是通过ClassReader.accept()方法实现的。

多个ClassVisitor类建立初步联系是通过构造方法来实现的,建立后续联系,是通过visitXxx()方法来实现的:

ClassWriter类是继承自ClassVisitor类,所以两者建立联系和上一步类似。


package org.gk0d.asm;

public class Demo {
    public void test(){
        System.out.println("Hello,gk0d");
    }
}

尝试读取上面的Demo.class文件,然后将test方法的开始和结束加上字符串输出,将其增强为以下状态:

public class Demo {
  public void test(){
    System.out.println("Start");
    System.out.println("Hello,gk0d");
    System.out.println("End");
  }
}

最重要的是自定义一个ClassVisitor用来定义事件,也就是来修改test方法:

package org.gk0d.asm;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class DemoClassVisitor extends ClassVisitor implements Opcodes {

    public DemoClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        // 当方法名为test时候进行修改
        if(name.equals("test")){
            mv = new TestMethodVisitor(mv);
        }
        return mv;
    }

    class TestMethodVisitor extends MethodVisitor implements Opcodes{

        public TestMethodVisitor(MethodVisitor mv) {
            super(Opcodes.ASM5, mv);
        }

        @Override
        public void visitCode() {
            super.visitCode();
            // 在开始扫描code区时 即方法开始时添加方法调用System.out.println("Start");
            // 首先System.out是一个field: public final static PrintStream out = null;
            mv.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");
            // 将字符串常量加载到栈
            mv.visitLdcInsn("Start");
            // 调用println:  public void println(String x)
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V",false);
        }

        @Override
        public void visitInsn(int opcode) {
            //    int IRETURN = 172; // visitInsn
            //    int LRETURN = 173; // -
            //    int FRETURN = 174; // -
            //    int DRETURN = 175; // -
            //    int ARETURN = 176; // -
            //    int RETURN = 177; // -
            // 判断opcode是否处于结束状态,return或者抛出异常的情况,在结束前添加字节码
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW){
                mv.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");
                mv.visitLdcInsn("End");
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V",false);

            }
            super.visitInsn(opcode);
        }
    }
}

通过ClassReader、ClassVisitor、ClassWriter来实现具体的修改,

package org.gk0d.asm;


import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import java.io.*;

public class Test {
    public static void main(String[] args) throws IOException {
        //读取读取class
        ClassReader classReader = new ClassReader("org.gk0d.asm.Demo");
        //构建ClassWriter
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        //调用自定义的ClassVisitor
        ClassVisitor classVisitor = new DemoClassVisitor(classWriter);
        //结合ClassReader和ClassVisitor
        classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
        // 获取字节码的byte数组
        byte[] bytes = classWriter.toByteArray();

        FileOutputStream fileOutputStream = new FileOutputStream(new File("D:\\Project\\JavaProject\\ASM_Demo\\src\\main\\java\\org\\gk0d\\asm\\Demo.class"));
        fileOutputStream.write(bytes);
        fileOutputStream.close();
        
        System.out.println("修改成功");

    }

}

image-20230314155101815

image-20230314155210716

小结

image-20230313200855113

ASM往往在一些框架的底层起着重要的作用。介绍两个关于ASM的应用场景:SpringJDK

Spring

Spring框架当中的AOP是依赖于ASM的,具体来说,Spring的AOP,可以通过JDK的动态代理来实现,也可以通过CGLIB实现。其中,CGLib 是在ASM的基础上构建起来的,所以,Spring AOP是间接的使用了ASM。

JDK

Java 8中引入了一个非常重要的特性,就是支持Lambda表达式。Lambda表达式,允许把方法作为参数进行传递,它能够使代码变的更加简洁紧凑,在Java 8版本,Lambda表达式的调用是通过ASM来实现的。

rt.jar文件的jdk.internal.org.objectweb.asm包当中,就包含了JDK内置的ASM代码。在JDK 8版本当中,它使用了ASM 5.0版

XXX

上面只写了一部分,ASM中还有许多关键的类,但由于篇幅问题,也就不一一介绍了。

只要把字节码结构搞懂,再一一对照ASM给出的各种类就很好理解了。

参考文档

ASM (ow2.io)