1. 简介
本文将深入探讨 Java Instrumentation API,它允许我们在不修改源码的前提下,动态修改已编译的 Java 类字节码。这在性能监控、APM 工具开发、热修复等场景中非常实用。
我们还会介绍 Java Agent 的概念,以及如何利用它实现字节码增强(instrumentation)。整个过程无需改动原有业务代码,简单粗暴却极其强大。
2. 项目结构与目标
我们通过一个实际案例来演示:构建一个 ATM 取款应用,并通过 Java Agent 动态为其添加性能耗时统计功能。
整个项目包含两个模块:
application
:ATM 应用主程序,提供取款功能agent
:Java Agent 模块,负责在运行时修改 ATM 类的字节码,插入耗时统计逻辑
✅ 核心优势:无需修改 ATM 源码,即可为其添加方法执行时间监控
项目 Maven 结构如下:
<groupId>com.baeldung.instrumentation</groupId>
<artifactId>base</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>agent</module>
<module>application</module>
</modules>
在深入细节前,先搞清楚什么是 Java Agent。
3. 什么是 Java Agent
Java Agent 本质上是一个特殊的 JAR 包,它利用 JVM 提供的 Instrumentation API 在类加载时或运行时修改其字节码。
一个 Agent 要生效,必须实现以下两个方法之一(或两者都实现):
premain
:在 JVM 启动时通过-javaagent
参数静态加载agentmain
:在 JVM 运行时通过 Java Attach API 动态 attach
⚠️ 注意:动态 attach 的能力依赖于 JVM 实现(如 OpenJDK、Oracle JDK 支持),并非所有 JVM 都提供。
接下来我们先看如何使用 Agent,再手搓一个。
4. 加载 Java Agent
Agent 支持两种加载方式:静态加载和动态加载。
4.1 静态加载(Static Load)
在应用启动时通过 -javaagent
参数加载,字节码修改发生在任何业务代码执行之前。
使用方式:
java -javaagent:agent.jar -jar application.jar
📌 必须将 -javaagent
放在 -jar
前面,否则不会生效。
执行日志如下:
22:24:39.296 [main] INFO - [Agent] In premain method
22:24:39.300 [main] INFO - [Agent] Transforming class MyAtm
22:24:39.407 [main] INFO - [Application] Starting ATM application
22:24:41.409 [main] INFO - [Application] Successful Withdrawal of [7] units!
22:24:41.410 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!
22:24:53.411 [main] INFO - [Application] Successful Withdrawal of [8] units!
22:24:53.411 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!
可以看到:
premain
优先执行MyAtm
类被成功 transform- 原本没有耗时日志的 ATM 应用,现在自动输出了每笔交易耗时
这就是字节码增强的威力。
4.2 动态加载(Dynamic Load)
将 Agent attach 到一个正在运行的 JVM 进程中,实现“热插拔”功能。适用于生产环境临时诊断性能问题,无需重启服务。
场景
ATM 应用已在生产运行,我们想临时开启耗时监控。
实现方式
使用 Java Attach API 编写一个 AgentLoader
:
VirtualMachine jvm = VirtualMachine.attach(jvmPid);
jvm.loadAgent(agentFile.getAbsolutePath());
jvm.detach();
为了简化演示,我们将 AgentLoader
放在 application 模块中。
再写一个启动类 Launcher
,用于区分是启动应用还是加载 Agent:
public class Launcher {
public static void main(String[] args) throws Exception {
if(args[0].equals("StartMyAtmApplication")) {
new MyAtmApplication().run(args);
} else if(args[0].equals("LoadAgent")) {
new AgentLoader().run(args);
}
}
}
启动应用
java -jar application.jar StartMyAtmApplication
22:44:21.154 [main] INFO - [Application] Starting ATM application
22:44:23.157 [main] INFO - [Application] Successful Withdrawal of [7] units!
动态 attach Agent
在第一次交易后,执行:
java -jar application.jar LoadAgent
22:44:27.022 [main] INFO - Attaching to target JVM with PID: 6575
22:44:27.306 [main] INFO - Attached to target JVM and loaded Java agent successfully
查看效果
后续交易自动带上耗时日志:
22:44:27.229 [Attach Listener] INFO - [Agent] In agentmain method
22:44:27.230 [Attach Listener] INFO - [Agent] Transforming class MyAtm
22:44:33.157 [main] INFO - [Application] Successful Withdrawal of [8] units!
22:44:33.157 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!
✅ 动态生效!这就是 agentmain
的作用。
5. 手写一个 Java Agent
现在我们从零实现一个 Agent,核心是使用 Javassist 操作字节码。
5.1 Instrumentation API 核心方法
Agent 依赖 java.lang.instrument.Instrumentation
接口,常用方法有:
addTransformer
:注册类转换器getAllLoadedClasses
:获取当前 JVM 所有已加载类retransformClasses
:对已加载类重新转换(需开启Can-Retransform-Classes
)removeTransformer
:移除转换器redefineClasses
:完全替换类定义(慎用,会丢失原有类状态)
5.2 实现 premain
和 agentmain
为了让 Agent 支持静态和动态加载,我们两个方法都实现:
public static void premain(
String agentArgs, Instrumentation inst) {
LOGGER.info("[Agent] In premain method");
String className = "com.baeldung.instrumentation.application.MyAtm";
transformClass(className, inst);
}
public static void agentmain(
String agentArgs, Instrumentation inst) {
LOGGER.info("[Agent] In agentmain method");
String className = "com.baeldung.instrumentation.application.MyAtm";
transformClass(className, inst);
}
transformClass
方法负责查找目标类并触发转换:
private static void transformClass(
String className, Instrumentation instrumentation) {
Class<?> targetCls = null;
ClassLoader targetClassLoader = null;
try {
targetCls = Class.forName(className);
targetClassLoader = targetCls.getClassLoader();
transform(targetCls, targetClassLoader, instrumentation);
return;
} catch (Exception ex) {
LOGGER.error("Class [{}] not found with Class.forName", className);
}
for(Class<?> clazz : instrumentation.getAllLoadedClasses()) {
if(clazz.getName().equals(className)) {
targetCls = clazz;
targetClassLoader = targetCls.getClassLoader();
transform(targetCls, targetClassLoader, instrumentation);
return;
}
}
throw new RuntimeException("Failed to find class [" + className + "]");
}
private static void transform(
Class<?> clazz,
ClassLoader classLoader,
Instrumentation instrumentation) {
AtmTransformer dt = new AtmTransformer(clazz.getName(), classLoader);
instrumentation.addTransformer(dt, true); // true 表示支持 retransform
try {
instrumentation.retransformClasses(clazz);
} catch (Exception ex) {
throw new RuntimeException("Transform failed for: [" + clazz.getName() + "]", ex);
}
}
5.3 实现 ClassFileTransformer
转换器必须实现 ClassFileTransformer
接口,核心是 transform
方法:
public class AtmTransformer implements ClassFileTransformer {
@Override
public byte[] transform(
ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
byte[] byteCode = classfileBuffer;
String finalTargetClassName = this.targetClassName.replaceAll("\\.", "/");
if (!className.equals(finalTargetClassName)) {
return byteCode;
}
if (className.equals(finalTargetClassName) && loader.equals(targetClassLoader)) {
LOGGER.info("[Agent] Transforming class MyAtm");
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get(targetClassName);
CtMethod m = cc.getDeclaredMethod("withdrawMoney");
// 插入开始时间
m.addLocalVariable("startTime", CtClass.longType);
m.insertBefore("startTime = System.currentTimeMillis();");
StringBuilder endBlock = new StringBuilder();
m.addLocalVariable("endTime", CtClass.longType);
m.addLocalVariable("opTime", CtClass.longType);
endBlock.append("endTime = System.currentTimeMillis();");
endBlock.append("opTime = (endTime-startTime)/1000;");
endBlock.append("LOGGER.info(\"[Application] Withdrawal operation completed in:\" + opTime + \" seconds!\");");
// 在方法末尾插入耗时日志
m.insertAfter(endBlock.toString());
byteCode = cc.toBytecode();
cc.detach();
} catch (NotFoundException | CannotCompileException | IOException e) {
LOGGER.error("Exception during transformation", e);
}
}
return byteCode;
}
}
✅ 踩坑提示:retransformClasses
需要在 addTransformer
时传入 true
,否则会抛 UnmodifiableClassException
。
5.4 编写 Agent Manifest 文件
Agent JAR 必须在 MANIFEST.MF
中声明关键属性:
Agent-Class: com.baeldung.instrumentation.agent.MyInstrumentationAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.baeldung.instrumentation.agent.MyInstrumentationAgent
📌 注意:
Can-Retransform-Classes: true
是动态修改字节码的前提- 类名必须写全路径
至此,Agent 开发完成。使用方式见第 4 节。
6. 总结
本文系统介绍了 Java Instrumentation 的核心机制:
- ✅ 静态加载:通过
-javaagent
在启动时增强 - ✅ 动态加载:通过 Attach API 实现运行时 attach
- ✅ 字节码操作:使用 Javassist 在方法前后插入逻辑
- ✅ 无侵入监控:业务代码零改动实现 APM 功能
Instrumentation 是 JVM 高级特性,掌握后可开发出强大的诊断工具。实际项目中,像 SkyWalking、Pinpoint 等 APM 框架底层都依赖此技术。
完整代码示例见 GitHub 仓库。