‌ Java Agent是一种特殊的Java程序,它可以在JVM启动时或运行时动态加载,用于监控和修改其他Java应用程序的行为‌。通过Java Agent,开发者可以在不修改目标应用程序源码的情况下,动态地插入功能,如性能分析、日志记录、代码覆盖率测试、热更新等‌。

一、Java Agent的主要功能

‌1、监控类的加载‌:在类加载到JVM时,可以对类进行操作,例如记录日志、统计加载时间‌。

‌2、修改类的字节码‌:在类被加载时,可以修改其字节码,例如插入调试代码、改变类的方法行为‌。

‌3、重新定义已加载的类‌:在程序运行时,可以重新定义已经加载的类(需要JVM支持)‌。

4‌、监控和获取对象的内存信息‌:可以获取对象的大小,用于内存分析‌。

二、Java Agent的实现方式

1‌、JVM启动时加载‌:在启动Java应用程序时,通过-javaagent参数加载。这种方式会在目标应用启动前执行,可以拦截所有类的加载过程‌

‌2、运行时动态附加‌:在应用程序已经启动的情况下,通过附加到目标JVM进程来加载。这需要Java提供的Attach API‌

三、Java Agent的历史背景和具体应用场景

Java Agent功能是JDK1.5引入的,通过java.lang.instrument接口实现。这个接口基于JVMTI(Java Virtual Machine Tool Interface)机制,允许开发者构建一个独立于应用程序的代理程序,用于监测和协助运行在JVM上的程序‌

四、示例

示例1:静态加载方式(启动执行)

工程1 (agent)
步骤1:pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>demo-javaagent</artifactId>
    <version>1.0</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.25.0-GA</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.1.1</version>
                <configuration>
                    <descriptorRefs>
                        <!--将应用的所有依赖包都打到jar包中。如果依赖的是 jar 包,jar 包会被解压开,平铺到最终的 uber-jar 里去。输出格式为 jar-->
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <!-- 设置manifest配置文件-->
                        <manifestEntries>
                            <!--Premain-Class: 代表 Agent 静态加载时会调用的类全路径名。-->
                            <Premain-Class>demo.MethodAgentMain</Premain-Class>
                            <!--Agent-Class: 代表 Agent 动态加载时会调用的类全路径名。-->
                            <Agent-Class>demo.MethodAgentMain</Agent-Class>
                            <!--Can-Redefine-Classes: 是否可进行类定义。-->
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <!--Can-Retransform-Classes: 是否可进行类转换。-->
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <!--绑定到package生命周期阶段上-->
                        <phase>package</phase>
                        <goals>
                            <!--绑定到package生命周期阶段上-->
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

步骤2: 创建 premain 方法,方法的主要功能是修改 App setName() 方法体

package demo;

import java.lang.instrument.Instrumentation;

public class MethodAgentMain {

    public static void premain(String args, Instrumentation inst) {
        MyTransformer tran = new MyTransformer();
        inst.addTransformer(tran);
    }
}
package demo;

import javassist.*;

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

public class MyTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if ("org/example/App".equals(className)) {
            try {

                // 从ClassPool获得CtClass对象
                final ClassPool classPool = ClassPool.getDefault();

                // 尝试添加额外的类路径(如果需要)
                classPool.appendClassPath(new ClassClassPath(this.getClass()));
                classPool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));

                final CtClass clazz = classPool.get("org.example.App");

                CtMethod convertToAbbr = clazz.getDeclaredMethod("setName");
                String methodBody = "{\n" +
                        "        this.name = \"ccc\" + \" aaa\";\n" +
                        "    }";

                convertToAbbr.setBody(methodBody);

                byte[] byteCode = clazz.toBytecode();
                clazz.detach();
                return byteCode;
            } catch (NotFoundException | CannotCompileException | IOException e) {
                e.printStackTrace();
            }
        }

        System.out.println(className);
        return classfileBuffer;
    }

}

步骤3: 编译打包
执行 mvn clean package 编译打包,最终打包生成了 agent jar 包,结果示例:
Java agent-LMLPHP

工程2(主工程)

package org.example;

public class App {
    private int code;
    private String name;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "App{" +
                "code=" + code +
                ", name='" + name + '\'' +
                '}';
    }
}
package org.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        App app = new App();
        app.setName("a");
        app.setCode(123);
        System.out.println(app);
        SpringApplication.run(DemoApplication.class, args);
    }
}

启动 javaagent

java -javaagent:demo-javaagent-1.0-jar-with-dependencies.jar -jar demo-1.0.0-SNAPSHOT.jar > output.log

查看运行结果:

App{code=123, name='ccc aaa'}

name 属性被成功修改。

示例2:动态加载方式(启动之后,接口调用触发)

在接口调用时触发某些行为,可以使用 Java Agent 来改变接口方法调用的行为

