Understanding Java Agents
A powerful tool you might have missed.
Join the DZone community and get the full member experience.
Join For FreeLet’s say that you have an application running in production. Every once in a while, it gets into a broken state, the error is hard to reproduce, and you need some more information out of the application.
So are you wondering about the solution?
What you could do is dynamically attach some set of code to your application and carefully rewrite it so that the code dumps additional information you can log, or else you can dump the application stage into a text file. Java gives us a facility to do this using Java Agent.
Have you ever wondered how our Java code is hot-swapped in our IDE? It’s because of agents. Another interesting fact about Java agent is Application profilers are using the same technique at the backend to collect information of memory usage, memory leakage, and execution time of methods.
So what is a Java Agent?
Java agents are a special type of class which, by using the Java Instrumentation API, can intercept applications running on the JVM, modifying their bytecode. Java agents are extremely powerful and also dangerous.
Before diving in, I will explain how a Java Agent intercepts a Class using the simple HelloWorld Example.
xxxxxxxxxx
public class Hello {
public static void main(String[] args){
System.out.println("hello world");
}
}
As shown in the below diagram, Classloaders are in charge of loading classes from binary to in-memory. When you run the compiled HelloWorld application (HelloWorld.class), the agent could be viewed as a way to intercept classloaders behavior at runtime. You may think how come the java byte code will be restructured so that agent can add the relevant code at the correct places. The interesting fact is, for Java programs, the structure of the bytecode is really close to the original Java program source code. Hence, while we don’t instrument the Java program itself, we use a very close representation of it. One thing to note is that there are non-Java languages that compile into Java bytecode (such as Scala, Clojure, and Kotlin), which means that the structure and shape of the bytecode for programs can be very different.
Implementing a Java Agent
Java agents are based on facility, coming from the Java platform and the entry point to that is a java.lang instrument
package, which provides services that allow agents to instrument programs running on the JVM. The package is quite simple and self-contained, as it contains a couple of exception classes, a data class, the class definition, and two interfaces. Out of those two, we only need to implement classFileTransformer
interface, if we want to write a Java agent.
There are two ways to define an Agent.
The first one is a static agent, which means that we build our agent we package it as a jar file, and when we start our Java application, we pass in a special JVM argument called javaagent
. Then we give it the location of the agent jar on disk, and then the JVM does its magic.
xxxxxxxxxx
$ java -javaagent:<path of agent jar file> -jar <path of the packaged jar file you want to intecept>
We need to add a special manifest entry, which is called the pre-main class, and of course, this is a fully qualified name class definition.
xxxxxxxxxx
Premain-Class : org.example.JavaAgent
The class looks something like this
xxxxxxxxxx
public class JavaAgent {
/**
* As soon as the JVM initializes, This method will be called.
*
* @param agentArgs The list of agent arguments
* @param instrumentation The instrumentation object
* @throws InstantiationException
*/
public static void premain(String agentArgs, Instrumentation instrumentation) throws InstantiationException {
InterceptingClassTransformer interceptingClassTransformer = new InterceptingClassTransformer();
interceptingClassTransformer.init();
instrumentation.addTransformer(interceptingClassTransformer);
}
}
premain
method takes two arguments:
agentArgs
— String arguments, whatever the user has chosen to pass as arguments to the Java agent invocation.instrumentation
is from the java.lang instrument package, and we can add a newClassFileTransformer
object, which contains the actual logic of our Agent.
The second option is called a dynamic agent.
Instead of instrumenting the way you launch the application, what you can do is write a small piece of code that takes and connects to an existing JVM and tells it to load a certain agent.
xxxxxxxxxx
VirtualMachine vm = VirtualMachine.attach(vmPid);
vm.load(agentFilePath);
vm.detach();
This argument agentFilePath
is the exact same one as in the static agent approach. It has to be the file name of the agent jar so no input streams no bytes. There are two caveats with this approach. The first one is that this is private API living under the com sun space and it usually works for hotspot implementations. The second one is that sorting with java 9 you can no longer use this code to attach to the JVM it’s running.
Class Transformation
This is the interface that we need to implement for our agent in order to transform the classes.
xxxxxxxxxx
public interface ClassFileTransformer {
byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
}
It’s a bit of a mouthful, but I will explain the necessary arguments in the method signature. The first important one is the className
the primary purpose of this parameter is to help to find and differentiate between the right class you want to intercept and others. Obviously, you may not want to intercept each class in your application, and the simplest way to do that is to check with the conditional statement.
Then ClassLoader
, which is mostly used in environments that don’t have a flat class space for basic applications you probably can get away without looking at it, but as soon as you run into something more complicated or a modular platform you need to look in the ClassLoader. classfileBuffer
is the current definition of the class before being instrumented. To intercept it, you need to read this byte array using libraries and intercept your code and then have to transform back to bytecode again to return.
There are several byte code generation libraries. You need to do the research and decide for yourself in terms of whether it is a high-level API or low-level API, community size, and the license. The demo I put below is Javassist because I think it has a nice balance between high-level and low-level API s and also is a triple license so it should be available for almost anyone to consume. so this is the body of the implementation of the ClassFileTransformer
.
xxxxxxxxxx
public byte[] transform(ClassLoader loader, ..)
throws .. {
byte[] byteCode = classfileBuffer;
// If you wanted to intercept all the classs then you can remove this conditional check.
if (className.equals("Example")) {
try {
ClassPool classPool = scopedClassPoolFactory.create(loader, rootPool,
ScopedClassPoolRepositoryImpl.getInstance());
CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod[] methods = ctClass.getDeclaredMethods();
for (CtMethod method : methods) {
if (method.equals("main")) {
method.insertAfter("System.out.println(\"Logging using Agent\");");
}
}
byteCode = ctClass.toBytecode();
ctClass.detach();
} catch (Throwable ex) {
log.log(Level.SEVERE, "Error in transforming the class: " + className, ex);
}
}
return byteCode;
}
ctClass.detach();
} catch (Throwable ex) {
log.log(Level.SEVERE, "Error in transforming the class: " + className, ex);
}
}
return byteCode;
}
Here, from the classPool
, we can directly get the class bypassing the classfileBuffer
since I wanted to work with the method main
. We loop through all the method in the class definition and get the class we wanted. We do not have to work with bytecode at all. We can simply pass it some legal Java code, and then Javassist will compile it generates the new bytecode and give us that definition.
There are three ways to insert some Java code into the method. insertAfter(..)
inserts bytecode at the end of the body. It inserts bytecode at the end of the body.insertAt(..)
inserts bytecode at the specified line in the body and insertBefore(..)
inserts bytecode at the beginning of the body.
Getting hands-on with Java Agent
- Download the sample application and Java Agent from the link pointed out.
- Build both the repo using going into the path and execute the command
mvn clean install
- Now, you will get the jar files in the target. Copy the path of the
.jar
file in Example Application and copy the path of the-dependencies.jar
file in JavaAgent. - First, run the application only with the Example Application using the command
$ java -jar <path of the packaged jar>
and observe the output.Hi I am main.
will be printed in the console. - Then, run the application attached with the java agent using the command
$ java -javaagent:<path of agent jar file> -jar <path of the packaged jar
file you want to intercept>
and observe the output.Logging using Agent
will be printed additionally in the console. This ensures that the java agent has been intercepted and added to the body of themain
method.
In summary, If you want to implement a Java Agent:
- You need to create two Java classes. One with the with
premain
method (JavaAgent) and another class which extends theClassFileTransformer
(CustomTransformer) - Inside the body of the
premain
method, you need to add the object of the class which extends theClassFileTransformer
- Then you need to add the logic inside the overridden method
transform
inside CustomTransformer. - When transforming the bytecode inside the transform method you may need to use the bytecode generation libraries according to your purpose.
- You need to specify the
premain
class in the Manifest and build the jar. - Use the
javaagent
tag to load the agent with the application you wanted to intercept.
Me and Java Agent
I am developing some sort of debugger for WSO2 Identity Server, which gets the important variables from the Authentication flow from the server. As I mentioned, in the beginning, It is impossible to change the whole code that we want to intercept. So It is easy to dynamically attach some set of code to Server and carefully rewrite it so that the code fires the additional information you can use to debug. This architecture which does debug without starting the Java Debugging or any code manipulation was amazed me, so I thought of putting some words about this amazing tool.
Conclusion
In this post, we’ve looked at the extremely powerful entry in the Java developer toolbelt: the Java agent. which has the power to access to classes loaded into the JVM. You might wonder if all that we have done too much work for little result. The answer would be a firm “No.” First, you must keep in mind that Hello world example is elaborated here to explain the use of the Java agents. Things that can be done with java agent is enormous and they come handy when it’s complex code to rewrite. I have only just scratched the surface of what can be achieved with java agent, but hopefully, after reading this post, you will now know of their existence and can investigate things further. However, For persistence and proper monitoring, building a reliable java agent is a task that needs to be tackled by a team of dedicated engineers. Let me know how you got on!
Happy blogging! Happy Coding :)
Opinions expressed by DZone contributors are their own.
Comments