Java Agent开发案例

wen java案例 2

本文目录导读:

Java Agent开发案例

  1. 核心原理
  2. 开发准备
  3. 案例 1:方法执行耗时监控(简易 APM)
  4. 案例 2:实现简易 @Log 注解(打印入参和返回值)
  5. 进阶技巧与注意事项

Java Agent 是 Java 平台提供的一个强大的工具,允许开发者在不修改应用源代码的情况下,修改类的字节码,它常用于性能监控、APM(应用性能管理)、热部署、代码调试、安全检测等场景。

下面我将为您提供一个从入门到进阶的完整开发案例,包括基础原理简单案例(方法耗时统计)和进阶案例(实现一个简易的 @Log 注解)。


核心原理

Java Agent 有两种入口方式:

  1. premain:在 main 方法之前执行,通常用于在 JVM 启动时通过 -javaagent 参数加载。
  2. agentmain:在 main 方法之后执行,通常用于附加到正在运行的 JVM 上(Attach API)。

它们都依赖 Instrumentation 接口提供的 ClassFileTransformer 来转换类的字节码。


开发准备

  1. 搭建 Maven 项目 创建一个普通的 Maven 项目(如 my-agent)。

  2. 引入依赖

    <dependencies>
        <!-- 字节码操作框架,使用 javassist 或 ASM,这里用 javassist 简单些 -->
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.29.2-GA</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <!-- 指定 premain 方法所在类 -->
                            <Premain-Class>com.example.PremainAgent</Premain-Class>
                            <!-- 可选:允许重新转换类 -->
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

案例 1:方法执行耗时监控(简易 APM)

编写 Agent 入口类

// PremainAgent.java
package com.example;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
public class PremainAgent {
    // 1. JVM 启动时加载 Agent 会调用这个方法
    public static void premain(String args, Instrumentation inst) {
        System.out.println("[Agent] 启动 premain,参数:" + args);
        // 添加自定义的 ClassFileTransformer
        inst.addTransformer(new MethodTimeTransformer(), true);
        System.out.println("[Agent] 已添加转换器");
    }
}

编写 ClassFileTransformer

这个类会拦截目标类的加载,并修改其字节码,在方法前后插入计时逻辑。

// MethodTimeTransformer.java
package com.example;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import javassist.*;
public class MethodTimeTransformer implements ClassFileTransformer {
    // 可以通过配置指定要监控的包名,这里写死为示例
    private static final String TARGET_PACKAGE = "com/example/app/";
    @Override
    public byte[] transform(ClassLoader loader, String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) throws IllegalClassFormatException {
        // 1. 只处理目标包下的类(避免修改 JDK 或无关的类)
        if (className == null || !className.startsWith(TARGET_PACKAGE)) {
            return null; // 返回 null 表示不修改
        }
        System.out.println("[Agent] 正在转换类:" + className);
        try {
            // 2. 使用 javassist 处理字节码
            ClassPool pool = ClassPool.getDefault();
            // 需要设置类路径,否则可能找不到依赖
            pool.appendSystemPath();
            // 通过原始字节码创建 CtClass(这样可以避免因找不到类路径而失败)
            CtClass ctClass = pool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));
            // 3. 如果类已经被冻结(做过修改),跳过
            if (ctClass.isFrozen()) {
                return null;
            }
            // 4. 获取所有方法
            CtMethod[] methods = ctClass.getDeclaredMethods();
            for (CtMethod method : methods) {
                // 跳过抽象方法和 native 方法
                if (Modifier.isAbstract(method.getModifiers()) || Modifier.isNative(method.getModifiers())) {
                    continue;
                }
                System.out.println("[Agent]   - 修改方法:" + method.getName());
                // 在方法开头插入代码:long start = System.nanoTime();
                method.addLocalVariable("startTime", CtClass.longType);
                method.insertBefore("startTime = System.nanoTime();");
                // 在方法结尾插入代码(包括 return 语句之前)
                method.insertAfter("long endTime = System.nanoTime();" +
                        "long cost = (endTime - startTime) / 1000000;" + // 转为毫秒
                        "System.out.println(\"[Agent] 方法 \" + \"" + className + "." + method.getName() + "\" + \" 耗时:" + "\" + cost + \" ms\");");
            }
            // 5. 返回修改后的字节码
            byte[] modifiedBytes = ctClass.toBytecode();
            ctClass.detach(); // 清理
            return modifiedBytes;
        } catch (Exception e) {
            System.err.println("[Agent] 转换类失败:" + className + ",原因:" + e.getMessage());
            e.printStackTrace();
        }
        return null;
    }
}

编写测试项目

创建一个独立的 Maven 项目(如 my-app),带一个主类:

