本文属于Java ASM系列一:Core API当中的一篇。


对于《Java ASM系列一:Core API》有配套的视频讲解,可以点击这里这里进行查看;同时,也可以点击这里查看源码资料。


1. Cla***eader类

Cla***eader类和ClassWriter类,从功能角度来说,是完全相反的两个类,一个用于读取.class文件,另一个用于生成.class文件。

1.1 class info

第一个部分,Cla***eader的父类是Object类。与ClassWriter类不同的是,Cla***eader类并没有继承自ClassVisitor类。

Cla***eader类的定义如下:

public class Cla***eader {
}

ClassWriter类的定义如下:

public class ClassWriter extends ClassVisitor {
}

1.2 fields

第二个部分,Cla***eader类定义的字段有哪些。我们选取出其中的3个字段进行介绍,即classFileBuffer字段、cpInfoOffsets字段和header字段。

public class Cla***eader {
    //第1组,真实的数据部分
    final byte[] classFileBuffer;

    //第2组,数据的索引信息
    private final int[] cpInfoOffsets;
    public final int header;
}

为什么选择这3个字段呢?因为这3个字段能够体现出Cla***eader类处理.class文件的整体思路:

  • 第1组,classFileBuffer字段:它里面包含的信息,就是从.class文件中读取出来的字节码数据。
  • 第2组,cpInfoOffsets字段和header字段:它们分别标识了classFileBuffer中数据里包含的常量池(constant pool)和访问标识(access flag)的位置信息。

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

.class文件 --> Cla***eader --> byte[] --> 经过各种转换 --> ClassWriter --> byte[] --> .class文件
  • 第一,从一个.class文件(例如HelloWorld.class)开始,它可能存储于磁盘的某个位置;
  • 第二,使用Cla***eader类将这个.class文件的内容读取出来,其实这些内容(byte[])就是Cla***eader对象中的classFileBuffer字段的内容;
  • 第三,为了增加某些功能,就对这些原始内容(byte[])进行转换;
  • 第四,等各种转换都完成之后,再交给ClassWriter类处理,调用它的toByteArray()方法,从而得到新的内容(byte[]);
  • 第五,将新生成的内容(byte[])存储到一个具体的.class文件中,那么这个新的.class文件就具备了一些新的功能。

1.3 constructors

第三个部分,Cla***eader类定义的构造方法有哪些。在Cla***eader类当中定义了5个构造方法。但是,从本质上来说,这5个构造方法本质上是同一个构造方法的不同表现形式。其中,最常用的构造方法有两个:

  • 第一个是Cla***eader cr = new Cla***eader("sample.HelloWorld");
  • 第二个是Cla***eader cr = new Cla***eader(bytes);
public class Cla***eader {

    public Cla***eader(final String className) throws IOException { // 第一个构造方法(常用)
        this(
            readStream(ClassLoader.getSystemResourceAsStream(className.replace('.', '/') + ".class"), true)
        );
    }

    public Cla***eader(final byte[] classFile) { // 第二个构造方法(常用)
        this(classFile, 0, classFile.length);
    }

    public Cla***eader(final byte[] classFileBuffer, final int classFileOffset, final int classFileLength) {
        this(classFileBuffer, classFileOffset, true);
    }

    Cla***eader( // 这是最根本、最本质的构造方法
        final byte[] classFileBuffer,
        final int classFileOffset,
        final boolean checkClassVersion) {
        // ......
    }

    private static byte[] readStream(final InputStream inputStream, final boolean close) throws IOException {
        if (inputStream == null) {
            throw new IOException("Class not found");
        }
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            byte[] data = new byte[INPUT_STREAM_DATA_CHUNK_SIZE];
            int bytesRead;
            while ((bytesRead = inputStream.read(data, 0, data.length)) != -1) {
                outputStream.write(data, 0, bytesRead);
            }
            outputStream.flush();
            return outputStream.toByteArray();
        } finally {
            if (close) {
                inputStream.close();
            }
        }
    }
}

所有构造方法,本质上都执行下面的逻辑:

public class Cla***eader {
    Cla***eader(final byte[] classFileBuffer, final int classFileOffset, final boolean checkClassVersion) {
        this.classFileBuffer = classFileBuffer;

        // Check the class' major_version.
        // This field is after the magic and minor_version fields, which use 4 and 2 bytes respectively.
        if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V16) {
            throw new IllegalArgumentException("Unsupported class file major version " + readShort(classFileOffset + 6));
        }

        // Create the constant pool arrays.
        // The constant_pool_count field is after the magic, minor_version and major_version fields,
        // which use 4, 2 and 2 bytes respectively.
        int constantPoolCount = readUnsignedShort(classFileOffset + 8);
        cpInfoOffsets = new int[constantPoolCount];

