Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

Life is too short to have a dog

First, the JVM “Agent” — Java Agent

In daily development, we often need to write some monitoring codes that are not highly relevant to business, such as log printing at the method exit and method execution time statistics. For Java programs, the most convenient is to use AOP in Spring to complete the corresponding monitoring program writing. So how would a pure Java application write a monitor before the Spring framework was born? The following will introduce a tool that comes with the JDK — Java Agent.

If AOP is a proxy at the code level, Java Agent is a proxy at the JVM level. By using the API provided by the JVM (i.e., THE JVM TI, in this case the corresponding call is made through the Instrumentation class), the Java Agent can modify/replace the class file that has been loaded, so that we can add some non-business monitoring code on top of the original code. Such as method time statistics.

The JVM TI (JVM TOOL INTERFACE) is a set of tools provided by the JVM to operate on the JVM, or to put it more kindly, the JVM provides a backdoor method to the children. JVMTI allows developers to perform multiple operations on the JVM by registering event hooks through an interface and firing predefined hooks in response to JVM events. Events include class file loading, exception generation and catching, thread start and end, critical section entry and exit, member variable modification, GC start and end, method call entry and exit, critical section contention and wait, VM start and exit, and so on.

Similar to AOP, Java Agent injection has a “cut point”, that is, the timing of execution. It is important to note that the timing of Java Agent execution is closely related to how it is started. There are two ways to start Java Agent in the JDK: statically loading and dynamically Attch. Details are as follows:

  • Static loadingIn JDK1.5, The Java Agent can only be started statically loaded, which we often see used-javaagent:xxx.jarTo start the Java Agent through the command line. To load the Java Agent in this way, ensure that theAgentStatic public is provided inpremianMethods. The pointcut for Java Agent injection launched statically will be inmainMethod is executed before, butpremainMethods andmainMethods all belong to the same thread, i.emainThread;
  • Dynamic Attach: Starting with JDK1.6,InstrumentationSupport for dynamic change of class definitions at runtime, when the Java Agent can passAttach APIDynamically loaded while the JVM process is running. The Attach API is actually an interjvm communication capability provided by the JDK that allows developers to dynamically load Java Agent modules into a specific JVM process after the target JVM has been started using the Attach API. Start the Java Agent in dynamic loading modeAgentStatic public is provided inagentmainMethods;

Either using static loading or using dynamic Attach simply provides a way to cut into the target Java process and a pointcut. If you want to really operate on class files, you also need to use Instrumentation classes. The bottom layer of the Instrumentation class actually depends on the JVM TI mentioned above, with the help of the JVM TI provides a backdoor Instrumentation class has been loaded to modify, redefine the ability, These capabilities are exposed through the implementation classes of ClassFileTransformer. Note that class file converters registered in Instrumentation will be called when the class is loaded or redefined, in addition to its own dependent classes, all future class definitions will be viewed by the converter. Let’s take a quick look at how Java Agent is used with an example.

A simple example

Either static startup or dynamic Attach, you need to create an Agent class.

DemoAgent

/**
 * Demo Agent
 *
 * @author brucebat
 * @version 1.0
 * @sinceCreated at 2021/9/27 4:45pm */
public class DemoAgent {

    /** ** to process before the main thread starts **@paramAgentArgs Proxy request parameter *@paramInstrumentation pile * /
    public static void premain(String agentArgs, Instrumentation instrumentation) {
        System.out.println("Premain: This is an experimental DemoAgent.");
        System.out.println("premain: " + Thread.currentThread().getName() + ", threadId: " + Thread.currentThread().getId());
        System.out.println("Premain, whether the current thread is a protection thread:" + Thread.currentThread().isDaemon());
        instrumentation.addTransformer(new DefineTransformer("premain"));
    }

    public static void agentmain(String agentArgs, Instrumentation instrumentation) {
        System.out.println("Agnetmain: This is an experimental DemoAgent with a thread name:" + Thread.currentThread().getName());
        System.out.println(Thread.currentThread().getThreadGroup().getName() + ", threadId: " + Thread.currentThread().getId());
        System.out.println("Whether the current thread is a protection thread:" + Thread.currentThread().isDaemon());
        instrumentation.addTransformer(new DefineTransformer("agentmain"));
    }

    /** * all class files loaded into the JVM are converted */
    static class DefineTransformer implements ClassFileTransformer {

