Quantcast
Channel: Baeldung
Viewing all articles
Browse latest Browse all 4535

A Guide to Java Bytecode Manipulation with ASM

$
0
0

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 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.


Viewing all articles
Browse latest Browse all 4535

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>