        // Compute the offset of each constant pool entry,
        // as well as a conservative estimate of the maximum length of the constant pool strings.
        // The first constant pool entry is after the magic, minor_version, major_version and constant_pool_count fields,
        // which use 4, 2, 2 and 2 bytes respectively.
        int currentCpInfoIndex = 1;
        int currentCpInfoOffset = classFileOffset + 10;

        // The offset of the other entries depend on the total size of all the previous entries.
        while (currentCpInfoIndex < constantPoolCount) {
            cpInfoOffsets[currentCpInfoIndex++] = currentCpInfoOffset + 1;
            int cpInfoSize;
            switch (classFileBuffer[currentCpInfoOffset]) {
                case Symbol.CONSTANT_FIELDREF_TAG:
                case Symbol.CONSTANT_METHODREF_TAG:
                case Symbol.CONSTANT_INTERFACE_METHODREF_TAG:
                case Symbol.CONSTANT_INTEGER_TAG:
                case Symbol.CONSTANT_FLOAT_TAG:
                case Symbol.CONSTANT_NAME_AND_TYPE_TAG:
                    cpInfoSize = 5;
                    break;
                case Symbol.CONSTANT_DYNAMIC_TAG:
                    cpInfoSize = 5;
                    break;
                case Symbol.CONSTANT_INVOKE_DYNAMIC_TAG:
                    cpInfoSize = 5;
                    break;
                case Symbol.CONSTANT_LONG_TAG:
                case Symbol.CONSTANT_DOUBLE_TAG:
                    cpInfoSize = 9;
                    currentCpInfoIndex++;
                    break;
                case Symbol.CONSTANT_UTF8_TAG:
                    cpInfoSize = 3 + readUnsignedShort(currentCpInfoOffset + 1);
                    break;
                case Symbol.CONSTANT_METHOD_HANDLE_TAG:
                    cpInfoSize = 4;
                    break;
                case Symbol.CONSTANT_CLASS_TAG:
                case Symbol.CONSTANT_STRING_TAG:
                case Symbol.CONSTANT_METHOD_TYPE_TAG:
                case Symbol.CONSTANT_PACKAGE_TAG:
                case Symbol.CONSTANT_MODULE_TAG:
                    cpInfoSize = 3;
                    break;
                default:
                    throw new IllegalArgumentException();
            }
            currentCpInfoOffset += cpInfoSize;
        }

        // The Classfile's access_flags field is just after the last constant pool entry.
        header = currentCpInfoOffset;
    }
}

上面的代码,要结合ClassFile的结构进行理解:

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

1.4 methods

第四个部分,Cla***eader类定义的方法有哪些。

1.4.1 getXxx()方法

这里介绍的几个getXxx()方法,都是在header字段的基础上获得的:

public class Cla***eader {
    public int getAccess() {
        return readUnsignedShort(header);
    }

    public String getClassName() {
        // this_class is just after the access_flags field (using 2 bytes).
        return readClass(header + 2, new char[maxStringLength]);
    }

    public String getSuperName() {
        // super_class is after the access_flags and this_class fields (2 bytes each).
        return readClass(header + 4, new char[maxStringLength]);
    }

    public String[] getInterfaces() {
        // interfaces_count is after the access_flags, this_class and super_class fields (2 bytes each).
        int currentOffset = header + 6;
        int interfacesCount = readUnsignedShort(currentOffset);
        String[] interfaces = new String[interfacesCount];
        if (interfacesCount > 0) {
            char[] charBuffer = new char[maxStringLength];
            for (int i = 0; i < interfacesCount; ++i) {
                currentOffset += 2;
                interfaces[i] = readClass(currentOffset, charBuffer);
            }
        }
        return interfaces;
    }
}

同样,上面的几个getXxx()方法也需要参考ClassFile结构来理解:

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

假如,有如下一个类:

import java.io.Serializable;

public class HelloWorld extends Exception implements Serializable, Cloneable {

}

我们可以使用Cla***eader类中的getXxx()方法来获取相应的信息:

import lsieun.utils.FileUtils;
import org.objectweb.asm.Cla***eader;

import java.util.Arrays;

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

        //(1)构建Cla***eader
        Cla***eader cr = new Cla***eader(bytes);

        // (2) 调用getXxx()方法
        int access = cr.getAccess();
        System.out.println("access: " + access);

        String className = cr.getClassName();
        System.out.println("className: " + className);

        String superName = cr.getSuperName();
        System.out.println("superName: " + superName);

        String[] interfaces = cr.getInterfaces();
        System.out.println("interfaces: " + Arrays.toString(interfaces));
    }
}