// com/example/app/UserService.java
package com.example.app;
public class UserService {
    public void getUser() {
        System.out.println("正在查询用户信息...");
        try {
            Thread.sleep(200); // 模拟耗时
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void saveUser() {
        System.out.println("正在保存用户...");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        UserService service = new UserService();
        service.getUser();
        service.saveUser();
        System.out.println("执行完毕!");
    }
}

配置启动参数

将 Agent 打包为 JAR(mvn clean package),然后运行测试程序时添加 JVM 参数:

java -javaagent:my-agent-1.0-SNAPSHOT.jar=myargs -jar my-app.jar

运行结果示例

[Agent] 启动 premain,参数:myargs
[Agent] 已添加转换器
[Agent] 正在转换类:com/example/app/UserService
[Agent]   - 修改方法:getUser
[Agent]   - 修改方法:saveUser
正在查询用户信息...
[Agent] 方法 com/example/app/UserService.getUser 耗时:201 ms
正在保存用户...
[Agent] 方法 com/example/app/UserService.saveUser 耗时:100 ms
执行完毕!

案例 2:实现简易 @Log 注解(打印入参和返回值)

这个案例展示了如何根据注解来增强代码,这是生产环境中非常常见的需求(如日志、权限校验)。

定义注解(在测试项目中定义)

// com/example/app/Log.java
package com.example.app;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
@Target(ElementType.METHOD)
public @interface Log {
}

修改 Agent 的转换逻辑

transform 方法中,检查方法是否被 @Log 注解标记。

// 在 MethodTimeTransformer 的 transform 方法中,修改核心逻辑:
// 替换原有检查方法是否为目标包的逻辑,改为检查注解
// 检查方法上的注解:使用 getAnnotations() 获取 javassist 的注解
Object[] annotations = method.getAnnotations();
boolean hasLogAnnotation = false;
for (Object annotation : annotations) {
    // 注意:这里获取的是 java.lang.annotation.Annotation
    if (annotation instanceof com.example.app.Log) {
        hasLogAnnotation = true;
        break;
    }
}
if (!hasLogAnnotation) {
    continue; // 跳过没有 @Log 注解的方法
}
System.out.println("[Agent] 发现 @Log 注解方法:" + method.getName());
// 在方法开头插入获取参数信息的代码
method.addLocalVariable("paramTypes", CtClass.javaLangObjectArr);
method.addLocalVariable("paramValues", CtClass.javaLangObjectArr);
// 插入代码:打印入参
String beforeCode = "{\n" +
        "    java.lang.StringBuilder sb = new java.lang.StringBuilder();\n" +
        "    sb.append(\"[Agent] 调用方法:\").append(\"" + className + "." + method.getName() + "\");\n" +
        "    sb.append(\" 参数:\");\n" +
        "    boolean first = true;\n" +
        "    for (Object obj : $args) {\n" + // $args 是 javassist 内置变量,表示所有参数
        "        if (!first) { sb.append(\", \"); }\n" +
        "        sb.append(obj);\n" +
        "        first = false;\n" +
        "    }\n" +
        "    System.out.println(sb.toString());\n" +
        "}";
method.insertBefore(beforeCode);
// 插入代码:打印返回值
// 注意:$_ 是 javassist 内置变量,表示方法的返回值
String afterCode = "System.out.println(\"[Agent] 方法结束,返回值:\" + $_);";
method.insertAfter(afterCode);

测试结果

UserServicesaveUser 方法上加 @Log,运行后输出:

正在查询用户信息...
[Agent] 方法 com/example/app/UserService.saveUser 耗时:100 ms
[Agent] 调用方法:com/example/app/UserService.saveUser 参数:
[Agent] 方法结束,返回值:null
正在保存用户...
执行完毕!

进阶技巧与注意事项

  1. 字节码框架选择

    • Javassist:上手快,API 简单(insertBeforeinsertAfter$args),适合大多数场景。
    • ASM:性能最高,但需要熟悉指令码,适合对性能极致要求的场景(如 Spring 框架底层)。
    • ByteBuddy:现代、优雅的 API,通常推荐新项目使用(比 Javassist 更强大,比 ASM 更易用)。
  2. 注意线程安全ClassFileTransformer 会被多个线程并发调用(类加载是多线程的),transform 方法内部应尽量使用局部变量,或者确保 ClassPool 的线程安全。

  3. 避免递归死循环: Agent 本身依赖了目标包中的类,修改 transform 时可能会触发该类重新加载,造成死循环,解决方案:

    • 提前加载 Agent 自身依赖。
    • transform 方法开头检查 className 是否包含 Agent 自身的类名。
  4. 动态附加(agentmain): 使用 VirtualMachine.attach(pid) 可以在运行时挂载 Agent,这常用于在线诊断工具(如 Arthas)。

  5. MANIFEST.MF 配置清单: 除了 Premain-Class,还有 Agent-Class(用于 agentmain)、Boot-Class-Path(引导类路径)和 Can-Retransform-Classes 等关键属性。

  6. 使用 ByteBuddy 简化开发: 如果觉得手动处理字节码太繁琐,可以试试 ByteBuddy 库,它的代码更简洁:

    new AgentBuilder.Default()
        .type(ElementMatchers.nameStartsWith("com.example.app"))
        .transform((builder, typeDescription, classLoader, module) ->
            builder.method(ElementMatchers.any())
                .intercept(MethodDelegation.to(MyInterceptor.class))
        ).installOn(inst);

通过以上两个案例,您可以从零构建一个完整的 Java Agent:

  1. Premain + Instrumentation 是基础入口。
  2. ClassFileTransformer 负责拦截并修改类。
  3. Javassist/ASM 提供便捷 API 来操作字节码。

Java Agent 是深入 JVM 和字节码技术的绝佳入口,也是实现各种“黑科技”框架(热部署、Mock、链路追踪)的核心原理,建议先从简单的耗时统计做起,逐步尝试更复杂的字节码操作。

抱歉,评论功能暂时关闭!