“This is the first day of my participation in the Gwen Challenge in November. See details of the event: The last Gwen Challenge in 2021”.

One, foreword

The so-called Java Agent, its functions are based on java.lang. Instrument classes to complete. Instrument provides the capability to allow Java programming language agents to detect programs running on the JVM by modifying bytecode. Instrument is located in rT.jar, under the java.lang. Instrument package. Instrument can be used to detect or assist programs running in the JVM. Even replace and modify the loaded class. This is also known as hot deployment, hot loading. Instrument: The loading behavior of a class is interfered with (modify and replace)

Instrument is implemented based on the Java Virtual Machine Tool Interface (JVMTI), which is a set of local programming interfaces provided by Java VMS for jVM-related tools. JVMTI is event-driven and simply adds hooks to the JVM runtime that allow developers to customize the functionality.

What open source software uses this technology? Github.com/alibaba/art… Github.com/apache/skyw… Wait…

2. Preliminary exploration of relevant API

2.1 Instrumentation

Java. Lang. Instrument package under the key class for: Java. Lang. Instrument. Instrumentation. This interface provides a series of alternative methods for transforming class definitions. Let’s take a look at the main methods of the interface to illustrate the following:

addTransformer

Used to register transformer. All future class definitions will be visible to Transformer, except for the definition of the class on which any registered converter depends. Transformer will be called when the class is loaded or re-defined (redefineClasses can trigger). If canRetransform is true, this means that transformer is also called when they are retransformed (via retransformClasses below). AddTransformer has the following two overload methods:

void addTransformer(ClassFileTransformer transformer,boolean canRetransform)

void addTransformer(ClassFileTransformer transformer)
Copy the code

redefineClasses

void redefineClasses(ClassDefinition... definitions)
              throws ClassNotFoundException,
                     UnmodifiableClassException
Copy the code

This method is used to replace class definitions that do not reference existing class file bytes, as is done when recompiling from source code to fix and continue debugging. This method operates on a series of ClassDefinitions to allow interdependent changes to multiple classes at the same time (a redefinition of class A may require a redefinition of class B). If the target class is executing on Redifine, the original bytecode behavior will be executed. When a new call is made to the behavior, the new behavior after re-define will be used.

Note: This Re-define does not trigger the initialization behavior of the class

We can re-define method bodies, constant pools, attributes, but we can’t add, remove, rename methods, methods, input parameters, change method signatures, or change inheritance. Of course, in future releases, these limitations may not exist.

Class file bytes are not checked, validated, and installed before conversion, and this method throws an exception if the resulting byte is wrong. No class will be redefined if an exception is thrown

retransformClasses

Converts to classes already loaded by the JVM, which can be converted by a registered ClassFileTransformer when the class is initially loaded or when a class is redefineClass; However, the transform behavior is not triggered for classes that are already loaded and therefore cannot be listened to by our agent, so we can fire an event using retransformClasses. This event can be captured by ClassFileTransformer to transform these classes.

This method will call its transform method for every single one registered with addTransformer where canRetransform is true, and the converted class file bytes will be installed as the new definition of the class, thus having the new behavior.

RedefineClasses is different from retransformClasses

The redefineClasses are classes that have not been loaded before the JVM starts, and the retransformClasses are classes that have been initialized after the JVM starts.

2.2 ClassFileTransformer

In our agent, we need to provide an implementation of this interface to convert the class bytecode file before the JVM defines the class. This interface provides a method whose implementation can convert the supplied class file and return a new replacement class file:

byte[] transform(ClassLoader loader, String className, Class<? > classBeingRedefined, ProtectionDomain protectionDomain,byte[] classfileBuffer)
          throws IllegalClassFormatException
Copy the code

Three, Java Agent two kinds of implementation

Java Agent is actually a JAR file that specifies the agent implementation class to be loaded through related attributes in the MANIFEST file. There are two ways to implement agent: one is to set agent through command line before JVM startup; The other option is to attach after JVM startup.

Agent implementation before JVM startup

Instrument is introduced in JDK5. In JDK5, Instrument requires that the agent classes be set with the command-line argument JavaAgent before the target JVM program is run. Before the JVM is initialized, Instrument starts to set callback functions in the JVM. Detect characteristic class loading to complete the actual enhancement work.

-javaagent: jarpath[ =options]

Here jarPath is the path of our Agent JAR, which must conform to the JAR file specification. The manifest (meta-INF/manifest.mf) of the proxy JAR file must contain the attribute premain-class. The value of this property is the class name of the proxy class. The proxy class must implement a common static premain method, similar in principle to the main application entry point. After JVM initialization, each main method (Premain) is called in the order specified for the agents, and then the main method (main) of the actual application is called. Each premain method must be returned in startup order.

The premain method can have either of the following overloaded methods, and if both exist, the multi-parameter method is called first:

public static void premain(String agentArgs, Instrumentation inst);

public static void premain(String agentArgs);
Copy the code

Our proxy class will be loaded by the SystemClassLoader, and the premain method will execute under the same security and classloader rules as our main application’s main method. Premain can do whatever main application’s main method can do. If the agent class fails to be parsed, the agent class fails to be loaded, the agent class does not have a premain method, or the agent class method fails, the JVM will terminate.

Agent implementation after JVM startup

JDK6 begins to add many powerful features to Instrument. It is important to note that in JDK5 enhancements must be made by specifying Instrument on the command line before the target JVM is started. In practice, the target application may already be running. This is not what we want to do if the JVM does not need to be restarted, so Instrument provides the purpose of setting up the Java Agent after the JVM is started.