输出结果:

access: 33
className: sample/HelloWorld
superName: java/lang/Exception
interfaces: [java/io/Serializable, java/lang/Cloneable]

1.4.2 accept()方法

Cla***eader类当中,有一个accept()方法,这个方法接收一个ClassVisitor类型的参数,因此accept()方法是将Cla***eaderClassVisitor进行连接的“桥梁”。accept()方法的代码逻辑就是按照一定的顺序来调用ClassVisitor当中的visitXxx()方法。

public class Cla***eader {
    // A flag to skip the Code attributes.
    public static final int SKIP_CODE = 1;

    // A flag to skip the SourceFile, SourceDebugExtension,
    // LocalVariableTable, LocalVariableTypeTable,
    // LineNumberTable and MethodParameters attributes.
    public static final int SKIP_DEBUG = 2;

    // A flag to skip the StackMap and StackMapTable attributes.
    public static final int SKIP_FRAMES = 4;

    // A flag to expand the stack map frames.
    public static final int EXPAND_FRAMES = 8;

    public void accept(final ClassVisitor classVisitor, final int parsingOptions) {
        accept(classVisitor, new Attribute[0], parsingOptions);
    }

    public void accept(
        final ClassVisitor classVisitor,
        final Attribute[] attributePrototypes,
        final int parsingOptions) {
        Context context = new Context();
        context.attributePrototypes = attributePrototypes;
        context.parsingOptions = parsingOptions;
        context.charBuffer = new char[maxStringLength];

        // Read the access_flags, this_class, super_class, interface_count and interfaces fields.
        char[] charBuffer = context.charBuffer;
        int currentOffset = header;
        int accessFlags = readUnsignedShort(currentOffset);
        String thisClass = readClass(currentOffset + 2, charBuffer);
        String superClass = readClass(currentOffset + 4, charBuffer);
        String[] interfaces = new String[readUnsignedShort(currentOffset + 6)];
        currentOffset += 8;
        for (int i = 0; i < interfaces.length; ++i) {
          interfaces[i] = readClass(currentOffset, charBuffer);
          currentOffset += 2;
        }

        // ......

        // Visit the class declaration. The minor_version and major_version fields start 6 bytes before
        // the first constant pool entry, which itself starts at cpInfoOffsets[1] - 1 (by definition).
        classVisitor.visit(readInt(cpInfoOffsets[1] - 7), accessFlags, thisClass, signature, superClass, interfaces);

        // ......

        // Visit the fields and methods.
        int fieldsCount = readUnsignedShort(currentOffset);
        currentOffset += 2;
        while (fieldsCount-- > 0) {
          currentOffset = readField(classVisitor, context, currentOffset);
        }
        int methodsCount = readUnsignedShort(currentOffset);
        currentOffset += 2;
        while (methodsCount-- > 0) {
          currentOffset = readMethod(classVisitor, context, currentOffset);
        }

        // Visit the end of the class.
        classVisitor.visitEnd();
    }

}

另外,我们也可以回顾一下ClassVisitor类中visitXxx()方法的调用顺序:

visit
[visitSource][visitModule][visitNestHost][visitPermittedSubclass][visitOuterClass]
(
 visitAnnotation |
 visitTypeAnnotation |
 visitAttribute
)*
(
 visitNestMember |
 visitInnerClass |
 visitRecordComponent |
 visitField |
 visitMethod
)* 
visitEnd

2. 如何使用Cla***eader类

The ASM core API for generating and transforming compiled Java classes is based on the ClassVisitor abstract class.

ASM能够做什么

在现阶段,我们接触了ClassVisitorClassWriterCla***eader类,因此可以介绍Class Transformation的操作。

import lsieun.utils.FileUtils;
import org.objectweb.asm.Cla***eader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

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)构建Cla***eader
        Cla***eader cr = new Cla***eader(bytes1);

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

        //(3)串连ClassVisitor
        int api = Opcodes.ASM9;
        ClassVisitor cv = new ClassVisitor(api, cw) { /**/ };

        //(4)结合Cla***eader和ClassVisitor
        int parsingOptions = Cla***eader.SKIP_DEBUG | Cla***eader.SKIP_FRAMES;
        cr.accept(cv, parsingOptions);

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

        FileUtils.writeBytes(filepath, bytes2);
    }
}

代码的整体处理流程是如下这样的:

.class --> Cla***eader --> ClassVisitor1 ... --> ClassVisitorN --> ClassWriter --> .class文件

