What is a Java Agent and what is it for?

Lately, we have been seeing in those posts how to perform some monitoring tasks. Today we will see the Java Agents, what they are and how they work. Which can also help us to perform monitoring tasks, among other things. Something that we will try to explain in this post, first with some theory and then with the help of examples.
What are Java Agents?, they are basically Java libraries that include classes that implement the Java Instrumentation API. Which is available since JDK 1.5. This is a very simple API but at the same time, very powerful. It lays the foundation for application instrumentation.
The main functionality of this instrumentation is that it will allow us to add code to certain classes. And with this addition, we will be able to recompile data to be used mainly for monitoring and analysis or event logging purposes. Allowing us to obtain vital information about the application, that can help us to solve problems or obtain data that would not be possible otherwise.
The main quality of this instrumentation API is the alteration of the byte-code of the classes that are being executed by the virtual machine. Through this alteration, we will be able to perform the actions previously indicated.
But let’s focus, what do we need to create a Java Agent? We only have to follow these three steps:
- Develop a concrete method.
- Create and configure correctly the MANIFEST.MF file.
- Tell the virtual machine to take into account our library with the Java Agent.
The method to develop depends on when we want the agent to be associated with the execution of applications in the JVM. The instrumentation API will provide us with two different methods:
- premain: It will allow the execution of the Java Agent and its associated method before the execution of any other application in the JVM. For which the Java Agent must be indicated at JVM startup.
- agentmain: It will allow the execution of the Java Agent once the JVM is already running an application. This association must be done programmatically.
The creation of the MANIFEST.MF file must be done manually, although it can be included in the associated jar through a maven plugin. In this file, we will have to configure different properties. On the one hand, which is the class that contains the Java Agent method and properties about its behavior. The main ones are:
- Premain-Class: To indicate the class that implements the premain method.
- Agent-Class: To indicate the class that implements the agentmain method.
- Can-Redefine-Classes: To indicate whether or not the Java Agent can redefine the classes it interferes with.
- Can-Retransform-Classes: This allows to indicate if the Java Agent can or not transform the interfering class.
Once we have developed the method that performs the logic and we have configured the MANIFEST file correctly, we can generate the library. To do this we can make use of a couple of maven plugins, which will make our work easier:
- maven-shade-plugin: it allows us to create the library including the dependencies in case it has them.
- maven-jar-plugin: allows us to associate the MANIFEST file to the library to be created.
The third step, once we have the library, is to associate it with the JVM. As we have indicated previously, this can be done in two ways:
If we have implemented the premain method and configured the Premain-Class property, we will have to indicate the library at the time of starting the application we want to interfere with. Example:
java -javaagent:/full/path/to/javaAgent.jar -jar app.jar
In case we implement the agentmain method and configure the Agent-Class property, we must associate the Java Agent programmatically through the Java Attach API.
File agentFile = Paths.get("agent.jar").toFile();
VirtualMachine jvm = VirtualMachine.attach(VirtualMachine.list().get(0).id());
jvm.loadAgent(agentFile.getAbsolutePath());
// jvm.detach(); //to finish association
Now we know the theory so we can go to practice. Let’s see a couple of examples. One simple and basic and the other a little more complex.
In the simplest one, we will only implement the premain method, and we will set a couple of logs associated with its execution.
If we execute the typical static void main method, for example, and add the java agent as a JVM argument, we can see an output similar to this one:
[INFO ] 2021-03-04 [main] BasicAgentExample - Start from premain. agentArgs: null
[INFO ] 2021-03-04 [main] BasicAgentExample - Classname: sun/misc/PostVMInitHook....
As we can see, if we look a little at the methods available in the Instrumentation object, we will have the possibility to transform or redefine the functionality of a class. But this must be done with complex operations of modification of the bytes of those classes.
In the following example, more complex, we will transform the class that we are going to interfere with. But as we have indicated these operations are complex. So, to carry them out more easily we will make use of the Byte Buddy library.
The idea of this example is to create a Java Agent that allows us to measure how many times a certain method has been invoked. Besides, we will also filter which classes we want to intervene in, avoiding that tasks are performed on classes that do not interest us, as for example those from the JVM. In our example, we will intervene only the methods in those classes that extend an abstract class.
First, we implement the premain method:
The AgentBuilder constructor will allow us to indicate which elements we want to intervene through the type method and the ElementMatchers utility. The transform method will allow us to indicate on which element of that class we want to perform the transformation, in this case on the methods of the classes that have passed the filter.
The next step is to create an advice, which is an element associated with aspect programming, AOP, that allows us to execute our code at a specific point. The advice we create can be of different types, depending on when we want them to be taken into account with respect to that fixed point: before or after that specific point. In the example, we will execute our advice before accessing the methods that fulfill the filters.
Now we only have to create the class we want to instrumentalize.
To test our Java Agent, we will execute the class shown above. Indicating correctly the Java Agent library and the class that implements this agent in the MANIFEST file. The output will be similar to this:
[INFO ] 2021-03-04 [main] - CounterMeasureAdvice: Enter into: public static int ExampleThree.fibonacci(int)
[INFO ] 2021-03-04 [main] - Origin public static int ExampleThree.fibonacci(int) invoked 14 times
[INFO ] 2021-03-04 [main] - CounterMeasureAdvice: Enter into: public static int ExampleThree.fibonacci(int)
[INFO ] 2021-03-04 [main] - Origin public static int ExampleThree.fibonacci(int) invoked 15 times
As you can see, the use of Java Agent has many possibilities. Even with the Byte Buddy library, you can do complex things in a simple way.