Make writing a habit together! This is the fifth day of my participation in the “Gold Digging Day New Plan · April More text Challenge”. Click here for more details.

Agent technology in Java allows us to conduct Agent without invasively, which is most commonly used in program debugging, hot deployment, performance diagnostic analysis and other scenarios. Skywalking, a popular distributed link tracking project, captures logs through probe technology and reports data to OAP observation and analysis platform.

Introduction to Java Agent technology

Java Agent literally translates to Java Agent and is often referred to as Java Probe technology.

Java Agent, introduced in JDK1.5, is a technology that can modify Java bytecode on the fly. Classes in Java are compiled to form bytecodes that are executed by the JVM, which retrieve the bytecodes before they are executed, and modify the bytecodes through bytecode converters to perform additional functions.

The Java Agent is a JAR package that cannot run independently and works through a JVM process attached to the target program. At startup, you simply add the -JavaAgent parameter to the target program’s startup parameters to add the ClassFileTransformer bytecode converter, which is equivalent to adding an interceptor before the main method.

This section describes the Java Agent functions

The Java Agent provides the following functions

  • The Java Agent intercepts and modifies Java bytecode before loading it.
  • The Java Agent can modify loaded bytecode while the Jvm is running;

Application scenarios of the Java Agent

  • IDE debugging functions, such as Eclipse, IntelliJ IDEA;
  • Hot deployment features such as JRebel, XRebel, spring-Loaded;
  • Online diagnostic tools like Btrace, Greys, and Arthas from Ali;
  • Various performance analysis tools, such as Visual VM, JConsole, etc.
  • Full link performance testing tools, such as Skywalking, Pinpoint, etc.

Implementation principles of Java Agent

Before understanding the implementation principle of Java Agent, you need to have a clear understanding of the Java class loading mechanism. The first method is premain before the MAN method is executed, and the second method is Attach in the JVM. Attach is based on JVMTI.

The main interception is to modify the bytecode before the class is loaded

Here are some of the key terms:

  • JVMTI is the JVM Tool Interface, which is a set of interfaces exposed by the JVM for users to use for extension. JVMTI is event-driven. Every time the JVM executes certain logic, it will trigger some event callback interfaces

    JVMTI is the unified basis for implementing tools such as Debugger, Profiler, Monitor, And Thread Analyser. It is implemented in all mainstream Java virtual machines

  • JVMTIAgent is a dynamic library that uses some of the interfaces exposed by JVMTI to do things that we want to do, but cannot do normally. However, to distinguish it from ordinary dynamic libraries, it usually implements one or more of the following functions:

    • The Agent_OnLoad function, if the agent is loaded at startup, is set by JVM parameters
    • The Agent_OnAttach function is called during loading if the agent is not loaded at startup, but we attach to the target process and then send the corresponding target process a load command to load it
    • The Agent_OnUnload function is called when an Agent is unloaded
  • Javaagent depends on the Instrument JVMTIAgent (the corresponding dynamic library in Linux is libinStrument. So). There are also individual named JPLISAgent(Java Programming Language Instrumentation Services Agent), specifically for the Java Language to provide support for staking Services

  • Instrument implements both Agent_OnLoad and Agent_OnAttach methods, which means that when used, agents can be loaded either at startup or dynamically at runtime. During startup loading, instrument Agent can be indirectly loaded by using the -JavaAgent: JAR package path. During runtime dynamic loading relies on the ATTACH mechanism of the JVM, and the agent can be loaded by sending the load command

  • JVM Attach is an inter-process communication function provided by the JVM, which allows one process to transfer commands to another process and perform internal operations, such as thread dump. Jstack is executed for this purpose, and parameters such as PID are then passed to the thread that needs to dump

Java Agent case

We take the execution time of the printing method as an example, through the Java Agent to achieve.

First, we need to build a compact Maven project in which we build two Maven subprojects, one for implementing the plug-in Agent and one for implementing the test object.

We import the packages that are common to both projects in the parent application

    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.28. 0-GA</version>
        </dependency>
    </dependencies>
Copy the code

First we build the test object

/ / start the class
public class APPMain {

    public static void main(String[] args) {
        System.out.println("APP start!!"); AppInit.init(); }}// The simulated application initializes the class
public class AppInit {