This implementation needs to ensure the following three points: 1) The Manifest in the Agent JAR must contain the attribute agent-class, whose value is the agent Class name. 2) Agent classes must contain public static methods agentMain 3) System ClassLoad must support adding agent JARS to the System Class Path.

The Agent JAR will be added to the SystemClass Path, where the SystemClassLoader loads the main application. After the Agent class is loaded, the JVM will try to execute its AgentMain method. Similarly, The multi-parameter method is preferred if both of the following methods exist:

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


public static void agentmain(String agentArgs);
Copy the code

If you want to use preMain and AgentMain in an Agent class, you can use preMain and AgentMain in an Agent class. The answer is yes, but agentMain is not executed before JVM startup. Premain is not executed after the JVM starts.

If our agent fails to start (agent class cannot be loaded, AgentMain fails, Agent class has no valid AgentMain methods, etc.), the JVM will not terminate!

Fourth, the Manifest

4.1 Attribute Composition

There is a key file named meta-INF/manifest.mf in which we need to specify the agent class.

The attribute name describe
Premain-Class This property is used to specify the agent when the JVM starts, and it must contain the premain method. If this property does not exist, the JVM terminates. Note that is the full path of the class
Agent-Class This property specifies the agent class if the Agent implementation supports a mechanism for starting the agent at some time after the JVM starts. If this property does not exist, the agent will not start.
Boot-Class-Path This property specifies the path to be loaded by BootStrapClassLoad (the path needs to take the specified file), separated by Spaces
Can-Redefine-Classes The optional value is true false, case insensitive, the default is false. This property is used to specify whether the agent has an effect on redefineClass
Can-Retransform-Classes The optional value is true false, case insensitive, the default is false. This property specifies whether the agent has a effect on the retransformClass

4.2 File Generation Mode

There are two ways to generate this file: 1) we create this file manually and 2) through the Maven plugin

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <! Meta-inf/manifest.mf -->
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>xxx</Premain-Class>
                            <Agent-Class>xxx</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>

        </plugins>
    </build>
Copy the code

Four, in actual combat

Next, we will feel the charm of Java Agent closely through actual combat. The goal of this exercise is to replace the behavior of the target class.

4.1 Preparations

Here we initialize a Springboot project and make a simple controller as follows:

@RestController
public class MainController {
    @RequestMapping("/index")
    public String index(a){
        return "hello world"; }}Copy the code

So when I visit this address, the browser will display hello World as follows:

Next we will use the Java Agent to change the behavior of this Controller.

4.2 Replacement implementation before JVM startup

4.2.1 Define the ClassFileTransformer implementation

In our custom ClassFileTransformer, the bytecode is dynamically modified by Javassist to change what the Controller outputs

public class MyClassFileTransformer implements ClassFileTransformer

{
    @SneakyThrows
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<? > classBeingRedefined, ProtectionDomain protectionDomain,byte[] classfileBuffer) throws IllegalClassFormatException {
        if (className.contains("MainController")) {final ClassPool classPool = ClassPool.getDefault();
            final CtClass clazz = classPool.get("com.cf.springboot.controller.MainController");
            CtMethod convertToAbbr = clazz.getDeclaredMethod("index");
            String methodBody = "Return \" Hello world [version2]\";;
            convertToAbbr.setBody(methodBody);
            // Return bytecode and detachCtClass object
            byte[] byteCode = clazz.toBytecode();
            // Detach means to remove the Date object from memory that was loaded by Javassist. If you need to find it in memory the next time, it will be loaded again by JavAssist
            clazz.detach();
            return byteCode;
        }
        // If null is returned, the bytecode will not be modified
        return null; }}Copy the code

4.2.1 Defining the Agent Class implementation

public class BeforeJvmAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain invoke!");

        inst.addTransformer(new MyClassFileTransformer());
    }

    public static void main(String[] args) {
        System.out.println("main invoke!"); }}Copy the code

4.2.2 Package and Set command line parameters to start Spring Boot

Once started, observe the console output

You can see that premain has been executed recently, so try it

As you can see, our changes have taken effect

4.3 Replace the implementation after JVM startup

In this case, the ClassFileTransformer implementation is the same one we used in section 4.2, so we only need to look at the new implementation. From now on, our application is always on, and what we’re going to do is actually heat replace.

4.3.1 Agent Class implementation

public class AfterJvmAgent {
    public static void agentmain(String agentArgs, Instrumentation inst)
        throws ClassNotFoundException, UnmodifiableClassException {
        inst.addTransformer(new MyClassFileTransformer(), true);
        / / key point
        inst.retransformClasses(Class.forName("com.cf.springboot.controller.MainController".false,ClassLoader.getSystemClassLoader()));
    }

    public static void main(String[] args) {}}Copy the code

The key point here is to manually retransform the classes we need to enhance in our AgentMain.

4.3.2 Start the application and attach

Here we need to get the target JVM and attach our agent

public static void main(String[] args) throws Exception{
    List<VirtualMachineDescriptor> list = VirtualMachine.list();
    for (VirtualMachineDescriptor vmd : list) {
        // If the VM name is XXX, the VM is the target VM. Obtain the PID of the VM
        // Then load agent.jar and send it to the vm
        System.out.println(vmd.displayName());
        if (vmd.displayName().equals("com.cf.springboot.Application")) {
            VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
            virtualMachine.loadAgent("/ Users/zhuyu/code/spring - the boot/after_jvm_agent/target/after_jvm_agent - 0.0.1 - the SNAPSHOT. Jar"); virtualMachine.detach(); }}}Copy the code

Look at our request again at this time:

Perfect!

Five, the reference

Java programmers will know: understanding Instrument javaagent use guidelines based on Java Agent of Instrument being implement docs.oracle.com/javase/8/do… Zhuanlan.zhihu.com/p/135872794…