What is a Java Agent?

If you are a java developer reading this, you probably have little to no idea about Java agents. Java agents are a special type of class that can intercept applications running on the JVM (Java Virtual Machine) and modify their bytecode using the Java Instrumentation API. Although Java agents were introduced way back in JDK 1.5, to this day, many Java developers are still not sure about how to write a Java agent.

In this article, you will get a quick overview of Java agents. What are they? Why should you use them and how to write one yourself?

What is Java Instrument API?

Java agents are part of the Java Instrumentation API. Instrumentation is a technique of making changes in an existing application by adding a piece of Java code to it. It provides a mechanism to modify the bytecodes of the application without actually editing its source code file. This is an extremely powerful feature with a range of functional benefits that we will explore further below.

It provides the ability to redefine or retransform classes during run-time. Developers can change method bodies, the constant pool and even attributes. But the redefinition or retransformation must not include adding, removing or renaming fields or methods. It should also not include changes in the signatures of methods or inheritance.

Some other popular usages of Java agents include profiling, aspect oriented programming or mutation testing and many other things. Instrumentation can be done in both ways, statically and dynamically as well as can be done at  compile time or runtime.

It is extremely crucial to ensure that retransformed or redefined class bytecode must be checked, verified and installed by the developer right after applying them. If the resulting bytecode turns out to be incorrect, it will result in an exception that may crash JVM completely.

See Also: Developing REST/API: What Do Most Developers Get Wrong?

Java agent and instrumentation APIs are present in the package called java.lang.instrumentation.

Implementing a Java Agent

The entry point for Java agents is java.lang instrument package, which provides all the services that allow the agents to work with programs running on the JVM. The package is very simple. It contains some exception classes, a data class, the class definition, and two interfaces. We need to implement only one of those two interfaces for writing a java agent, that is “classFileTransformer” interface. We will be discussing it later in this article.

How to Define a Java Agent?

There are two ways to define java Agents, Static and Dynamic.

1. Static agent:

A Static agent is built as a jar file, and when the Java application is started, a special JVM argument, javaagent is first passed. Then we provide the location of the agent jar and the rest is done by JVM.

1. $ java -javaagent:<agent jar file path> -jar < packaged jar file path that you want to intecept>

 It is also required to add a special manifest entry, which is called the pre-main class.

1. Premain-Class : org.example.JavaAgent

 The code below depicts the class:

1. public class JavaAgent {
2. /*
3. * This method will be called as the JVM initializes,
4. */
5. public static void premain(String agentArgs, Instrumentation instrumentation) throws InstantiationException {
6. interceptingClassTransformer interceptingClassTransformer = new InterceptingClassTransformer();
7. interceptingClassTransformer.init();
8. instrumentation.addTransformer(interceptingClassTransformer);
9. }
10. } 

 Premain method takes two arguments: 

  • agentArgs: String arguments, that the user has chosen to pass as arguments to the Java agent invocation.
  • instrumentation: It is from the java.lang instrument package and it is required to add a new ClassFileTransformer object, which contains the actual logic of the Agent.

2. Dynamic agent

For defining a dynamic agent, instead of using instrumentation, you can write a small code that connects to an existing JVM and instruct it to load a certain agent.

1. VirtualMachine vm = VirtualMachine.attach(vmPid);
2. vm.load(agentFilePath);
3. vm.detach();

The argument agentFilePath is the same as in the static agent approach as it has to be the file name of the agent jar.

Java Agent Methods

Java agents are nothing more than just a normal Java class. The only difference is that you have to use some specific conventions while declaring a Java agent. The first convention is at the entry point of the agent. The entry point consists of a method called “premain,” see the code snippet below:

1. public static void premain(String agentArgs, Instrumentation inst) 

The reason behind it is that the premain method of every Java agent is called as soon as the JVM initializes. Once the Java Virtual Machine has initialized, each “premain” method of every Java agent is called depending on how the agents were specified at the start of JVM. After initialization, the actual Java application, the main method is called.

Every premain method must resume execution normally for the application to proceed to the start-up phase.

In case, the class does not implement the premain method, the JVM will then try to look for another overloaded version that is,

1. public static void premain(String agentArgs) 

It is important to note that each premain method must return for the code to proceed to the start-up phase.

The java agent should have another method as well called agentmain. Following are the two signatures for the method that can be used:

public static void agentmain(String agentArgs, Instrumentation inst) 
public static void agentmain(String agentArgs) 

These methods are used when the agents are called after the JVM initializes.

Agent Manifest File

Manifest files are usually located in a folder called MANIFEST.MF. It contains various metadata related to package distribution. Although they are usually not required, you will be needing them with Java agents. Some of the attributes included in the MANIFEST.MF files are as follows:

  • Premain-class: It defines the agent class. JVM will abort if this attribute is not defined.
  • Agent-class: It defines the process to start Java agents after JVM has started. The agents will not start if this attribute is undefined.
  • Can-Redefine-Classes: This defines the ability to redefine classes by the agent. The value can be true orfalse.
  • Can-Retransform-Classes: This defines the ability to retransform classes by the agent. The value can be true or false.
  • Can-Set-Native-Method-Prefix: This defines the ability to set a native method prefix by the Java agent. The value can be either true or false.
  • Boot-Class-Path: This defines the bootstrap class loader’s search path list.

Class Transformation

It is the interface required for a Java agent to transform the classes.

1. public interface ClassFileTransformer {
2. byte[] transform(ClassLoader loader, 
3. String className, 
4. Class<?> classBeingRedefined,
5. ProtectionDomain protectionDomain, 
6. byte[] classfileBuffer) 
7. throws IllegalClassFormatException;
8. }

The ClassFile Transformation contains the following things:

