A Guide to Java Bytecode Manipulation with ASM

1. Introduction

In this article, we’ll look at how to use the ASM
library for manipulating an existing Java class by adding fields,
adding methods, and changing the behavior of existing methods.

2. Dependencies

We need to add the ASM dependencies to our pom.xml:

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>6.0</version>
</dependency>
<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm-util</artifactId>
    <version>6.0</version>
</dependency>

We can get the latest versions of
asm
and
asm-util
from Maven Central.

3. ASM API Basics

The ASM API provides two styles of interacting with Java classes for
transformation and generation: event-based and tree-based.

3.1. Event-based API

This API is heavily based on the Visitor pattern and is similar in
feel to the SAX parsing model
of processing XML documents. It is
comprised, at its core, of the following components:

  • ClassReader – helps to read class files and is the beginning of
    transforming a class

  • ClassVisitor – provides the methods used to transform the class
    after reading the raw class files

  • ClassWriter – is used to output the final product of the class
    transformation

It’s in the ClassVisitor that we have all the visitor methods that
we’ll use to touch the different components (fields, methods, etc.) of a
given Java class. We do this by providing a subclass of
*ClassVisitor* to implement any changes in a given class.

Due to the need to preserve the integrity of the output class concerning
Java conventions and the resulting bytecode, this class requires a
strict order in which its methods should be called to generate correct
output.

The ClassVisitor methods in the event-based API are called in the
following order:

