一、Java Agent 简介
Java Agent 是 Java Instrumentation API 的一种应用。它可以在 JVM 启动时(premain)或者运行时(agentmain)加载,拦截和修改类的字节码。Agent 以 JAR 包的形式存在,并通过 -javaagent
参数或 Attach API 加载。
二、工作原理
1. JVM 启动时加载(Premain)
- JVM 启动时,如果指定了
-javaagent:xxx.jar
参数,会在主类的main
方法之前,优先调用 agent JAR 的premain
方法。 - 这样可以在应用启动之前,对类进行增强或监控。
2. 运行时动态加载(Agentmain)
- Java 6 之后,通过 Attach API 可以在应用运行过程中动态加载 agent。
- 此时会调用 agent 的
agentmain
方法。
3. Instrumentation API
- Agent 的核心是
java.lang.instrument.Instrumentation
接口。 - 通过它,可以拦截类的加载过程、修改字节码、注册类转换器(ClassFileTransformer)。
三、Agent 编写结构
1. MANIFEST.MF 文件
Agent JAR 需要在清单文件(MANIFEST.MF)中声明入口方法:
Premain-Class: com.example.MyAgent
Agent-Class: com.example.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
2. 入口方法
// JVM启动时加载
public static void premain(String agentArgs, Instrumentation inst)
// 运行时加载
public static void agentmain(String agentArgs, Instrumentation inst)
3. 字节码转换
通过注册 ClassFileTransformer
,可以在类加载时修改字节码:
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(
ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer
) throws IllegalClassFormatException {
// 使用 ASM 或 Javassist 修改字节码
return modifiedBytes;
}
}, true);
四、常见应用场景
- 性能监控:如 JProfiler、Arthas 等工具,利用 agent 动态采集 JVM 信息。
- AOP:无侵入地为方法添加日志、权限校验等切面逻辑。
- 安全防护:在关键类加载时注入安全检查代码。
- 热修复:动态替换有 bug 的类,无需重启 JVM。
- 自定义监控:采集业务指标、统计方法调用次数等。
五、常用字节码工具
- ASM:高效、灵活的字节码修改库。
- Javassist:更易用,支持直接操作 Java 源代码风格的 API。
- ByteBuddy:现代化,易用性和功能性兼备。
六、简单示例
public class MyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("Agent loaded!");
inst.addTransformer(new MyTransformer());
}
}
class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
// 这里可以用 ASM/Javassist 修改 classfileBuffer
return classfileBuffer; // 简单示例,不做修改
}
}
七、注意事项
- Agent 的字节码修改要小心,避免引入 bug 或影响性能。
- 部分类(如 java.*)受保护,不能随意修改。
- 动态加载 agent 需要目标进程开启 Attach API 支持。
八. Agent 的生命周期和限制
生命周期:
- Agent 的
premain
或agentmain
方法只会被调用一次。 - 注册的
ClassFileTransformer
会在类加载/重定义时生效。 - Agent 可以通过 Instrumentation 接口重新转换已加载的类(需支持 Can-Retransform-Classes)。
限制:
- 不能修改 JVM 内部的敏感类(如 java.lang.String)。
- 字节码修改后要保证类语义正确,否则会抛出异常。
- 动态加载 agent(Attach)需要 JVM 支持 Attach API,部分服务器环境可能关闭此功能。
九. 完整的 Agent 项目结构和打包流程
项目结构示例:
src/
main/
java/
com/example/MyAgent.java
com/example/MyTransformer.java
META-INF/
MANIFEST.MF
MANIFEST.MF 示例:
Premain-Class: com.example.MyAgent
Agent-Class: com.example.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
打包命令(Maven):
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
十. Agent 动态 Attach 示例
使用 Attach API 将 agent 动态加载到目标 JVM:
import com.sun.tools.attach.VirtualMachine;
public class AttachAgent {
public static void main(String[] args) throws Exception {
String pid = "目标JVM的PID";
String agentPath = "agent.jar路径";
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(agentPath, "agentArgs");
vm.detach();
}
}
- 需要 tools.jar(JDK自带)和目标 JVM 支持 Attach。
十一. 常见问题与调试技巧
-
Agent无法加载?
- 检查 MANIFEST.MF 配置和 jar 包路径。
- JVM 参数是否正确:
-javaagent:/path/to/agent.jar
-
类未被修改?
- transformer 的过滤条件是否正确。
- 是否需要重新转换已加载类(
retransformClasses
)。
-
调试技巧:
- 在 transform 方法内打印日志,确认 agent 是否生效。
- 用
javap -c
反编译目标类,检查字节码是否已修改。 - 使用 ByteBuddy 的
AgentBuilder.Listener
监听加载和转换过程。
结语
Java Agent 是 Java 平台极其强大的扩展能力之一,能实现无侵入的监控、调试、热修复等高级功能。实际应用时,建议结合 ASM/ByteBuddy 等库,处理好字节码兼容性和性能问题。