What is ASM?

ASM is a Java bytecode manipulation framework. It can be used to dynamically generate classes or enhance the functionality of existing classes. ASM can either generate binary class files directly or dynamically change the behavior of classes before they are loaded into the Java virtual machine. Java classes are stored in rigorously formatted.class files that have enough metadata to parse all the elements of the class: class names, methods, attributes, and Java bytecodes (instructions). After reading information from class files, ASM can change class behavior, analyze class information, and even generate new classes based on user requirements.

Why dynamically generate Java classes?

Imagine if an open source framework required you to add Java classes to implement functions such as log, Cache, transaction, etc. I don’t think you would use this open source framework. Dynamically generating classes can reduce intrusion into your code and increase user productivity.

Why ASM?

The most direct way to modify a Java class is to rewrite the class file directly. The Java specification specifies the format of class files, and editing bytecodes directly can indeed change the behavior of Java classes. To this day, there are some Java geeks who use primitive tools such as UltraEdit to operate on class files. Yes, this is the most straightforward method, but it requires the user to be familiar with the format of the Java Class file: carefully calculate the offset of the desired function to the file head, and recalculate the class file’s checksum to pass the Java virtual machine’s security mechanism.

As you can see, working directly with class files is cumbersome, as is why we all choose to use frameworks, which mask the underlying complexity. ASM is a powerful tool for manipulating classes.

Programming with ASM

ASM provides two apis:

  1. CoreAPI (ClassVisitor, MethodVisitor, etc.)
  2. TreeAPI (ClassNode, MethodNode, etc.)

The difference is that the CoreAPI is based on the event model and defines the Visitor for each element in the Class without loading the entire Class into memory. The TreeAPI reads the entire Class structure into memory as a Tree structure. The TreeAPI is simpler to use.

The following example uses the CoreAPI approach.

Add Maven:

<dependency> <groupId>org.ow2.asm</groupId> <artifactId> <version>5.0.4</version> </dependency>Copy the code

The usage is relatively stable, and version 5.0.4 is used more often.

First, there are several ways to modify a Class, such as directly modifying the current Class or subclassing it to enhance it.

The following example enhances the effect by subclassing the specified Class. The advantage is that the original Class is non-intrusive and the effect can be polymorphic.

First, define a class we want to enhance:

package com.zjz;

import java.util.Random;

/**
 * @author zhaojz created at 2019-08-22 10:49
 */
public class Student {
    public String name;

    public void studying() throws InterruptedException {
        System.out.println(this.name+"Learning..."); Thread.sleep(new Random().nextInt(5000)); }}Copy the code

Next, define a ClassReader:

ClassReader classReader = new ClassReader("com.zjz.Student");
Copy the code

Then define a ClassWriter:

 ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
Copy the code

ClassWriter.COMPUTE_MAXS represents automatic calculation of local variable and operand stack size. For more options, see asM.ow2.io

Next, the official visit to Class:

// Access the Class by ClassVisitor (anonymous Class, ClassVisitor ClassVisitor = new ClassVisitor(opcodes.asm5, classWriter) {// Declare a global variable, String enhancedSuperName, the parent of the enhanced subclass; @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {// Concatenate the class name of the subclass to be generated: Student$EnhancedByASM
   String enhancedName = name+"$EnhancedByASM"; // Set Student as parent enhancedSuperName = name; super.visit(version, access, enhancedName, signature, enhancedSuperName, interfaces); } @Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {// Here is the demo field access system.out.println ("Field:" + name);
    return super.visitField(access, name, desc, signature, value);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    System.out.println("Method:"+ name); MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); MethodVisitor wrappedMv = mv; // Determine the current reading methodif (name.equals("studying")) {/ / if is studying method, the packaging of a method Visitor wrappedMv = new StudentStudyingMethodVisitor (Opcodes ASM5, mv); }else if(name.equals("<init>"){// If it is a constructor, Processing subclasses of the parent class constructor calls wrappedMv = new StudentEnhancedConstructorMethodVisitor (Opcodes ASM5, mv, enhancedSuperName); }returnwrappedMv; }};Copy the code

