1、Java Agent的开发步骤说明

1、定义Agent类

创建一个包含premain方法的类,即:Agent类。
该类中的premain方法会在JVM启动时被调用,该方法的主要作用是注册类转换器ClassTransformer对象。

代码示例:

public class MyAgent {
    // JVM 启动时自动调用
    public static void premain(String args, Instrumentation inst) {
        // 添加类转换器
        inst.addTransformer(new MyClassTransformer());
    }
}

说明:

  • premain是JVM启动时加载Agent的入口方法。
  • Instrumentation inst参数由JVM提供,是操作类定义的核心工具。
  • 通过inst.addTransformer(…)注册一个转换器,相当于告诉JVM:“后续加载的类,你可以让我先处理一下”。

2、实现ClassFileTransformer

在ClassFileTransformer的transform方法中,动态修改目标类的字节码

代码示例:

public class MyClassTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className,
                          Class<?> classBeingRedefined,
                          ProtectionDomain protectionDomain,
                          byte[] classfileBuffer) {
        // 1、判断是否是目标类
        if (!"target/MyService".equals(className)) {
            return null; // 不修改
        }

        // 2、使用ASM等工具修改字节码
        byte[] modified = modifyBytecode(classfileBuffer);

        // 3、返回修改后的字节码
        return modified;
        }
}

说明:

  • transform()方法是字节码修改的“主战场”。
  • 当JVM即将定义一个类(ClassLoader.defineClass)之前,会先调用所有注册的transform方法。
  • 你可以在其中:
    • 读取原始字节码(classfileBuffer)
    • 使用ASM / Javassist / Byte Buddy修改字节码
    • 返回修改后的byte[]
    • 返回null表示不修改

3、打包Agent JAR

在MANIFEST.MF中指定Premain-Class。

MANIFEST.MF示例:

Manifest-Version: 1.0
Premain-Class: your.package.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

说明:

  • Premain-Class:必须,指定包含premain方法的类
  • Can-Redefine-Classes:允许重新定义类(如Arthas使用)
  • Can-Retransform-Classes:允许重新转换类(关键!如果你要修改已加载的类,必须开启)

4、启动应用时加载Agent

使用-javaagent参数指定Agent JAR。

bash示例:

java -javaagent:my-agent.jar -jar my-app.jar

5、JVM调用流程

  • 类加载时触发:当JVM加载类(ClassLoader.defineClass)时,会调用所有注册的ClassFileTransformer。
  • 字节码修改:transform方法会修改源字节码文件,并返回新的字节数组被JVM使用,作为新的类定义。
  • 类缓存:修改后的类会被缓存,后续直接使用修改后的版本。

总结:
Java Agent通过premain注册ClassFileTransformer,在类被JVM加载的瞬间修改其字节码,从而实现无侵入的代码增强

2、Java Agent具体开发示例

1、目标

假设我们有一个MyService类。我们希望不修改源码,通过Java Agent在hello()方法执行前后自动插入日志。

Service代码示例:

public class MyService {
    public void hello() {
        System.out.println("Hello, World!");
    }
}

日志效果要求:

BEFORE: hello()
Hello, World!
AFTER: hello()

2、实现

1、方式一:使用ASM+Java Agent
1、项目结构
java-agent-demo/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── agent/
│   │   │       ├── MyAgent.java
│   │   │       └── LoggingClassVisitor.java
│   │   └── resources/
│   │       └── META-INF/
│   │           └── MANIFEST.MF
│   └── test/
│       └── java/
│           └── MyService.java
2、pom.xml(Maven)
<?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>cn.demo</groupId>
    <artifactId>java-agent-demo</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>9.6</version>
        </dependency>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm-commons</artifactId>
            <version>9.6</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <archive>
                        <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

注意:
一定要添加之间的这一段。

3、src/main/resources/META-INF/MANIFEST.MF

示例:

Manifest-Version: 1.0
Premain-Class: agent.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

注意:
每行后面不能有空格,最后一行要换行

4、agent/MyAgent.java

代码示例:

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;

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