        private final String name;

        public DefineTransformer(String name) {
            this.name = name;
        }

        @Override
        public byte[] transform(ClassLoader loader, String className, Class<? > classBeingRedefined, ProtectionDomain protectionDomain,byte[] classfileBuffer) {
            System.out.println(name + " load Class:" + className + ", ThreadName: " + Thread.currentThread().getName() +
                    ", ThreadGroupName: " + Thread.currentThread().getThreadGroup().getName() + ", whether the thread is protected:" + Thread.currentThread().isDaemon());
            // This is just a gateway to the corresponding class file,
            // If you want to make actual changes to the class file you can use javaAssist or Byte Code,
            // You can also modify the above byte data directly
            returnclassfileBuffer; }}}Copy the code

Two methods are written here, a premain method and an AgentMain method, and a classfile converter is also written. In order to generate the module containing the corresponding Agent class, we also need to define the corresponding information in manifest.mf:

Manifest-version: 1.0 premain-class: agent.DemoAgent agent-class: agent.DemoAgent can-re-class: agent. true Can-Retransform-Classes: trueCopy the code

You also need to add the following packaging information to the POM file:

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <! All dependencies of the current project will be packaged into the target JAR -->
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <! MANIFEST MANIFEST MANIFEST MANIFEST MANIFEST MANIFEST MANIFEST MANIFEST
                        <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>attached</goal>
                        </goals>
                        <phase>package</phase>
                    </execution>
                </executions>
            </plugin>
Copy the code

1. Static boot mode

After the packaging is completed, the static startup mode only needs to add the corresponding instruction -javaAgent :{path}/xxx-jar-with-dependencies. Jar. Instead of showing all the results, we’ll just look at some of them:

Premain: this is an experimental DemoAgent premain: main, threadId: 1 premain, whether the current thread is a guard thread: falseCopy the code

You can see that the premain method actually belongs to the same thread group as the main method, the main thread.

2. Dynamic Attach

The dynamic Attach method needs to load the Java Agent module (JAR) into the specified JVM process by encoding as follows:

/ * * *@author brucebat
 * @version 1.0
 * @sinceCreated at 2021/10/108:21 PM */
public class TestAttach {

    public static void main(String[] args) throws IOException, AttachNotSupportedException {
        List<VirtualMachineDescriptor> virtualMachineDescriptors = VirtualMachine.list();
        for (VirtualMachineDescriptor virtualMachineDescriptor : virtualMachineDescriptors) {
            System.out.println("vm displayName : " + virtualMachineDescriptor.displayName());
            if (virtualMachineDescriptor.displayName().equals("App")) {
                System.out.println(virtualMachineDescriptor.id());
                The final point here is that instead of using the command line to specify the JVM process to monitor, you can specify the name of the virtual machine to monitor
                VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor.id());
                try {
                    virtualMachine.loadAgent("{path}/xxx-SNAPSHOT-jar-with-dependencies.jar");
                    System.out.println("ok");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    virtualMachine.detach();
                }
            }
        }
    }
}
Copy the code

Here, too, are some of the results:

Agnetmain: this is an experimental DemoAgent with the thread name: Attach Listener system, threadId: 11 Whether the current thread is a guard thread: trueCopy the code

You can see that the Java Agent started by Attach runs as a guard thread.

Three, application scenarios

In addition to the ability to dynamically modify class files, Java Agent has the distinct advantage of being completely application independent. With Java Agent, developers can add some monitoring capabilities to their applications in a way that is almost non-perceptive and non-intrusive. This is what SkyWalking, the current popular APM tool, does.

The usage scenarios of Java Agent are also mentioned in the official documents:

Examples of such benign tools include monitoring agents, profilers, coverage analyzers, and event loggers.

Examples of such benign tools include monitor agents, profilers, override profilers, and event loggers.

This is not shown in the above code, but it is noted in the comments of the code. The transform of the classfile converter can be changed directly to the byte array of the original classfile, the classfileBuffer in the method input parameter (you can use the ASM tool, You can also use Javassist to modify the source code (this method is recommended because it is more comfortable).

Four,

This article briefly explains the basic concepts and simple use of Java Agent. If you want to further understand the execution process of Java Agent, you can query the source code related to JVM. In the follow-up study, the author will try to explain the specific execution process further.