Let’s focus on the MethodVisitor:

/ / Studying methods of the Visitor static class StudentStudyingMethodVisitor extends MethodVisitor {public StudentStudyingMethodVisitor(int i, MethodVisitor methodVisitor) { super(i, methodVisitor); } //MethodVisitor defines different visitXXX() methods representing different access phases. //visitCode indicates the method just entered. @Override public voidvisitCode() {// Add a line system.currentTimemillis () to visitMethodInsn(opcodes.invokestatic,"java/lang/System"."currentTimeMillis"."()J".false); // And store it at visitVarInsn(opcodes.lstore, 1) at position 1 in the local variable table; Public void Override public void string (); // Long start = system.currentTimemillis () VisitInsn (int opCode) {// Opcode tells you which step is currently accessed. If it is >= opcodes. IRETURN && opcode <= opcodes. RETURN indicates that the method is about to exitif((opcode >= opcodes.ireturn && opcode <= opcodes.return)){ And pass it to the following method visitVarInsn(opcodes.lload, 1); // Then call a custom tool method, which is used to output the time visitMethodInsn(opcodes.invokestatic,"com/zjz/Before"."end"."(J)V".false); } super.visitInsn(opcode); }} the static class StudentEnhancedConstructorMethodVisitor extends MethodVisitor {/ / define a global variable records the superclass name private String superClassName; public StudentEnhancedConstructorMethodVisitor(int i, MethodVisitor methodVisitor,String superClassName) { super(i, methodVisitor); this.superClassName = superClassName; } @override public void visitMethodInsn(int opcode, String owner, String name, String desc, Boolean b) {Override public void visitMethodInsn(int opcode, String owner, String name, String desc, Boolean b) First access the superclass constructor, similar to super() in the source codeif (opcode==Opcodes.INVOKESPECIAL && name.equals("<init>")){ owner = superClassName; } super.visitMethodInsn(opcode, owner, name, desc, b); }}Copy the code

New ClassVisitor(opcodes.asm5, classWriter) defines the output of the data.

classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
Copy the code

This completes the Class reading, access modification, output process.

The careful viewer will see, where is the output? How do I access the newly generated classes? So we need to define a ClassLoader to load our generated Class:

 static class StudentClassLoader extends ClassLoader{
        public Class defineClassFromClassFile(String className,byte[] classFile) throws ClassFormatError{
            returndefineClass(className, classFile, 0, classFile.length); }}Copy the code

The byte array of the newly generated class is then fetched via ClassWriter and loaded into the JVM:

 byte[] data = classWriter.toByteArray();
 Class subStudent = classLoader.defineClassFromClassFile("com.zjz.Student$EnhancedByASM", data);
Copy the code

This completes the generation of a class, and the code above does one very simple thing: keep track of learning time.

To sum up:

The three core things in the ASM CoreAPI are ClassReader, Visitor, and ClassWriter, which are linked together through the responsibility chain pattern.

Visitor mode is used to access properties of methods, fields, and so on. If you need to modify a method or field, just give the original Visitor to Wrap.

Hooks on how to code require an understanding of jVM-specific bytecode instructions, as well as ASM’s associated OpCode.

ASM Bytecode Outline 2017

But how do you remember all those instructions, opcodes, symbols? For example in the code above:

visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System"."currentTimeMillis"."()J".false);
visitVarInsn(Opcodes.LSTORE, 1);
Copy the code

Opcodes.INVOKESTATIC, opcodes.lstore, ()J. In fact, in addition to practice makes perfect, you can also use tools.

If you are using IDEA, you can install the ASM Bytecode Outline 2017 plugin. Then right-click on the source file and select Show Bytecode Outline. You should see the following view:

Switch to ASMified view and you’ll see the same code as we wrote above, just Copy it and use it.

See sample complete source code: ASm_demo

References:

asm.ow2.io/

www.ibm.com/developerwo…

Juejin. Cn/post / 684490…