How to Use Java Agents with ASM Bytecode Framework?

Image for post
Image for post

By Zhang Shuai (Xunche), Senior Development Engineer of International Mid-end Business Division.

The author is responsible for the research and development of logistics expression and performance and is keen on the research of middleware technology.

1) Overview

It is surely possible to modify the bytecode that is being loaded using Java agents. There are two ways to realize a Java agent: the premain method and the agentmain method. The first method is realized by the premain function that runs prior to the main function. The second method is realized using the attach API during program runtime.

Before we get into agents, let’s take a brief look at Instrumentation. Provided by JDK 1.5, Instrumentation is an API for intercepting class loading events and modifying the bytecode. Its major functions are as follows.

2) Premain

The premain method is the most common way to realize an agent. It runs prior to the main function. To run the agent, pack it into a JAR package and modify the command as follows.

There are two overriding premain functions. During startup, JVM initially attempts to call the first function. If this function does not exist, JVM calls the second function.

2.1 A Simple Example

The following example explains how to use the premain function in a program. First, prepare test code, which is designed to run the main function and output “hello world”.

Next, prepare agent code, which is designed to run the premain function and output the input parameters.

To run the agent, set the value of Premain-Class in the META-INF/MANIFEST.MF file as the agent path, and then pack the agent into a JAR package. Alternatively, use IDEA to export the agent as a JAR package.

Next, compile and run the test code. To simplify this process, I have placed the compiled class under the same directory as the agent JAR package.

As the output shows, the premain function in the agent runs prior to the main function.

2.2 A Complicated Example

Now that we have learned about agents through the previous example, let’s take a look at the practical usage of an agent. The following example uses the agent to monitor functions. The general idea is as follows:

The agent inserts timestamp recording code at the execution entry and exit of a non-JDK function by using ASM, and after the execution of the function, the time cost can be calculated from the timestamps.

First, let’s look at the test code. The main method calls the sayHi function, which outputs “hi, xunche” and sleeps for a random period of time.

The agent then uses ASM to insert the code to calculate the execution time cost of a function. When JVM loads a class, the code is inserted into each function of the class. See the code as follows, and JDK’s built-in ASM is used here. Also, use ASM’s official class libraries for the same purpose.

The preceding code is a bit long, and you can skip the ASM section. We register a transformer using instrumentation.addTransformer. In the transformer, we have overridden the transform function. The classfileBuffer input parameter in the function is the original bytecode, and the return value is the actual bytecode to be loaded. The onMethodEnter function calls TimeHolder's start function while passing in the current function's name.

The onMethodExit calls TimeHolder’s cost function while passing in the current function’s name, and outputs the return value of the cost function.

The following shows the code for TimeHolder.

The agent code is now finished. The ASM part is not the focus in this article, and you may learn more about it in a specific article about ASM subsequent editions. After running the agent, let’s look at the current code in the class. Compared to the original test code, each function has been appended with the monitoring code.

3) Agentmain

As shown in the previous example, the premain method modifies the bytecode before the startup of an application to achieve desired functions. Different from the premain method, the agentmain method intercepts the loading of a class during the runtime of a Java application through the attach API provided by JDK. The agentmain method is explained in the following example.

3.1 Practice Example

The goal of this example is to realize a tool to remotely collect function calling information from a running Java process. You may be thinking that it sounds like BTrace. In fact, BTrace is realized in the same way. Due to limited time, the code in this example is rough, and the point is to grasp the idea behind it rather than the details of the code.

The implementation idea is as follows:

  • The agent modifies the bytecode of the functions of a specified class to collect the input parameters and return values of the functions. Then, the agent sends the collected results to the server through a socket.
  • The server accesses the running Java process through the attach API and loads the agent.
  • The server loads the agent while specifying the target class and functions.
  • The server opens a port to receive requests from the target process.

Let’s look at the test code first. Every 100 milliseconds, it runs the sayHi function and sleeps for a random period of time.

Next, let’s look at the code for the agent. Similar to the previous agent that monitors the time cost of a function, this agent inserts the code at the exit of a function to collect the input parameters and return values of the function by using ASM. Then, the agent sends the collected information to the server through a socket.

The preceding code shows the code for the agent. The onMethodExit function obtains the request parameters and response parameters and calls Sender’s send function. In this example, the idea about accessing local variable tables comes from the LocalVariableTableParameterNameDiscoverer of Spring.

Next, let’s look at the code for Sender.

Considering that it’s easy to understand the Sender code. Next, let’s look at the code for the server. The server opens a port for listening, accepts requests, and loads the agent through the attach API.

After running the preceding program, note that the server has received the request and response information from org.xunche.app.HelloTraceAgent.sayHi.

4) Summary

This article described the basic usage of Java agents using premain and agentmain methods and implemented a tool to collect the calling information of a function from a running process. In fact, the functions of agents are far more extensive than what is described here. Agents are used in many objects, such as BTrace, Arms, and Springloaded.

The views expressed herein are for reference only and don’t necessarily represent the official views of Alibaba Cloud.

Original Source:

Written by

Follow me to keep abreast with the latest technology news, industry insights, and developer trends.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store