    public static void init(a) {
        try {
            System.out.println("Initializing APP...");
            Thread.sleep(1000);
        } catch(InterruptedException e) { e.printStackTrace(); }}}Copy the code

Then we start the program, test if it works, and when it works, we start building the probe

In the probe program we need to write, change the Transformer of the original class, through the custom Transformer class to complete the output method execution time function,

First, the Agent program entry is constructed

public class RunTimeAgent {

    public static void premain(String arg, Instrumentation instrumentation) {
        System.out.println("Probe activated!!");
        System.out.println("Probe input parameter:" + arg);
        instrumentation.addTransformer(newRunTimeTransformer()); }}Copy the code

This method is used when each Class is loaded. We can use className to intercept the specified Class, and then use javassist to process the Class. The idea here is similar to reflection, but more powerful than reflection, and you can dynamically modify the bytecode.

Javassist is an open source library for analyzing, editing, and creating Java bytecode.

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class RunTimeTransformer implements ClassFileTransformer {

    private static final String INJECTED_CLASS = "com.zhj.test.init.AppInit";

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<? > classBeingRedefined, ProtectionDomain protectionDomain,byte[] classfileBuffer) throws IllegalClassFormatException {
        String realClassName = className.replace("/".".");
        if (realClassName.equals(INJECTED_CLASS)) {
            System.out.println("Intercepted class name:" + realClassName);
            CtClass ctClass;
            try {
                // Using Javassist, get the bytecode class
                ClassPool classPool = ClassPool.getDefault();
                ctClass = classPool.get(realClassName);

                // Get all method instances of the class, or select methods to enhance
                CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
                for (CtMethod method : declaredMethods) {
                    System.out.println(method.getName() + "Method intercepted");
                    method.addLocalVariable("time", CtClass.longType);
                    method.insertBefore("System.out.println(\"-- start executing --\");");
                    method.insertBefore("time = System.currentTimeMillis();");
                    method.insertAfter("System.out.println(\"-- end execution --\");");
                    method.insertAfter("System.out.println(\" Runtime: \" + (system.currentTimemillis () - time));");
                }
                return ctClass.toBytecode();
            } catch (Throwable e) { // Use Throwable instead of ExceptionSystem.out.println(e.getMessage()); e.printStackTrace(); }}returnclassfileBuffer; }}Copy the code

We need to configure and compile the packaged plug-in in Maven, so that we can easily generate the Agent JAR package with Maven

<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> The < version > 3.5.1 track of < / version > <! -- Specifies the JDK version for Maven compilation. Maven2 uses JDk1.3 --> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> The < version > 3.2.0 < / version > < configuration > < archive > <! Manifest.mf --> < MANIFEST > <addClasspath>true</addClasspath> </ MANIFEST > <manifestEntries> < Menifest Version - > 1.0 < / Menifest - Version > < Premain - Class > com. ZHJ. Agent. RunTimeAgent < / Premain - Class > <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin> </plugins> </build>Copy the code

If you do not need to create a meta-INF/manifest.mf file under Resources, you will need to specify this file. The file contents are as follows

Manifest-Version: 1.0
Premain-Class: com.zhj.agent.RunTimeAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

Copy the code

Notification file manifest.mf

  • Manifest-Version

    File version

  • Premain-Class

    The class that contains the premain method (the full pathname of the class) runs the proxy before the main method

  • Agent-Class

    The class that contains the AgentMain method (the full pathname of the class) can modify the class structure once main starts

  • Boot-Class-Path

    Sets the list of paths searched by the bootstrap classloader. When the platform-specific mechanism for finding a class fails, the bootstrap classloader searches these paths. Search the paths in the order listed. The paths in the list are separated by one or more Spaces. (optional)

  • Can-Redefine-Classes true

    Represents classes required by this proxy can be redefined, default is false (optional)

  • Can-Retransform-Classes true

    Represents classes required by this agent that can be reconverted, default is false (optional)

  • Can-Set-Native-Method-Prefix true

    Indicates that the native method prefix required by this proxy can be set, default is false (optional)

  • .

Finally, the JAR package of Agent is generated by Maven, and then the initiator of the test target program is modified to add JVM parameters

– javaAgent :F:\code\myCode\agent-test\runtime-agent\target\ runtime-agent-1.0-snapshot. jar=hello

End result:

This completes non-intrusive proxying.