public class MyAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("Java Agent 已加载(ASM 版)");

        inst.addTransformer(new ClassFileTransformer() {   // 注册ClassFileTransformer类
            @Override
            public byte[] transform(ClassLoader loader,      // transform方法修改字节码
                                    String className,
                                    Class<?> classBeingRedefined,
                                    ProtectionDomain protectionDomain,
                                    byte[] classfileBuffer) throws IllegalClassFormatException {

                // 只处理 MyService 类
                if (!"com/example/MyClass".equals(className)) {
                    return null;
                }

                System.out.println("正在转换类: " + className);

                ClassReader cr = new ClassReader(classfileBuffer);
                ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
                ClassVisitor cv = new LoggingClassVisitor(cw);
                cr.accept(cv, ClassReader.EXPAND_FRAMES);

                return cw.toByteArray();
            }
        });
    }
}
5、agent/LoggingClassVisitor.java

代码示例:

import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter;
import static org.objectweb.asm.Opcodes.*;

public class LoggingClassVisitor extends ClassVisitor {
    public LoggingClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM9, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor,
                                     String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);

        if (mv != null && name.equals("hello")) {
            return new AdviceAdapter(Opcodes.ASM9, mv, access, name, descriptor) {
                @Override
                protected void onMethodEnter() {
                    // 在hello()方法执行前插入日志:System.out.println("BEFORE: hello()");
                    getStatic(Type.getType(System.class), "out", Type.getType(java.io.PrintStream.class));
                    push("BEFORE: hello()");
                    invokeVirtual(Type.getType(java.io.PrintStream.class),
                            new Method("println", "(Ljava/lang/String;)V"));
                }

                @Override
                protected void onMethodExit(int opcode) {
                    // 在方法返回前插入日志:System.out.println("AFTER: hello()");                    
                    if (opcode != ATHROW) {
                        getStatic(Type.getType(System.class), "out", Type.getType(java.io.PrintStream.class));
                        push("AFTER: hello()");
                        invokeVirtual(Type.getType(java.io.PrintStream.class),
                                new Method("println", "(Ljava/lang/String;)V"));
                    }
                }
            };
        }
        return mv;
    }
}
6、MyService.java(测试类)

代码示例:

// src/test/java/MyService.java
public class MyService {
    public static void main(String[] args) {
        new MyService().hello();
    }

    public void hello() {
        System.out.println("Hello, World!");
    }
}
2、方式二:使用Byte Buddy+Java Agent(更简单)

步骤同方法一一样,只需替依赖和MyAgent.java。

依赖示例:

<dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy</artifactId>
    <version>1.14.0</version>
</dependency>
<dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy-agent</artifactId>
    <version>1.14.0</version>
</dependency>

代码示例:

// agent/MyAgent.java
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;

import java.lang.instrument.Instrumentation;

public class MyAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("Java Agent 已加载(Byte Buddy 版)");

        new AgentBuilder.Default()
            .type(ElementMatchers.named("MyService")) // 拦截类
            .transform((builder, typeDescription, classLoader, module) ->
                builder.method(ElementMatchers.named("hello")) // 拦截方法
                       .intercept(Advice.to(LoggingAdvice.class)) // 插入逻辑
            )
            .installOn(inst);
    }

    @SuppressWarnings("unused")
    public static class LoggingAdvice {
        @Advice.OnMethodEnter
        static void enter(@Advice.Origin String method) {   // 方法进入前执行
            System.out.println("BEFORE: " + method);
        }

        @Advice.OnMethodExit
        static void exit(@Advice.Origin String method) {    // 方法进入后执行
            System.out.println("AFTER: " + method);
        }
    }
}

3、编译和运行步骤

1、编译打包

bash示例:

mvn clean package

生成:target/java-agent-demo-1.0.0.jar

2、运行测试程序(带上Agent)

bash示例:

java -javaagent:target/java-agent-demo-1.0.0.jar \
     -cp target/test-classes \
     MyService

3、输出结果

BEFORE: hello()
Hello, World!
AFTER: hello()

4、验证Agent是否生效

1、如果不加 -javaagent,不会打印 BEFORE/AFTER。
2、加上后就会插入日志,说明字节码已被修改。

4、ASM vs Byte Buddy Agent

在这里插入图片描述

向阳前行,Dare To Be!!!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