浅谈JavaAgent
字数 1626 2025-08-24 07:48:33
JavaAgent 技术详解
一、JavaAgent 概述
JavaAgent 是 Java 提供的一种特殊机制,允许开发者在 JVM 启动时或运行时动态修改字节码。JavaAgent 主要通过 -javaagent 参数指定一个 jar 包来实现,该 jar 包需要满足以下要求:
- MANIFEST.MF 文件中必须指定 Premain-Class 或 Agent-Class 项
- 指定的类必须实现 premain() 或 agentmain() 方法
JavaAgent 主要分为两种加载方式:
- 启动时加载(Premain)
- 运行时加载(Agentmain)
二、启动时加载 Agent (Premain)
2.1 基本概念
Premain-Class 指定的类会在 main 函数之前执行,函数声明有两种形式:
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
其中 Instrumentation inst 参数的方法优先级更高。
2.2 实现示例
- 创建 Premain 类:
import java.lang.instrument.Instrumentation;
public class PreMainDemo {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println(agentArgs);
for (int i = 0; i < 5; i++) {
System.out.println("premain is loading.....");
}
}
}
- 创建 MANIFEST.MF 文件:
Manifest-Version: 1.0
Premain-Class: Agent.PreMainDemo
- 打包:
jar cvfm agent.jar agent.mf Agent/PreMainDemo.class
- 测试类:
public class Hello {
public static void main(String[] args) {
System.out.println("Hello,Sentiment!");
}
}
- 运行:
java -javaagent:agent.jar=Sentiment -jar hello.jar
2.3 局限性
- 必须在目标程序启动前加载
- 无法对已运行的 JVM 进程生效
三、运行时加载 Agent (Agentmain)
3.1 基本概念
Agentmain 通过 Attach API 实现运行时加载,解决了 Premain 的局限性。主要函数声明:
public static void agentmain(String agentArgs, Instrumentation inst)
public static void agentmain(String agentArgs)
3.2 Attach API 核心类
-
VirtualMachine:
list(): 获取所有 JVM 列表attach(String id): 根据 pid 连接到 JVMdetach(): 断开连接loadAgent(String agent): 加载 agent
-
VirtualMachineDescriptor: 描述虚拟机的容器类
3.3 实现示例
- Agent 类:
public class AgentMainDemo {
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("agentmain start.");
}
}
- MANIFEST.MF:
Manifest-Version: 1.0
Agent-Class: Agent.AgentMainDemo
- 目标程序:
public class Hello {
public static void main(String[] args) throws InterruptedException {
System.out.println("Hello.main() in test project start!!");
Thread.sleep(300000000);
System.out.println("Hello.main() in test project end!!");
}
}
- Attach 程序:
public class AttchDemo {
public static void main(String[] args) throws AgentLoadException, IOException,
AgentInitializationException, AttachNotSupportedException {
VirtualMachine attach = VirtualMachine.attach("14460"); // 目标进程ID
attach.loadAgent("D:\\java\\AgentMemory\\target\\classes\\agent.jar");
attach.detach();
}
}
四、Instrumentation 接口详解
Instrumentation 是与 JVM 交互的核心接口,主要方法:
addTransformer(ClassFileTransformer transformer): 添加类转换器removeTransformer(ClassFileTransformer transformer): 移除类转换器retransformClasses(Class<?>... classes): 重新转换类isModifiableClass(Class<?> theClass): 判断类是否可修改getAllLoadedClasses(): 获取所有已加载类
4.1 获取已加载类示例
public class AgentMainDemo {
public static void agentmain(String agentArgs, Instrumentation inst) {
Class[] classes = inst.getAllLoadedClasses();
for (Class aClass : classes) {
String result = "class ==> " + aClass.getName();
System.out.println(result);
}
}
}
4.2 检查类可修改性
public class AgentMainDemo {
public static void agentmain(String agentArgs, Instrumentation inst) {
Class[] classes = inst.getAllLoadedClasses();
for (Class aClass : classes) {
String result = "class ==> " + aClass.getName() + "\n\t" +
"Modifiable ==> " + (inst.isModifiableClass(aClass) ? "true" : "false") + "\n";
if (result.contains("true")) {
System.out.println(result);
}
}
}
}
五、动态修改字节码
5.1 ClassFileTransformer 接口
public interface ClassFileTransformer {
default byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
....
}
}
5.2 结合 Javassist 修改字节码
- 目标类:
public class Hello {
public void Hello() {
System.out.println("This is Sentiment !");
}
}
- 测试程序:
public class HelloWorld {
public static void main(String[] args) throws InterruptedException {
Hello h1 = new Hello();
h1.Hello();
Thread.sleep(15000);
Hello h2 = new Hello();
h2.Hello();
}
}
- AgentMain 类:
public class AgentMainDemo {
public static void agentmain(String agentArgs, Instrumentation inst)
throws UnmodifiableClassException {
Class[] classes = inst.getAllLoadedClasses();
for (Class aClass : classes) {
if (aClass.getName().equals(TransformerDemo.editClassName)) {
inst.addTransformer(new TransformerDemo(), true);
inst.retransformClasses(aClass);
}
}
}
}
- Transformer 实现:
public class TransformerDemo implements ClassFileTransformer {
public static final String editClassName = "Agent.Hello";
public static final String editMethod = "Hello";
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
try {
ClassPool cp = ClassPool.getDefault();
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
cp.insertClassPath(ccp);
}
CtClass ctc = cp.get(editClassName);
CtMethod method = ctc.getDeclaredMethod(editMethod);
// 修改方法体
String source = "{System.out.println(\"Has been modified\");}";
method.setBody(source);
byte[] bytes = ctc.toBytecode();
ctc.detach();
return bytes;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
- MANIFEST.MF 配置:
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: Agent.AgentMainDemo
六、注意事项
- 对于 Web 服务器环境,需要使用
insertClassPath()添加类搜索路径:
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
cp.insertClassPath(ccp);
-
修改已加载类需要在 MANIFEST.MF 中添加:
Can-Redefine-Classes: trueCan-Retransform-Classes: true
-
注意 MANIFEST.MF 文件格式要求,最后必须有空行
-
使用 Attach API 需要引入 tools.jar(通常位于 JDK 的 lib 目录下)
七、总结
JavaAgent 技术提供了强大的字节码操作能力,可以用于:
- 性能监控
- 热部署
- AOP 实现
- 安全防护
- 调试工具开发
掌握 JavaAgent 技术需要理解 JVM 类加载机制、字节码操作工具(如 Javassist、ASM)以及 Instrumentation API 的使用。