1.Java Agent 概述

Java Agent 是一种特殊的程序,从java5开始支持,它可以在不修改目标应用程序代码的前提下,通过 [Java 虚拟机](https://so.csdn.net/so/search?q=Java 虚拟机&spm=1001.2101.3001.7020)(JVM)提供的字节码增强技术,对目标应用的类加载过程进行干预,实现对类的字节码修改、增强或监控等功能。

2.Java Agent原理

2.1 Premain/Agentmain

两种加载方式。Premain / Agentmain 是 JVM 规范规定的“入口函数”,由 JVM 本身 在启动或 attach 时主动调用;Agent 作者只是按约定实现这两个静态方法,把启动参数和 Instrumentation 对象“交”给业务代码。

  • premain(静态加载)(JDK5):在应用程序主类(main 方法)启动前加载并执行,通过-javaagent命令行参数指定 Agent Jar 包。
  • Agentmain(动态加载)(JDK6):在应用程序运行过程中动态附着(Attach)到目标 JVM 上并执行,适用于已启动的应用程序,通过 JVM 提供的 Attach API 实现。
public static void premain(String agentArgs, Instrumentation inst){}
public static void agentmain(String agentArgs, Instrumentation inst){}

这两个方法有两个入参:agentArgs(String)/Instrumentation inst(Instrumentation 接口的实例)

  • agentArgs(String):你在命令行或 attach 时给 Agent 的参数字符串。例如:-javaagent:myagent.jar=port=8848,debug=true

    这里 port=8848,debug=true 就是 agentArgs。

  • Instrumentation inst(Instrumentation 接口的实例):这是 JVM 内部创建好的“官方后门”对象,Agent 拿到它就能注册 ClassFileTransformer、retransformClasses 等。

2.2 Instrumentation ApI

Instrumentation是 Java SE 5 在java.lang.instrument包下引入的一个接口,接口提供了一组用于操作类和对象的方法,主要用于字节码操作。作用是接收一个byte[]的原始字节码,然后交由ClassFileTransformer进行字节码增强,然后返回修改过的byte[]。

作用 典型方法 说明
注册类转换器 addTransformer 类加载或重加载时被回调,拿到原始字节数组
重新转换已加载类 retransformClasses 对线上进程再次触发上述回调
重定义类 redefineClasses 直接替换类定义(风险高)
查询已加载类 getAllLoadedClasses 做诊断、过滤
计算对象大小 getObjectSize(obj) 内存分析
添加 jar 到 boot classpath appendToBootstrapClassLoaderSearch 解决类可见性问题

通过上述方法可以实现的功能:

  • 类转换:允许在类加载时对字节码进行修改。
  • 代理类生成:可以在运行时生成新的类。
  • 对象监控:可以获取JVM中的对象信息,如内存使用情况。

2.3 字节码增强技术

基于 JVM 的类加载机制,在类被加载到 JVM 之前,拦截类的字节码流,通过字节码操作框架(如 ASM、Javassist、Byte Buddy 等)对字节码进行修改,注入自定义逻辑。

特性 ASM Byte Buddy cglib Javassist
学习曲线 陡峭(直接操作字节码) 平缓(API 友好) 中等 平缓(类似反射 API)
性能 极高(直接生成字节码) 高(运行时代理优化) 中(基于反射封装)
应用场景 AOP 框架(如 Spring AOP) 动态代理、测试框架 无接口类代理 运行时类修改
API 风格 底层(操作字节码指令) 面向对象(流畅 API) 封装 ASM 高级(类似反射)
对 Java 版本支持 全版本(灵活适配) 最新版本优先支持 主流版本 主流版本
依赖大小 小(核心库约 1 MB) 中等(约 2 MB) 依赖 ASM 中等(约 3 MB)
典型应用 Spring、Hibernate Mockito、Hibernate Spring AOP 动态代理、ORM 工具

这些字节码操作的框架是对字节码进行修改,然后由Agent将修改后的字节码交给JVM执行。

3.Java Agent的应用场景

在这里插入图片描述

APM:应用性能监控

4.Java Agent 生命周期

1. JVM 启动/attach
   │
   ├─ 找到 premain/agentmain
   │     ↓
   │   你的 premain/agentmain 把 Transformer 注册到 Instrumentation
   │
2. 当某个类第一次加载(或 retransform)
   │
   ├─ JVM 把原始字节码 byte[] 交给 Instrumentation
   │     ↓
   │   Instrumentation 回调 Transformer.transform(...)
   │     ↓
   │   Transformer 内部用 ASM/ByteBuddy 改字节码
   │     ↓
   │   Transformer 把新的 byte[] 返回给 Instrumentation
   │
3. Instrumentation 把新字节码送回 JVM
   │
   └─ JVM 真正加载/替换这个类

数据流:

JVM --> 原始字节码 --> Instrumentation --> Transformer(用ASM/ByteBuddy 修改) --> 修改后字节码 --> Instrumentation --> JVM

5.如何使用 Java Agent?

5.1 使用步骤:

  • 编写 Agent类:包含 premain() 或 agentmain() 方法。
  • 编写 MANIFEST.MF 文件:指定 Agent 的入口类。
  • 打包成 JAR 文件:包含 Agent 类和 MANIFEST 文件。
  • 使用 Agent:通过指定 JVM 参数或 Attach 机制加载 Agent。

5.2 示例–实现方法开始和结束时打印日志

1.创建一个maven工程,作为一个agent。
2.引入POM,并指定agent的入口
<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.3.0</version>
                <configuration>
                    <descriptorRefs>
                        <!--将应用的所有依赖包都打到jar包中。如果依赖的是 jar 包,jar 包会被解压开,平铺到最终的 uber-jar 里去。输出格式为 jar-->
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <!-- 设置manifest配置文件-->
                        <manifestEntries>
                            <!--Premain-Class: 代表 Agent 静态加载时会调用的类全路径名。-->
                            <Premain-Class>com.linging.MethodAgentMain</Premain-Class>
                            <!--Agent-Class: 代表 Agent 动态加载时会调用的类全路径名。-->
                            <Agent-Class>com.linging.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.3</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
3.编写 Agent类:MethodAgentMain

我们使用了premain()静态加载方式,agentmain动态加载方式。并用到了Instrumentation类结合javassist代码生成库进行字节码的修改。

public class MethodAgentMain {

    /** 被转换的类 */
    public static final String TRANSFORM_CLASS = "org.example.agent.AgentTest";

    /** 静态加载。Java agent指定的premain方法,会在main方法之前被调用 */
    public static void premain(String args, Instrumentation instrumentation) {
        System.out.println("premain start!");
        addTransformer(instrumentation);
        System.out.println("premain end!");
    }

    /** 动态加载。Java agent指定的agentmain方法,会在main方法之后被调用 */
    public static void agentmain(String args, Instrumentation instrumentation) {
        System.out.println("agentmain start!");
        addTransformer(instrumentation);
        Class<?>[] classes = instrumentation.getAllLoadedClasses();
        if (classes != null){
            for (Class<?> c: classes) {
                if (c.isInterface() ||c.isAnnotation() ||c.isArray() ||c.isEnum()){
                    continue;
                }
                if (c.getName().equals(TRANSFORM_CLASS)) {
                    try {
                        System.out.println("retransformClasses start, class: " + c.getName());
                        /*
                         * retransformClasses()对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
                         * retransformClasses()可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
                         */
                        instrumentation.retransformClasses(c);
                        System.out.println("retransformClasses end, class: " + c.getName());
                    } catch (UnmodifiableClassException e) {
                        System.out.println("retransformClasses error, class: " + c.getName() + ", ex:" + e);
                        e.printStackTrace();
                    }
                }
            }
        }
        System.out.println("agentmain end!");
    }

    private static void addTransformer (Instrumentation instrumentation) {
        /* Instrumentation提供的addTransformer方法,在类加载时会回调ClassFileTransformer接口 */
        instrumentation.addTransformer(new ClassFileTransformer() {
            public byte[] transform(ClassLoader l,String className, Class<?> c,ProtectionDomain pd, byte[] b){
                if(className == null){
                    return null;
                }
                try {
                    className = className.replace("/", ".");
                    if (className.equals(TRANSFORM_CLASS)) {
                        final ClassPool classPool = ClassPool.getDefault();
                        final CtClass clazz = classPool.get(TRANSFORM_CLASS);

                        for (CtMethod method : clazz.getMethods()) {
                            /*
                             * Modifier.isNative(methods[i].getModifiers())过滤本地方法,否则会报
                             * javassist.CannotCompileException: no method body  at javassist.CtBehavior.addLocalVariable()
                             * 报错原因如下
                             * 来自Stack Overflow网友解答
                             * Native methods cannot be instrumented because they have no bytecodes.
                             * However if native method prefix is supported ( Transformer.isNativeMethodPrefixSupported() )
                             * then you can use Transformer.setNativeMethodPrefix() to wrap a native method call inside a non-native call
                             * which can then be instrumented
                             */
                            if (Modifier.isNative(method.getModifiers())) {
                                continue;
                            }

                            method.insertBefore("System.out.println(\"" + clazz.getSimpleName() + "."
                                    + method.getName() + " start.\");");
                            method.insertAfter("System.out.println(\"" + clazz.getSimpleName() + "."
                                    + method.getName() + " end.\");", false);
                        }

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

                return null;
            }
        }, true);
    }
}
4.编译打包
执行 mvn clean package 编译打包,最终打包生成了 agent jar 包

在这里插入图片描述

5.验证agent功能的测试类

创建一个maven工程。

在这里插入图片描述

6.测试agent静态加载

在 IDEA 的 Run/Debug Configurations 中,点击 Modify options,勾选上 add VM options,在 VM options 栏增加如下参数:

-javaagent:/ideaProject/local-project/spring-boot-db-sharding-demo/java-agent-demo/java-agent/target/java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar
public class AgentTest {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            System.out.println("process result: " + process());
            System.out.println("=============================>");
            Thread.sleep(5000);
        }
    }

    public static String process() {
        System.out.println("process!");
        return "success";
    }
}

运行 AgentTest.java的 main 方法,可以看到控制台日志:

premain start!
premain end!
AgentTest.main start.
AgentTest.process start.
process!
AgentTest.process end.
process result: success
=============================>
AgentTest.process start.
process!
AgentTest.process end.
process result: success
=============================>
AgentTest.process start.
process!
AgentTest.process end.
process result: success
=============================>
....

可以看到:process! 和 AgentTest.process end. 是方法的执行结果,然后:AgentTest.process start. 和 process result: success 是字节码增强的打印日志。

7.测试agent动态加载

动态加载不是通过 -javaagent: 的方式实现,而是通过 Attach API 的方式。

编写调用 Attach API 的测试类,如果tools中的jar包未加载,则java8可以直接在idea中添加tools.jar依赖。

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;

public class AttachMain {

    public static void main(String[] args) throws Exception {
        List<VirtualMachineDescriptor> listBefore = VirtualMachine.list();
        // agentmain()方法所在jar包
        String jar = "/ideaProject/local-project/spring-boot-db-sharding-demo/java-agent-demo/java-agent/target/java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar";

        for (VirtualMachineDescriptor virtualMachineDescriptor : VirtualMachine.list()) {
            // 针对指定名称的JVM实例
            if (virtualMachineDescriptor.displayName().equals("org.example.agent.AgentTest")) {
                System.out.println("将对该进程的vm进行增强:org.example.agent.AgentTest的vm进程, pid=" + virtualMachineDescriptor.id());
                // attach到新JVM
                VirtualMachine vm = VirtualMachine.attach(virtualMachineDescriptor);
                // 加载agentmain所在的jar包
                vm.loadAgent(jar);
                // detach
                vm.detach();
            }
        }
    }
}

先直接运行 org.example.agent.AgentTest#main,注意不用加 -javaagent: 启动参数。

约15秒后,再运行 org.example.agent.AttachMain#main,可以看到 org.example.agent.AttachMain#main 打印的日志:

将对该进程的vm进行增强:org.example.agent.AgentTest的vm进程, pid=25216

之后可以看到 org.example.agent.AgentTest#main打印的日志中多了记录方法运行开始和结束的内容:

process!
process result: success
=============================>
process!
process result: success
=============================>
process!
process result: success
=============================>
agentmain start!
retransformClasses start, class: org.example.agent.AgentTest
retransformClasses end, class: org.example.agent.AgentTest
agentmain end!
AgentTest.process start.
process!
AgentTest.process end.
process result: success
=============================>
AgentTest.process start.
process!
AgentTest.process end.
process result: success
=============================>
AgentTest.process start.
process!
AgentTest.process end.
process result: success
=============================>
AgentTest.process start.
process!
AgentTest.process end.
process result: success
=============================>
.....

可以看到动态增强了。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