Instrumentation is the process of injecting code into a compiled program. In Java, this can be done statically and dynamically. Using static intrumentation, a class’ bytecode is modified and saved to disk; permanently modifying the class. With dynamic instrumentation, the class’ bytecode is modified in memory right before being loaded.
This is the first in a series of several ways to go about doing dynamic instrumentation in Java. I will be making use of the Javassist bytecode manipulation library for this series. In this first post, I will be going over Java dynamic instrumentation used within the main program. First, you will need Java installed (of course) and the Javassist jar file (I am using version 3.15). While the Javassist API documentation will provide a thorough description of the classes and functions involved, I will be covering the basics.
The following contains no instrumentation:
$ javac Inst0.java HelloWorld.java $ java Inst0 Hello World! Goodbye world
We are now going to add some instrumentation in to output some basic tracing.
$ javac -classpath /path/to/javassist.jar: Inst1.java HelloWorld.java $ java -cp /path/to/javassist.jar: Inst1 HelloWorld.do1() Start Hello World! HelloWorld.do1() End Goodbye world
*Note: The ':'s are necessary.
In this code, the classes to take note of are
ClassPool object contains
CtClass objects represent class files.
CtMethod objects represent methods.
To modify a class, we must first obtain a reference to a
CtClass object representing the class from a
pool.get("HelloWorld") returns a reference to the
CtClass object for the
HellowWorld class. To modify a class’ method, we must first get a reference to its corresponding
CtMethod from the class’
CtClass instance. In the above code we do this by using
hw_ctc.getDeclaredMethod("do1") to get the
To add code to the beginning and end of a method we use the
insertAfter() methods of
CtMethod. They allow us to place the corresponding bytecode of the string argument at the beginning and end (before any returns). It should be noted that the access of the bytecode is limited (see the API entry for more information) and any code that exceeds these limits will throw a
toClass() method of
CtClass creates a
java.lang.Class object from the
CtClass and loads the class. The code will now use the modified class definition of
HelloWorld as the regular one from this point on.white-space: -pre-wrap;
When we call
hw.do1() we see that there are extra lines printed before and after the
System.out.println("Hello World!") in the original
If we want to add this tracing to every method in a class, using
getDeclaredMethod() for each method is not nearly as useful as
$ javac -classpath /path/to/javassist.jar: Inst2.java HelloWorld.java $ java -cp /path/to/javassist.jar: Inst2 HelloWorld.do1 Start Hello World! HelloWorld.do1 End HelloWorld.do2 Start Goodbye world HelloWorld.do2 End
getDeclaredMethods() returns an array of references to all
CtMethod objects in a
CtClass object. We now loop through all of the methods in
HellowWorld and apply the
insertAfter() to all of them.
At this point we can add functionality to the methods in classes that doesn’t affect the flow of logic in the middle of the methods, such as tracing method calls. In the next post, I will go over more advanced and powerful forms class and method manipulation.