我们可以将整体的处理流程想像成一条河流,那么

  • 第一步,构建Cla***eader。生成的Cla***eader对象,它是这条“河流”的“源头”。
  • 第二步,构建ClassWriter。生成的ClassWriter对象,它是这条“河流”的“归处”,它可以想像成是“百川东到海”中的“大海”。
  • 第三步,串连ClassVisitor。生成的ClassVisitor对象,它是这条“河流”上的重要节点,可以想像成一个“水库”;可以有多个ClassVisitor对象,也就是在这条“河流”上存在多个“水库”,这些“水库”可以对“河水”进行一些处理,最终会这些“水库”的水会流向“大海”;也就是说多个ClassVisitor对象最终会连接到ClassWriter对象上。
  • 第四步,结合Cla***eaderClassVisitor。在Cla***eader类上,有一个accept()方法,它接收一个ClassVisitor类型的对象;换句话说,就是将“河流”的“源头”和后续的“水库”连接起来。
  • 第五步,生成byte[]。到这一步,就是所有的“河水”都流入ClassWriter这个“大海”当中,这个时候我们调用ClassWriter.toByteArray()方法,就能够得到byte[]内容。

ASM里的核心类

3. parsingOptions参数

Cla***eader类当中,accept()方法接收一个int类型的parsingOptions参数。

public void accept(final ClassVisitor classVisitor, final int parsingOptions)

parsingOptions参数可以选取的值有以下5个:

  • 0
  • Cla***eader.SKIP_CODE
  • Cla***eader.SKIP_DEBUG
  • Cla***eader.SKIP_FRAMES
  • Cla***eader.EXPAND_FRAMES

推荐使用:

  • 在调用Cla***eader.accept()方法时,其中的parsingOptions参数,推荐使用Cla***eader.SKIP_DEBUG | Cla***eader.SKIP_FRAMES
  • 在创建ClassWriter对象时,其中的flags参数,推荐使用ClassWriter.COMPUTE_FRAMES

示例代码如下:

Cla***eader cr = new Cla***eader(bytes);
int parsingOptions = Cla***eader.SKIP_DEBUG | Cla***eader.SKIP_FRAMES;
cr.accept(cv, parsingOptions);

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

为什么我们推荐使用Cla***eader.SKIP_DEBUG | Cla***eader.SKIP_FRAMES呢?因为使用这样的一个值,可以生成最少的ASM代码,但是又能实现完整的功能。

  • 0:会生成所有的ASM代码,包括调试信息、frame信息和代码信息。
  • Cla***eader.SKIP_CODE:会忽略代码信息,例如,会忽略对于MethodVisitor.visitXxxInsn()方法的调用。
  • Cla***eader.SKIP_DEBUG:会忽略调试信息,例如,会忽略对于MethodVisitor.visitParameter()MethodVisitor.visitLineNumber()MethodVisitor.visitLocalVariable()等方法的调用。
  • Cla***eader.SKIP_FRAMES:会忽略frame信息,例如,会忽略对于MethodVisitor.visitFrame()方法的调用。
  • Cla***eader.EXPAND_FRAMES:会对frame信息进行扩展,例如,会对MethodVisitor.visitFrame()方法的参数有影响。

对于这些参数的使用,我们可以在ASMPrint类的基础上进行实验。

我们使用Cla***eader.SKIP_DEBUG的时候,就不会生成调试信息。因为这些调试信息主要是记录某一条instruction在代码当中的行数,以及变量的名字等信息;如果没有这些调试信息,也不会影响程序的正常运行,也就是说功能不受影响,因此省略这些信息,就会让ASM代码尽可能的简洁。

我们使用Cla***eader.SKIP_FRAMES的时候,就会忽略frame的信息。为什么要忽略这些frame信息呢?因为frame计算的细节会很繁琐,需要处理的情况也有很多,总的来说,就是比较麻烦。我们解决这个麻烦的方式,就是让ASM帮助我们来计算frame的情况,也就是在创建ClassWriter对象的时候使用ClassWriter.COMPUTE_FRAMES选项。

在刚开始学习ASM的时候,对于parsingOptions参数,我们推荐使用Cla***eader.SKIP_DEBUG | Cla***eader.SKIP_FRAMES的组合值。但是,以后,随着大家对ASM的知识越来越熟悉,或者随着功能需求的变化,大家可以尝试着使用其它的选项值。

4. 总结

本文主要对Cla***eader类进行了介绍,内容总结如下:

  • 第一点,了解Cla***eader类的成员都有哪些。
  • 第二点,如何使用Cla***eader类,来进行Class Transformation的操作。
  • 第三点,在Cla***eader类当中,对于accept()方法的parsingOptions参数,我们推荐使用Cla***eader.SKIP_DEBUG | Cla***eader.SKIP_FRAMES