visit
visitSource?
visitOuterClass?
( visitAnnotation | visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd

3.2. Tree-based API

This API is a more object-oriented API and is analogous to the JAXB
model
of processing XML documents.

It’s still based on the event-based API, but it introduces the
ClassNode root class. This class serves as the entry point into the
class structure.

4. Working With the Event-based ASM API

We’ll modify the java.lang.Integer class with ASM. And we need to
grasp a fundamental concept at this point: the ClassVisitor class
contains all the necessary visitor methods to create or modify all the
parts of a class
.

We only need to override the necessary visitor method to implement our
changes. Let’s start by setting up the prerequisite components:

public class CustomClassWriter {

    static String className = "java.lang.Integer";
    static String cloneableInterface = "java/lang/Cloneable";
    ClassReader reader;
    ClassWriter writer;

    public CustomClassWriter() {
        reader = new ClassReader(className);
        writer = new ClassWriter(reader, 0);
    }
}

We use this as a basis to add the Cloneable interface to the stock
Integer class, and we also add a field and a method.

4.1. Working With Fields

Let’s create our ClassVisitor that we’ll use to add a field to the
Integer class:

public class AddFieldAdapter extends ClassVisitor {
    private String fieldName;
    private String fieldDefault;
    private int access = org.objectweb.asm.Opcodes.ACC_PUBLIC;
    private boolean isFieldPresent;

    public AddFieldAdapter(
      String fieldName, int fieldAccess, ClassVisitor cv) {
        super(ASM4, cv);
        this.cv = cv;
        this.fieldName = fieldName;
        this.access = fieldAccess;
    }
}

Next, let’s override the visitField method, where we first check if
the field we plan to add already exists and set a flag to indicate the
status
.

We still have to forward the method call to the parent class — this
needs to happen as the visitField method is called for every field in
the class. Failing to forward the call means no fields will be written
to the class.

This method also allows us to modify the visibility or type of existing
fields
:

@Override
public FieldVisitor visitField(
  int access, String name, String desc, String signature, Object value) {
    if (name.equals(fieldName)) {
        isFieldPresent = true;
    }
    return cv.visitField(access, name, desc, signature, value);
}

We first check the flag set in the earlier visitField method and call
the visitField method again, this time providing the name, access
modifier, and description. This method returns an instance of
FieldVisitor.

The visitEnd method is the last method called in order of the
visitor methods. This is the recommended position to carry out the
field insertion logic
.

Then, we need to call the visitEnd method on this object to signal
that we’re done visiting this field:

@Override
public void visitEnd() {
    if (!isFieldPresent) {
        FieldVisitor fv = cv.visitField(
          access, fieldName, fieldType, null, null);
        if (fv != null) {
            fv.visitEnd();
        }
    }
    cv.visitEnd();
}

It’s important to be sure that all the ASM components used come from
the org.objectweb.asm package
— a lot of libraries use the ASM
library internally and IDEs could auto-insert the bundled ASM libraries.

We now use our adapter in the addField method, obtaining a
transformed version of java.lang.Integer
with our added field:

public class CustomClassWriter {
    AddFieldAdapter addFieldAdapter;
    //...
    public byte[] addField() {
        addFieldAdapter = new AddFieldAdapter(
          "aNewBooleanField",
          org.objectweb.asm.Opcodes.ACC_PUBLIC,
          writer);
        reader.accept(addFieldAdapter, 0);
        return writer.toByteArray();
    }
}

We’ve overridden the visitField and visitEnd methods.

Everything to be done concerning fields happens with the visitField
method. This means we can also modify existing fields (say, transforming
a private field to the public) by changing the desired values passed to
the visitField method.

4.2. Working With Methods

Generating whole methods in the ASM API is more involved than other
operations in the class. This involves a significant amount of low-level
byte-code manipulation and, as a result, is beyond the scope of this
article.

For most practical uses, however, we can either modify an existing
method to make it more accessible
(perhaps make it public so that it
can be overridden or overloaded) or modify a class to make it
extensible
.

Let’s make the toUnsignedString method public:

public class PublicizeMethodAdapter extends ClassVisitor {
    public PublicizeMethodAdapter(int api, ClassVisitor cv) {
        super(ASM4, cv);
        this.cv = cv;
    }
    public MethodVisitor visitMethod(
      int access,
      String name,
      String desc,
      String signature,
      String[] exceptions) {
        if (name.equals("toUnsignedString0")) {
            return cv.visitMethod(
              ACC_PUBLIC + ACC_STATIC,
              name,
              desc,
              signature,
              exceptions);
        }
        return cv.visitMethod(
          access, name, desc, signature, exceptions);
   }
}

Like we did for the field modification, we merely intercept the visit
method and change the parameters we desire
.

In this case, we use the access modifiers in the
org.objectweb.asm.Opcodes package to change the visibility of the
method
. We then plug in our ClassVisitor:

public byte[] publicizeMethod() {
    pubMethAdapter = new PublicizeMethodAdapter(writer);
    reader.accept(pubMethAdapter, 0);
    return writer.toByteArray();
}

4.3. Working With Classes

Along the same lines as modifying methods, we modify classes by
intercepting the appropriate visitor method
. In this case, we intercept
visit, which is the very first method in the visitor hierarchy:

public class AddInterfaceAdapter extends ClassVisitor {

    public AddInterfaceAdapter(ClassVisitor cv) {
        super(ASM4, cv);
    }

    @Override
    public void visit(
      int version,
      int access,
      String name,
      String signature,
      String superName, String[] interfaces) {
        String[] holding = new String[interfaces.length + 1];
        holding[holding.length - 1] = cloneableInterface;
        System.arraycopy(interfaces, 0, holding, 0, interfaces.length);
        cv.visit(V1_8, access, name, signature, superName, holding);
    }
}

We override the visit method to add the Cloneable interface to the
array of interfaces to be supported by the Integer class. We plug this
in just like all the other uses of our adapters.

5. Using the Modified Class

So we’ve modified the Integer class. Now we need to be able to load
and use the modified version of the class.

In addition to simply writing the output of writer.toByteArray to disk
as a class file, there are some other ways to interact with our
customized Integer class.

5.1. Using the TraceClassVisitor

The ASM library provides the TraceClassVisitor utility class that
we’ll use to introspect the modified class. Thus we can confirm that
our changes have happened
.

Because the TraceClassVisitor is a ClassVisitor, we can use it as a
drop-in replacement for a standard ClassVisitor:

PrintWriter pw = new PrintWriter(System.out);

public PublicizeMethodAdapter(ClassVisitor cv) {
    super(ASM4, cv);
    this.cv = cv;
    tracer = new TraceClassVisitor(cv,pw);
}

public MethodVisitor visitMethod(
  int access,
  String name,
  String desc,
  String signature,
  String[] exceptions) {
    if (name.equals("toUnsignedString0")) {
        System.out.println("Visiting unsigned method");
        return tracer.visitMethod(
          ACC_PUBLIC + ACC_STATIC, name, desc, signature, exceptions);
    }
    return tracer.visitMethod(
      access, name, desc, signature, exceptions);
}

public void visitEnd(){
    tracer.visitEnd();
    System.out.println(tracer.p.getText());
}

What we have done here is to adapt the ClassVisitor that we passed to
our earlier PublicizeMethodAdapter with the TraceClassVisitor.

All the visiting will now be done with our tracer, which then can print
out the content of the transformed class, showing any modifications
we’ve made to it.

While the ASM documentation states that the TraceClassVisitor can
print out to the PrintWriter that’s supplied to the constructor, this
doesn’t appear to work properly in the latest version of ASM.

Fortunately, we have access to the underlying printer in the class and
were able to manually print out the tracer’s text contents in our
overridden visitEnd method.

5.2. Using Java Instrumentation

This is a more elegant solution that allows us to work with the JVM at a
closer level via
Instrumentation.

To instrument the java.lang.Integer class, we write an agent that
will be configured as a command line parameter with the JVM
. The agent
requires two components:

  • A class that implements a method named premain

  • An implementation of
    ClassFileTransformer
    in which we’ll conditionally supply the modified version of our class

public class Premain {
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(
              ClassLoader l,
              String name,
              Class c,
              ProtectionDomain d,
              byte[] b)
              throws IllegalClassFormatException {
                if(name.equals("java/lang/Integer")) {
                    CustomClassWriter cr = new CustomClassWriter(b);
                    return cr.addField();
                }
                return b;
            }
        });
    }
}

We now define our premain implementation class in a JAR manifest file
using the Maven jar plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>2.4</version>
    <configuration>
        <archive>
            <manifestEntries>
                <Premain-Class>
                    com.baeldung.examples.asm.instrumentation.Premain
                </Premain-Class>
                <Can-Retransform-Classes>
                    true
                </Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

Building and packaging our code so far produces the jar that we can load
as an agent. To use our customized Integer class in a hypothetical
YourClass.class“:

java YourClass -javaagent:"/path/to/theAgentJar.jar"

6. Conclusion

While we implemented our transformations here individually, ASM allows
us to chain multiple adapters together to achieve complex
transformations of classes.

In addition to the basic transformations we examined here, ASM also
supports interactions with annotations, generics, and inner classes.

We’ve seen some of the power of the ASM library — it removes a lot of
limitations we might encounter with third-party libraries and even
standard JDK classes.

ASM is widely used under the hood of some of the most popular libraries
(Spring, AspectJ, JDK, etc.) to perform a lot of “magic” on the fly.

You can find the source code for this article in the
GitHub project.

Leave a Reply

Your email address will not be published.