DEV Community

Ruby Valappil
Ruby Valappil

Posted on • Originally published at Medium on

Java Instrumentation — A Simple Working Example in Java

Java instrumentation

In this article, we will explore how to instrument a Java program.

Java package that provides services to allow instrumentation is java.lang.instrument.

What is Instrumentation?

From the docs — “The mechanism for instrumentation is modification of the byte-codes of methods.”

In simple words, we know that when we run our Java programs it gets converted to bytecode. These are then loaded to JVM using classloaders. Using Instrumentation APIs we get to modify the bytecodes of these programs at runtime.

Application monitoring tools use this same service to provide useful information on execution time etc.

The class that intercepts the bytecode is called an Agent Class.

Know the Components
The main components are,

  • Agent Class — It’s similar to the main class of a Java application. This class must contain a method named premain(). premain() method can have one of two possible signatures,

From the docs-
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);

  • Transformer Class — This class implements the ClassFileTransformer interface and implements the transform method.
    public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined,
    ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException

  • Manifest file — While creating a jar of a java application, we provide the path to the main class in the manifest file, similarly in the java agent (agent class + transformer class) we need to create a manifest file and add the path to the class that has the premain method.

What’s in the Java Instrument package
java.lang.instrument package provides two Interfaces,
ClassFileTransformer — This is implemented by a Java Agent Class.

Instrumentation — Provides services needed to perform instrumentation.

How to Implement?
A few third-party libraries are available in the market to perform operations on the bytecode. In this example, we will use javassist but you can achieve similar results using other libraries as well.

maven dependency to add javassist to our project is,

<!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>

In this tutorial, we will create two projects,
Our Main Application — that prints a Welcome message.
Our Java Agent — that prints a String value after our application is done printing the welcome message.

We will also be using maven to build our java applications. You may want to create a simple java application without using build tools and create a jar using the java jar command.

I prefer using the build tool so as to not worry about providing the path to third-party dependencies or creating the manifest file.

Step 1: Let’s create a maven project and print a welcome message. This would be our main application.

package myapp;
public class Sample {
public static void main(String[] args) {
System.out.println("Hey There");
}
}}

As we will be running the agent and our application from the command line, let’s give the path to the main class in the pom file(the equivalent of adding manifest file) and add maven-jar-plugin to generate a jar.

<artifactId>maven-jar-plugin</artifactId>
...
<manifest>
<addClasspath>true</addClasspath>
<mainClass>myapp.Sample</mainClass>
</manifest>

Step 2: Next, let’s create our second maven project. This would be our Java Agent.
Add a class that would implement the ClassFileTransformer. In this class, we will use the services provided by javaassist library and read the byte array of the class that class loader loads, and add our own features on top of it.

//Add instrumentation to Sample class alone
if (className.equals("myapp/Sample")) {
try {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod[] methods = ctClass.getDeclaredMethods();
for (CtMethod method : methods) {
method.insertAfter("System.out.println(\"adding end line..\");");
}
byteCode = ctClass.toBytecode();
ctClass.detach();
} catch (Throwable ex) {
System.out.println("Exception: " + ex);
ex.printStackTrace();
}
}

In the above example, we read the method from our main application and then add an extra feature after the method is executed.

We also need to create a class that would contain the premain method.

public static void premain(String args, Instrumentation instr) {
System.out.println("Inside premain.........");
instr.addTransformer(new MyTransformer());
}

We also need to make sure that we use maven assembly and not jar plugin.
<artifactId>maven-assembly-plugin</artifactId>
assembly plugin will make sure that all the third party dependencies are bundled together with the application class files. If we add a jar plugin, dependencies will not be bundled with the class files and we would get classnotfound exceptions for all the classes that are in javaassist library.

Also, we need to add the path to premain() class in the manifest entry of pom file.
<archive>
<manifest>
<addDefaultImplementationEntries>
true</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>
true</addDefaultSpecificationEntries>
</manifest>
<manifestEntries>
<Premain-Class>agent.MyAgent</Premain-Class>
<Can-Redefine-Classes>false</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>

Once the changes are made, build the jar files of both applications.
Testing
Once the jars are generated, run the below command and check the output

java -javaagent:agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar -jar myapp-0.0.1-SNAPSHOT.jar

output would be

Inside premain.........
Hey There
adding end line..

As expected, we see that the premain method was executed first, which adds the transformer, when the classloader loads our main application, it appends the print statement that the transformer instructed to add after the method is executed.

Conclusion
Complete agent and application code is available over Github. If you find the code useful, leave a star ⭐️

References
https://javapapers.com/core-java/java-instrumentation/116212d41081------2)

Discussion (0)