Java instrumentation example

In one of previous articles we discovered the idea of Java agents. We mentioned that instrumentation package can be used to develop them. It's the time to approach this topic deeper.

Through this article we'll present the famous instrumentation package. We'll begin by exploring its content. In the second part we'll implement discovered classes in a code. Its feature will be to increment a counter of invocation of given method.

java.lang.instrument package

java.lang.instrument package is quite stable. The single major changes were made between JDK 5 and JDK 6 where some methods were added to Instrumentation interface. Its implementation is an entry point to any byte-code manipulation through Java agents. It contains some methods to invoke class redefinition, such as: redefineClasses, retransformClasses. Instrumentation contains also some interesting reading methods. One of the most interesting is getObjectSize(Object) which helps to determine (approximately), how many place will be occupied by given object. Another useful method is isModifiableClass(Class<?>). Thanks to it we can verify if class from parameter can be changed. But the real transformation is possible thanks to registering of class transformers through addTransformer(ClassFileTransformer) methods.

ClassFileTransformer is an interface containing a single method, transform(ClassLoader, String, Class<?>, ProtectionDomain, byte[]). The main role of this method consists on changing the content of loaded class. It's called for definition and redefinition on every class. Source code modification can be achieved in different ways but one of the simplest is the use of Javassist tool.

Instrument example

Now when we know the basics, we can implement our own Java agent. First, we need to define a manifest file:

Manifest-Version: 1.0
Can-Retransform-Classes: true
Premain-Class: com.waitingforcode.instrumentation.Agent

It contains the configuration for Java agent. Premain-Class must terminate with blank line and contains the full name of Java agent. This agent, in its turn, must define a public static method called premain, as below:

public class Agent {

  public static void premain(String agentArgs, Instrumentation inst) {
    inst.addTransformer(new PrintingTransformer(agentArgs));
  }

}

As you can see, it's the place where Instrumentation instance is manipulated. In our case, a new transformer is defined. The transformer which body looks like:

public class PrintingTransformer implements ClassFileTransformer {

  private final String newContent;

  public PrintingTransformer(String newContent) {
      this.newContent = newContent;
  }

  @Override
  public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, 
  ProtectionDomain protectionDomain, byte[] classfileBuffer)
          throws IllegalClassFormatException {

    byte[] byteCode = classfileBuffer;

    if (className.contains("com/waitingforcode/SamplePrinter") && classBeingRedefined == null) {
      try {
        ClassPool cp = ClassPool.getDefault();
        CtClass cc = cp.get("com.waitingforcode.SamplePrinter");
        CtMethod m = cc.getDeclaredMethod("printLine");
        m.setBody("System.out.println(\""+newContent+"\");");
        byteCode = cc.toBytecode();
        cc.detach();
      } catch (Exception ex) {
        ex.printStackTrace();
        throw new IllegalClassFormatException("An error occurred on formatting class");
      }
    }
    return byteCode;
  }
}
// Original SamplePrinter
public class SamplePrinter {

  public void printLine() {
    System.out.println("Test string");
  }

}

There are no magic. If loaded class is SamplePrinter which hasn't been defined yet, we try to override the content of its printLine() method. Instead of printing "Test string" will output the text defined in command line argument. Main class looks like below:

public class Starter { 

  public static void main(String[] args) {
      SamplePrinter printer = new SamplePrinter();
      printer.printLine();
  }
}

To see the overriding, Starter class should be executed with -javaagent:/my_jar.jar=NewString parameter. After execution, we should see the application printing "NewString" instead of "Test string".

This article shows some basic information about instrumentation package. Its first part presents two main classes: Instrumentation coordinating classes redefinition and ClassFileTransformer making class content changes. The second part contains an example of class transformation consisting on replacing body of printLine() method.


If you liked it, you should read:

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!