The first most important argument here is the className. The main purpose of className is to find and differentiate between the class you want to intercept and others by their names. Likely, you may not want to intercept every class in your application. 

ClassLoader is mostly used in environments that do not have a flat class space for basic applications. It is mostly used when you are dealing with a modular platform.

classfileBuffer is the current definition of the class before it is instrumented. It is required to read this byte array using libraries to intercept your code and then it has to transform back to bytecode again to return.

There are numerous byte code generation libraries available. You need to do some research before selecting a library based on factors like whether it is a high-level API or low-level API, the community size, and the license.

We will be using Javassist. It is a third-party library by top Java developers and provides a nice balance between high-level and low-level APIs. Since it is triple licensed so it should be available for almost any Java developer. 

1. @Override
2. public byte[] transform(ClassLoader loader, …)
3. throws … {
4. byte[] byteCode = classfileBuffer;
5. // It will only intercept one class. If you want to intercept all the classes then it just needs to remove the conditional statement below.
6.	 
7. if (className.equals("Example")) {
8. try {
9. ClassPool classPool = scopedClassPoolFactory.create(loader, rootPool,
10. ScopedClassPoolRepositoryImpl.getInstance());
11. CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
12. CtMethod[] methods = ctClass.getDeclaredMethods();
13. for (CtMethod method : methods) {
14. if (method.equals("main")) {
15. method.insertAfter("System.out.println(\"We are Logging using this java Agent\");");
16. }
17. }
18. byteCode = ctClass.toBytecode();
19. ctClass.detach();
20. } catch (Throwable ex) {
21. log.log(Level.SEVERE, "Error occurred in transforming this class: " + className, ex);
22. }
23. }
24. return byteCode;
25. }
26. ctClass.detach();
27. } catch (Throwable ex) {
28. log.log(Level.SEVERE, "Error occured in transforming this class: " + className, ex);
29. }
30. }
31. return byteCode;
32. }

Here, from the classPool, we can directly get to the class by bypassing the classfileBuffer as we want to work with the method main. This code loop through all the method in the class definition and get to the class we wanted. It does not require us to work on the bytecode at all. We can simply pass it a piece of Java code, and then the Javassist library will compile it and give us the definition after generating the new bytecode.

There are three ways possible to insert a piece of Java code into the method:

  • insertAfter(..) – You can insert bytecode at the end of the body.
  • insertAt(..) – you can insert bytecode at the specified line in the body.
  • insertBefore(..) – You can insert bytecode at the beginning of the body.

Start Writing a Java Agent

The first thing we need to write a Java agent is the agent class. The agent class is a simple Java class that implements the methods we have discussed earlier in this article. Your Java Virtual Machine will always start with trying to locate the class that is specified in -javaagent parameter to the Virtual Machine, and the premain method will get executed first before the main method.

See this code below:

1. import java.lang.instrument.Instrumentation;
2. public class JavaAgent {
3. public static void premain(String args, Instrumentation instrumentation){
4. ClassLogger transformer = new ClassLogger();
5. instrumentation.addTransformer(transformer);
6. }
7. }

Notice the Instrumentation parameter that we have access to in the premain method. It will allow us to register ClassFileTransformer. A registered ClassFileTransformer can then intercept the loading of all application classes and can have access to their bytecode.

In this particular case, as we already discussed, ClassFileTransformer will also be used to transform the bytecode of the application classes and can make the JVM load behave not as intended bytes. After registering the ClassLogger transformer, let’s see how it is implemented.

Implementing a ClassLogger Transformer

1. public class ClassLogger implements ClassFileTransformer {
2. @Override
3. public byte[] transform(ClassLoader loader,
4. String className,
5. Class<?> classBeingRedefined,
6. ProtectionDomain protectionDomain,
7. byte[] classfileBuffer) throws IllegalClassFormatException {
8. Path path = Paths.get("classes/" + className + ".class");
9. Files.write(path, classfileBuffer); 
10. } finally { return classfileBuffer; }
11. }
12. }

The code is very straightforward and simple here. The transform method now has access to the application class name and the bytes that correspond to the body of the class. In this particular scenario, we will dump the bytes into a file.

Now the last thing is packaging a jar file and supplying a manifest file along with it. It will specify our agent as the Premain-Class. 

In the code below, you will see that it is the Gradle build file part that creates the Jar file:

1. jar {
2. archiveName = "${rootProject.name}-${rootProject.version}.jar"
3. manifest {
4. attributes(
5. 'Premain-Class': 'JavaAgent',
6. 'Can-Redefine-Classes': 'true',
7. 'Can-Retransform-Classes': 'true',
8. Can-Set-Native-Method-Prefix': 'true',
9. 'Implementation-Title': "ClassLogger",
10. 'Implementation-Version': rootProject.version
11. )
12. }
13. }

Lastly, Building your javaagent jar

All is done and we are now ready to build your javaagent jar. All you have to do is to supply the -javaagent parameter.

Here is the code:

1. java -jar newapp.jar
2. java -javaagent:/path/to/agent.jar -jar newapp.jar 

Conclusion

In this article, we have looked at a very powerful tool for Java developers: the Java agents. Java agents are one of the most versatile tools as it allows the developers to access classes loaded into the JVM. This article is barely an introduction to Java agents.

There is an abundance of problems and challenges you can solve with them when it comes to rewriting the complex Java code especially when you do not have access to the source code. Hopefully, you will now be able to write your Java agents. However, it is not a single developer’s task. Writing some reliable and optimized Java agents is a task performed by a dedicated team of Java developers but you can now further learn and practice yourself to be one of them.

Author

Shaharyar Lalani is a developer with a strong interest in business analysis, project management, and UX design. He writes and teaches extensively on themes current in the world of web and app development, especially in Java technology.

Write A Comment