Simple byte code injection example with Javassist

This is a simple example that will time how long a method takes to execute, without changing the source code.

First, we need to download the javassist software from http://www.javassist.org

Please the javassist.jar file in your classpath. Also, set your classpath to include the tools.jar file in your JDK installation, as well as your current directory.

In the same directory, create a file name SimpleTransform.java with the following contents:

import java.security.*;
import java.lang.instrument.*;
import java.util.*;
import javassist.*;

public class SimpleTransformer implements ClassFileTransformer {

  public SimpleTransformer() {
    super();
  }

  public byte[] transform(ClassLoader loader, String className, Class redefiningClass, ProtectionDomain domain, byte[] bytes) throws IllegalClassFormatException {
    return transformClass(redefiningClass,bytes);
  }

  private byte[] transformClass(Class classToTransform, byte[] b) {
    ClassPool pool = ClassPool.getDefault();
    CtClass cl = null;
	try {
      cl = pool.makeClass(new java.io.ByteArrayInputStream(b));
      CtBehavior[] methods = cl.getDeclaredBehaviors();
      for (int i = 0; i < methods.length; i++) {
        if (methods[i].isEmpty() == false) {
          changeMethod(methods[i]);
        }
      }
      b = cl.toBytecode();
    }
    catch (Exception e) {
      e.printStackTrace();
    }
    finally {
      if (cl != null) {
        cl.detach();
      }
    }
    return b;
  }

  private void changeMethod(CtBehavior method) throws NotFoundException, CannotCompileException {
    if (method.getName().equals("doIt")) {
      method.insertBefore("System.out.println(\"started method at \" + new java.util.Date());");
      method.insertAfter("System.out.println(\"ended method at \" + new java.util.Date());");
    }
  }
}

In the same directory, create another file named SimpleMain.java with the following contents:

import java.lang.instrument.Instrumentation;
 
public class SimpleMain {
  public static void premain(String agentArguments, Instrumentation instrumentation) {	
    instrumentation.addTransformer(new SimpleTransformer());
  }	
}

Next, create a simple class named foo.java with the following contents:

class foo {
  public static void main(String args[]) {
    foo f = new foo();
    f.doIt();
  }

  void doIt() {
    try {
      System.out.println("ran doIt() method");
      Thread.sleep(5000);
    }
    catch (Exception e) {
	  e.printStackTrace();
	}
  }
}

We also need a custom manifest with the following contents, which you should create in the same directory as the one in which we have been working:

Boot-Class-Path: javassist.jar
Premain-Class: SimpleMain

Lastly, we will create our jar file that contains our software above. After compiling each class above, issue the following command in the same directory we have been using:

jar cvfm myAgent.jar mymanifest.txt SimpleMain.class SimpleTransformer.class

We can then run our class with what is below:

java -javaagent:myAgent.jar foo

You should see something similar to what is below:

c:\java>java -javaagent:myAgent.jar foo
started method at Wed Jan 02 21:02:54 EST 2013
ran doIt() method
ended method at Wed Jan 02 21:02:59 EST 2013
c:\java>

We can use this to selectively time methods in a given class, or entire application.

It should be noted that you can pass more than one javaagent on the command line to your JVM. However, if there are namespace collisions, you will end up with a "frankenstein" class.

To test this, simple create two directories with the SimpleMain.java, SimpleTransformer.java, and mymanifest.txt files above. Add something to the modified class such as "Agent1 started" and "Agent2 started", respectively.

You will find output such as below when you run it.

c:\>java -javaagent:agent1\myAgent.jar -javaagent:agent2\myAgent.jar foo
Agent1 started method at Wed Jul 09 07:13:53 EDT 2014
Agent1 started method at Wed Jul 09 07:13:53 EDT 2014
ran doIt() method
Agent1 ended method at Wed Jul 09 07:13:58 EDT 2014
Agent1 ended method at Wed Jul 09 07:13:58 EDT 2014
c:\>

Notice two lines for each, but they were pulled from the agent1 jar file. This is because they each injected the behaviour at the same point in the class. This isn't too harmful, but if it were injecting changed variables, or otherwise modifying the class, it could get messy.

The first one listed will get first cut at it. However, I wouldn't support something like this without extensive testing.

3 comments for “Simple byte code injection example with Javassist

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.