JavaAgent

Java Agent原理与实战

1.Java Agent 概述

Java Agent 是一种特殊的程序,从java5开始支持,它可以在不修改目标应用程序代码的前提下,通过 [Java 虚拟机](https://so.youkuaiyun.com/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 classpathappendToBootstrapClassLoaderSearch解决类可见性问题

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

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

2.3 字节码增强技术

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

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

可以看到动态增强了。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值