本文目录导读:

Java Agent 是 Java 平台提供的一个强大的工具,允许开发者在不修改应用源代码的情况下,修改类的字节码,它常用于性能监控、APM(应用性能管理)、热部署、代码调试、安全检测等场景。
下面我将为您提供一个从入门到进阶的完整开发案例,包括基础原理、简单案例(方法耗时统计)和进阶案例(实现一个简易的 @Log 注解)。
核心原理
Java Agent 有两种入口方式:
- premain:在
main方法之前执行,通常用于在 JVM 启动时通过-javaagent参数加载。 - agentmain:在
main方法之后执行,通常用于附加到正在运行的 JVM 上(Attach API)。
它们都依赖 Instrumentation 接口提供的 ClassFileTransformer 来转换类的字节码。
开发准备
-
搭建 Maven 项目 创建一个普通的 Maven 项目(如
my-agent)。 -
引入依赖
<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);
测试结果
在 UserService 的 saveUser 方法上加 @Log,运行后输出:
正在查询用户信息...
[Agent] 方法 com/example/app/UserService.saveUser 耗时:100 ms
[Agent] 调用方法:com/example/app/UserService.saveUser 参数:
[Agent] 方法结束,返回值:null
正在保存用户...
执行完毕!
进阶技巧与注意事项
-
字节码框架选择:
- Javassist:上手快,API 简单(
insertBefore、insertAfter、$args),适合大多数场景。 - ASM:性能最高,但需要熟悉指令码,适合对性能极致要求的场景(如 Spring 框架底层)。
- ByteBuddy:现代、优雅的 API,通常推荐新项目使用(比 Javassist 更强大,比 ASM 更易用)。
- Javassist:上手快,API 简单(
-
注意线程安全:
ClassFileTransformer会被多个线程并发调用(类加载是多线程的),transform方法内部应尽量使用局部变量,或者确保 ClassPool 的线程安全。 -
避免递归死循环: Agent 本身依赖了目标包中的类,修改
transform时可能会触发该类重新加载,造成死循环,解决方案:- 提前加载 Agent 自身依赖。
- 在
transform方法开头检查className是否包含 Agent 自身的类名。
-
动态附加(agentmain): 使用
VirtualMachine.attach(pid)可以在运行时挂载 Agent,这常用于在线诊断工具(如 Arthas)。 -
MANIFEST.MF 配置清单: 除了
Premain-Class,还有Agent-Class(用于agentmain)、Boot-Class-Path(引导类路径)和Can-Retransform-Classes等关键属性。 -
使用 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:
- Premain + Instrumentation 是基础入口。
- ClassFileTransformer 负责拦截并修改类。
- Javassist/ASM 提供便捷 API 来操作字节码。
Java Agent 是深入 JVM 和字节码技术的绝佳入口,也是实现各种“黑科技”框架(热部署、Mock、链路追踪)的核心原理,建议先从简单的耗时统计做起,逐步尝试更复杂的字节码操作。