preface

ASM is known as a bytecode authoring tool, and countless “legendary” frameworks have showered their magic on it.

Recently I found in my work that I need to strengthen this part of ability, otherwise many technical solutions are always very troublesome… But ASM alone can’t really do anything, because at the end of the day it’s just a handy tool for rewriting classes. Gradle’s Transform API, annotations, and other characters are needed to make it work.

So in the next period of time, I will try my best to output their actual combat content in this respect.

The body of the

In this article we will talk about some of the uses of ASM, focusing on ASM. So the bytecode part will not be expanded, and those who are interested in it can learn about it by themselves

There are several core classes: ClassReader, ClassWrite, ClassVisitor… And so on a variety of Visitor series.

From the definition of the class name, we can guess that ClassReader is used to read class files; ClassWrite Is used to rewrite class files. But the ClassVisitor, FieldVisitor… Abstract the flow of class, method and object access.

The first step is to add dependencies. AMS is now available in a very advanced version, but let’s just use any version

implementation 'org. Ow2. Asm: asm: 6.0'
implementation 'org. Ow2. Asm: asm - util: 6.0'
Copy the code

A, reading Class

Here is a simple read class demo code:

fun main(a) {
    val cp = ClassPrinter()
    val cr = ClassReader("com.test.asm.AsmDemo")
    cr.accept(cp, 0)}class AsmDemo {
    private val hello = "Hello ASM"

    fun testAsm(a) {
        invokeMethod()
    }

    private fun invokeMethod(a) {
        print(hello)
    }
}


class ClassPrinter : ClassVisitor(ASM6) {
    override fun visit(
        version: Int, access: Int, name: String? , signature:String? , superName:String? , interfaces:Array<String>) {
        println("$name extends $superName {")}// For the sake of simplicity, some less important methods have been removed

    override fun visitField(access: Int, name: String? , desc:String? , signature:String? , value:Any?).: FieldVisitor? {
        println("    visitField(name:$name desc:$desc signature:$signature)")
        return null
    }

    override fun visitMethod(access: Int, name: String? , desc:String? , signature:String? , exceptions:Array<String>?: MethodVisitor? {
        println("    visitMethod(name:$name desc:$desc signature:$signature)")
        return null
    }

    override fun visitEnd(a) {
        println("}")}}Copy the code

The code runs with the following output:

com/test/asm/AsmDemo extends java/lang/Object { visitField(name:hello desc:Ljava/lang/String; signature:null) visitMethod(name:testAsm desc:()V signature:null) visitMethod(name:invokeMethod desc:()V signature:null)  visitMethod(name:<init> desc:()V signature:null) }Copy the code

We can read the corresponding Class through the ClassReader(” com.test.asm.asmdemo “). ClassPrinter: ClassVisitor(ASM6) learns the details of the corresponding class through the corresponding visit method.

Second, the writing Class

The writing process is relatively complex, after all, the operation space is relatively large. For example, the following operation removes a method:

fun main(a) {
    val cr = ClassReader("com.test.asm.AsmDemo")
    val cw = ClassWriter(cr, 0)
    val adapter = RemoveMethodAdapter(cw, "testAsm")
    cr.accept(adapter, 0)
    / / output class
    val outFile = File("... Local address/com/test/asm/TestAsmDemo class")
    outFile.writeBytes(cw.toByteArray())
}

public class RemoveMethodAdapter extends ClassVisitor {
      private String mName;
      private String mDesc;
      public RemoveMethodAdapter( ClassVisitor cv, String mName, String mDesc) {
          super(ASM7, cv);
          this.mName = mName;
		  this.mDesc = mDesc;
      }
      @Override
      public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
          if (name.equals(mName) && desc.equals(mDesc)) {
              // Do not delegate to the next accessor -> this will remove the party
              return null;
		  }
          returncv.visitMethod(access, name, desc, signature, exceptions); }}Copy the code

Open outputTestAsmDemo.class:

We can see that the testAsm() method has been removed from the ClassWriter instance.

Three, source speed reading

The whole process starts with the accept() method of ClassReader:

public void accept(
      final ClassVisitor classVisitor,
      final Attribute[] attributePrototypes,
      final int parsingOptions) {
 	// Omit a lot of code
    if ((parsingOptions & SKIP_DEBUG) == 0&& (sourceFile ! =null|| sourceDebugExtension ! =null)) { classVisitor.visitSource(sourceFile, sourceDebugExtension); }}Copy the code

This is a simple snippet of code that essentially reads the class file, parses it according to the class specification, and then calls back to the interface method corresponding to the ClassVisitor.

For the implementation of ClassVisitor, it could be our own implementation class so that we can access the ClassReader parsing the class.

But in general we need to rewrite the class, and the core class is ClassWriter.

public class ClassWriter extends ClassVisitor {
  // Omit a lot of code
  @Override
  public final MethodVisitor visitMethod(
      final int access,
      final String name,
      final String descriptor,
      final String signature,
      final String[] exceptions) {
    MethodWriter methodWriter =
        new MethodWriter(symbolTable, access, name, descriptor, signature, exceptions, compute);
    if (firstMethod == null) {
      firstMethod = methodWriter;
    } else {
      lastMethod.mv = methodWriter;
    }
    returnlastMethod = methodWriter; }}Copy the code

Here we’ve individually intercepted the visitMethod() method, and you can see that the real implementation here is through MethodWriter:

final class MethodWriter extends MethodVisitor {
  // Omit a lot of code
  @Override
  public void visitMethodInsn(
      final int opcode,
      final String owner,
      final String name,
      final String descriptor,
      final boolean isInterface) {
    lastBytecodeOffset = code.length;
    Symbol methodrefSymbol = symbolTable.addConstantMethodref(owner, name, descriptor, isInterface);
    if (opcode == Opcodes.INVOKEINTERFACE) {
      code.put12(Opcodes.INVOKEINTERFACE, methodrefSymbol.index)
          .put11(methodrefSymbol.getArgumentsAndReturnSizes() >> 2.0);
    } else {
      code.put12(opcode, methodrefSymbol.index);
    }
    // Omit some code}}Copy the code

As you can see, this encapsulates the code that writes the class. The main reason we can rewrite the class is because of the encapsulation of the ClassWriter class, which is based on the visitor pattern of the ClassVisitor to see how the ClassReader loads and parses the class.

More we don’t see, we are interested in their own with a wave. After all, the above code is enough to understand the overall ASM workflow.

Four, summary

Combined with the above content, let’s make a summary

First, ASM is based entirely on the visitor pattern (it doesn’t matter if you don’t know this pattern, it doesn’t affect your understanding).

  • ClassReader parses the class file and calls back the methods of the corresponding ClassVisitor interface
    • ClassReader just parses the class file and calls back to the ClassVisitor, so ClassReader is done
  • ClassWriter is an implementation of the ClassVisitor interface
    • This encapsulates writing to the class file
      • Specific code in the various Writer implementation of ClassWriter.
    • The ClassWriter is also a ClassVisitor
      • It is our first layer “proxy”, and our custom ClassVisitor is passed to the ClassWriter to forward the visitor process

Therefore, it can be said that ClassReader + ClassVisitor uses the standard visitor pattern. The purpose is that the ASM framework is based on a set of interfaces that allow us to access the flow of a class file.

When we need to rewrite a class, we need the ClassWriter special ClassVisitor to augment the capability.

The end of the

This article is short, but it’s a prelude to “behind the scenes” bytecode editing.

Later articles will step into the real application. There is a wide range of knowledge involved, and I will try to present it in a colloquial way.

More updated articles welcome to pay attention to our public number: salted fish is turning over.