工程1 (agent)
步骤1:pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>demo-javaagent</artifactId>
    <version>1.0</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.25.0-GA</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.1.1</version>
                <configuration>
                    <descriptorRefs>
                        <!--将应用的所有依赖包都打到jar包中。如果依赖的是 jar 包,jar 包会被解压开,平铺到最终的 uber-jar 里去。输出格式为 jar-->
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <!-- 设置manifest配置文件-->
                        <manifestEntries>
                            <!--Premain-Class: 代表 Agent 静态加载时会调用的类全路径名。-->
                            <Premain-Class>demo.MethodAgentMain</Premain-Class>
                            <!--Agent-Class: 代表 Agent 动态加载时会调用的类全路径名。-->
                            <Agent-Class>demo.MethodAgentMain</Agent-Class>
                            <!--Can-Redefine-Classes: 是否可进行类定义。-->
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <!--Can-Retransform-Classes: 是否可进行类转换。-->
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <!--绑定到package生命周期阶段上-->
                        <phase>package</phase>
                        <goals>
                            <!--绑定到package生命周期阶段上-->
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

步骤2: 创建 premain 方法,方法的主要功能是修改方法体,在方法调用前后添加日志输出

package demo;

import java.lang.instrument.Instrumentation;

public class MethodAgentMain {

    public static void premain(String args, Instrumentation inst) {
        MyTransformer tran = new MyTransformer();
        inst.addTransformer(tran);
    }
}
package demo;

import javassist.*;

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

public class MyTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

            if (className != null && className.startsWith("org/example/controller/")) {
                ClassPool pool = ClassPool.getDefault();
                // 尝试添加额外的类路径(如果需要)
                pool.appendClassPath(new ClassClassPath(this.getClass()));
                pool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));

                CtClass ctClass;
                try {
                    ctClass = pool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));

                    for (CtBehavior method : ctClass.getDeclaredBehaviors()) {
                        if (method.isEmpty() || method.getMethodInfo() == null) {
                            continue;
                        }

                        // 修改方法体,在方法调用前后添加日志输出
                        method.insertBefore("System.out.println(\"Before method call: \" + $sig);");
                        method.insertAfter("System.out.println(\"After method call: \" + $sig);");
                    }

                    return ctClass.toBytecode();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return null;
        }

}

步骤3: 编译打包
执行 mvn clean package 编译打包,最终打包生成了 agent jar 包,结果示例:
Java agent-LMLPHP
工程2(主工程)
一个普通的spring工程

package org.example.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Description: TODO
 * @Author: Top
 * @Version: V1.0
 * @Date: 2020-01-15 15:03
 */
@RestController
@RequestMapping("/api/{edition}")
public class ConsumerController {

    @Autowired
    private Environment env;

    @GetMapping("/detail")
    @ResponseBody
    public String detail(String name) throws JsonProcessingException {
        return name;
    }

启动工程:

java -javaagent:demo-javaagent-1.0-jar-with-dependencies.jar -jar demo-1.0.0-SNAPSHOT.jar > output.log

执行结果:

Before method call: [Ljava.lang.Class;@bab9ac
consumer
After method call: [Ljava.lang.Class;@2756b30e

Java agent原理说明:

主流的JVM都提供了Instrumentation的实现,但是鉴于Instrumentation的特殊功能,并不适合直接提供在JDK的runtime里,而更适合出现在Java程序的外层,以上帝视角在合适的时机出现。
因此如果想使用Instrumentation功能,拿到Instrumentation实例,我们必须通过Java agent。
Java agent是一种特殊的Java程序(Jar文件),它是Instrumentation的客户端。与普通Java程序通过main方法启动不同,agent 并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API与虚拟机交互。
Java agent与Instrumentation密不可分,二者也需要在一起使用。因为Instrumentation的实例会作为参数注入到Java agent的启动方法中。

Instrumentation是Java提供的JVM接口,该接口提供了一系列查看和操作Java类定义的方法,例如修改类的字节码、向 classLoader 的 classpath 下加入jar文件等。使得开发者可以通过Java语言来操作和监控JVM内部的一些状态,进而实现Java程序的监控分析,甚至实现一些特殊功能(如AOP、热部署)。

public interface Instrumentation {
    /**
     * 注册一个Transformer,从此之后的类加载都会被Transformer拦截。
     * Transformer可以直接对类的字节码byte[]进行修改
     */
    void addTransformer(ClassFileTransformer transformer);
    
    /**
     * 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
     * retransformClasses可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
     */
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    
    /**
     * 获取一个对象的大小
     */
    long getObjectSize(Object objectToSize);
    
    /**
     * 将一个jar加入到bootstrap classloader的 classpath里
     */
    void appendToBootstrapClassLoaderSearch(JarFile jarfile);
    
    /**
     * 获取当前被JVM加载的所有类对象
     */
    Class[] getAllLoadedClasses();
}

注意:
Instrumentation的局限性

在运行时,我们可以通过Instrumentation的redefineClasses方法进行类重定义,在redefineClasses方法上有一段注释需要特别注意:
java 代码解读复制代码     * The redefinition may change method bodies, the constant pool and attributes.
     * The redefinition must not add, remove or rename fields or methods, change the
     * signatures of methods, or change inheritance.  These restrictions maybe be
     * lifted in future versions.  The class file bytes are not checked, verified and installed
     * until after the transformations have been applied, if the resultant bytes are in
     * error this method will throw an exception.

这里面提到,我们不可以增加、删除或者重命名字段和方法,改变方法的签名或者类的继承关系。认识到这一点很重要,当我们通过ASM获取到增强的字节码之后,如果增强后的字节码没有遵守这些规则,那么调用redefineClasses方法来进行类的重定义就会失败。
01-10 14:07