文章目录
1.Java Agent 概述
Java Agent 是一种特殊的程序,从java5开始支持,它可以在不修改目标应用程序代码的前提下,通过 [Java 虚拟机](https://so.youkuaiyun.com/so/search?q=Java 虚拟机&spm=1001.2101.3001.7020)(JVM)提供的字节码增强技术,对目标应用的类加载过程进行干预,实现对类的字节码修改、增强或监控等功能。
2.Java Agent原理
2.1 Premain/Agentmain
两种加载方式。Premain / Agentmain 是 JVM 规范规定的“入口函数”,由 JVM 本身 在启动或 attach 时主动调用;Agent 作者只是按约定实现这两个静态方法,把启动参数和 Instrumentation 对象“交”给业务代码。
- premain(静态加载)(JDK5):在应用程序主类(main 方法)启动前加载并执行,通过
-javaagent命令行参数指定 Agent Jar 包。 - Agentmain(动态加载)(JDK6):在应用程序运行过程中动态附着(Attach)到目标 JVM 上并执行,适用于已启动的应用程序,通过 JVM 提供的 Attach API 实现。
public static void premain(String agentArgs, Instrumentation inst){}
public static void agentmain(String agentArgs, Instrumentation inst){}
这两个方法有两个入参:agentArgs(String)/Instrumentation inst(Instrumentation 接口的实例)
-
agentArgs(String):你在命令行或 attach 时给 Agent 的参数字符串。例如:
-javaagent:myagent.jar=port=8848,debug=true这里
port=8848,debug=true就是 agentArgs。 -
Instrumentation inst(Instrumentation 接口的实例):这是 JVM 内部创建好的“官方后门”对象,Agent 拿到它就能注册 ClassFileTransformer、retransformClasses 等。
2.2 Instrumentation ApI
Instrumentation是 Java SE 5 在java.lang.instrument包下引入的一个接口,接口提供了一组用于操作类和对象的方法,主要用于字节码操作。作用是接收一个byte[]的原始字节码,然后交由ClassFileTransformer进行字节码增强,然后返回修改过的byte[]。
| 作用 | 典型方法 | 说明 |
|---|---|---|
| 注册类转换器 | addTransformer | 类加载或重加载时被回调,拿到原始字节数组 |
| 重新转换已加载类 | retransformClasses | 对线上进程再次触发上述回调 |
| 重定义类 | redefineClasses | 直接替换类定义(风险高) |
| 查询已加载类 | getAllLoadedClasses | 做诊断、过滤 |
| 计算对象大小 | getObjectSize(obj) | 内存分析 |
| 添加 jar 到 boot classpath | appendToBootstrapClassLoaderSearch | 解决类可见性问题 |
通过上述方法可以实现的功能:
- 类转换:允许在类加载时对字节码进行修改。
- 代理类生成:可以在运行时生成新的类。
- 对象监控:可以获取JVM中的对象信息,如内存使用情况。
2.3 字节码增强技术
基于 JVM 的类加载机制,在类被加载到 JVM 之前,拦截类的字节码流,通过字节码操作框架(如 ASM、Javassist、Byte Buddy 等)对字节码进行修改,注入自定义逻辑。
| 特性 | ASM | Byte Buddy | cglib | Javassist |
|---|---|---|---|---|
| 学习曲线 | 陡峭(直接操作字节码) | 平缓(API 友好) | 中等 | 平缓(类似反射 API) |
| 性能 | 极高(直接生成字节码) | 高(运行时代理优化) | 高 | 中(基于反射封装) |
| 应用场景 | AOP 框架(如 Spring AOP) | 动态代理、测试框架 | 无接口类代理 | 运行时类修改 |
| API 风格 | 底层(操作字节码指令) | 面向对象(流畅 API) | 封装 ASM | 高级(类似反射) |
| 对 Java 版本支持 | 全版本(灵活适配) | 最新版本优先支持 | 主流版本 | 主流版本 |
| 依赖大小 | 小(核心库约 1 MB) | 中等(约 2 MB) | 依赖 ASM | 中等(约 3 MB) |
| 典型应用 | Spring、Hibernate | Mockito、Hibernate | Spring AOP | 动态代理、ORM 工具 |
这些字节码操作的框架是对字节码进行修改,然后由Agent将修改后的字节码交给JVM执行。
3.Java Agent的应用场景

APM:应用性能监控
4.Java Agent 生命周期
1. JVM 启动/attach
│
├─ 找到 premain/agentmain
│ ↓
│ 你的 premain/agentmain 把 Transformer 注册到 Instrumentation
│
2. 当某个类第一次加载(或 retransform)
│
├─ JVM 把原始字节码 byte[] 交给 Instrumentation
│ ↓
│ Instrumentation 回调 Transformer.transform(...)
│ ↓
│ Transformer 内部用 ASM/ByteBuddy 改字节码
│ ↓
│ Transformer 把新的 byte[] 返回给 Instrumentation
│
3. Instrumentation 把新字节码送回 JVM
│
└─ JVM 真正加载/替换这个类
数据流:
JVM --> 原始字节码 --> Instrumentation --> Transformer(用ASM/ByteBuddy 修改) --> 修改后字节码 --> Instrumentation --> JVM
5.如何使用 Java Agent?
5.1 使用步骤:
- 编写 Agent类:包含 premain() 或 agentmain() 方法。
- 编写 MANIFEST.MF 文件:指定 Agent 的入口类。
- 打包成 JAR 文件:包含 Agent 类和 MANIFEST 文件。
- 使用 Agent:通过指定 JVM 参数或 Attach 机制加载 Agent。
5.2 示例–实现方法开始和结束时打印日志
1.创建一个maven工程,作为一个agent。
2.引入POM,并指定agent的入口
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<descriptorRefs>
<!--将应用的所有依赖包都打到jar包中。如果依赖的是 jar 包,jar 包会被解压开,平铺到最终的 uber-jar 里去。输出格式为 jar-->
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<!-- 设置manifest配置文件-->
<manifestEntries>
<!--Premain-Class: 代表 Agent 静态加载时会调用的类全路径名。-->
<Premain-Class>com.linging.MethodAgentMain</Premain-Class>
<!--Agent-Class: 代表 Agent 动态加载时会调用的类全路径名。-->
<Agent-Class>com.linging.MethodAgentMain</Agent-Class>
<!--Can-Redefine-Classes: 是否可进行类定义。-->
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<!--Can-Retransform-Classes: 是否可进行类转换。-->
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<!--绑定到package生命周期阶段上-->
<phase>package</phase>
<goals>
<!--绑定到package生命周期阶段上-->
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
3.编写 Agent类:MethodAgentMain
我们使用了premain()静态加载方式,agentmain动态加载方式。并用到了Instrumentation类结合javassist代码生成库进行字节码的修改。
public class MethodAgentMain {
/** 被转换的类 */
public static final String TRANSFORM_CLASS = "org.example.agent.AgentTest";
/** 静态加载。Java agent指定的premain方法,会在main方法之前被调用 */
public static void premain(String args, Instrumentation instrumentation) {
System.out.println("premain start!");
addTransformer(instrumentation);
System.out.println("premain end!");
}
/** 动态加载。Java agent指定的agentmain方法,会在main方法之后被调用 */
public static void agentmain(String args, Instrumentation instrumentation) {
System.out.println("agentmain start!");
addTransformer(instrumentation);
Class<?>[] classes = instrumentation.getAllLoadedClasses();
if (classes != null){
for (Class<?> c: classes) {
if (c.isInterface() ||c.isAnnotation() ||c.isArray() ||c.isEnum()){
continue;
}
if (c.getName().equals(TRANSFORM_CLASS)) {
try {
System.out.println("retransformClasses start, class: " + c.getName());
/*
* retransformClasses()对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
* retransformClasses()可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
*/
instrumentation.retransformClasses(c);
System.out.println("retransformClasses end, class: " + c.getName());
} catch (UnmodifiableClassException e) {
System.out.println("retransformClasses error, class: " + c.getName() + ", ex:" + e);
e.printStackTrace();
}
}
}
}
System.out.println("agentmain end!");
}
private static void addTransformer (Instrumentation instrumentation) {
/* Instrumentation提供的addTransformer方法,在类加载时会回调ClassFileTransformer接口 */
instrumentation.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader l,String className, Class<?> c,ProtectionDomain pd, byte[] b){
if(className == null){
return null;
}
try {
className = className.replace("/", ".");
if (className.equals(TRANSFORM_CLASS)) {
final ClassPool classPool = ClassPool.getDefault();
final CtClass clazz = classPool.get(TRANSFORM_CLASS);
for (CtMethod method : clazz.getMethods()) {
/*
* Modifier.isNative(methods[i].getModifiers())过滤本地方法,否则会报
* javassist.CannotCompileException: no method body at javassist.CtBehavior.addLocalVariable()
* 报错原因如下
* 来自Stack Overflow网友解答
* Native methods cannot be instrumented because they have no bytecodes.
* However if native method prefix is supported ( Transformer.isNativeMethodPrefixSupported() )
* then you can use Transformer.setNativeMethodPrefix() to wrap a native method call inside a non-native call
* which can then be instrumented
*/
if (Modifier.isNative(method.getModifiers())) {
continue;
}
method.insertBefore("System.out.println(\"" + clazz.getSimpleName() + "."
+ method.getName() + " start.\");");
method.insertAfter("System.out.println(\"" + clazz.getSimpleName() + "."
+ method.getName() + " end.\");", false);
}
return clazz.toBytecode();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}, true);
}
}
4.编译打包
执行 mvn clean package 编译打包,最终打包生成了 agent jar 包

5.验证agent功能的测试类
创建一个maven工程。

6.测试agent静态加载
在 IDEA 的 Run/Debug Configurations 中,点击 Modify options,勾选上 add VM options,在 VM options 栏增加如下参数:
-javaagent:/ideaProject/local-project/spring-boot-db-sharding-demo/java-agent-demo/java-agent/target/java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar
public class AgentTest {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
System.out.println("process result: " + process());
System.out.println("=============================>");
Thread.sleep(5000);
}
}
public static String process() {
System.out.println("process!");
return "success";
}
}
运行 AgentTest.java的 main 方法,可以看到控制台日志:
premain start!
premain end!
AgentTest.main start.
AgentTest.process start.
process!
AgentTest.process end.
process result: success
=============================>
AgentTest.process start.
process!
AgentTest.process end.
process result: success
=============================>
AgentTest.process start.
process!
AgentTest.process end.
process result: success
=============================>
....
可以看到:process! 和 AgentTest.process end. 是方法的执行结果,然后:AgentTest.process start. 和 process result: success 是字节码增强的打印日志。
7.测试agent动态加载
动态加载不是通过 -javaagent: 的方式实现,而是通过 Attach API 的方式。
编写调用 Attach API 的测试类,如果tools中的jar包未加载,则java8可以直接在idea中添加tools.jar依赖。
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class AttachMain {
public static void main(String[] args) throws Exception {
List<VirtualMachineDescriptor> listBefore = VirtualMachine.list();
// agentmain()方法所在jar包
String jar = "/ideaProject/local-project/spring-boot-db-sharding-demo/java-agent-demo/java-agent/target/java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar";
for (VirtualMachineDescriptor virtualMachineDescriptor : VirtualMachine.list()) {
// 针对指定名称的JVM实例
if (virtualMachineDescriptor.displayName().equals("org.example.agent.AgentTest")) {
System.out.println("将对该进程的vm进行增强:org.example.agent.AgentTest的vm进程, pid=" + virtualMachineDescriptor.id());
// attach到新JVM
VirtualMachine vm = VirtualMachine.attach(virtualMachineDescriptor);
// 加载agentmain所在的jar包
vm.loadAgent(jar);
// detach
vm.detach();
}
}
}
}
先直接运行 org.example.agent.AgentTest#main,注意不用加 -javaagent: 启动参数。
约15秒后,再运行 org.example.agent.AttachMain#main,可以看到 org.example.agent.AttachMain#main 打印的日志:
将对该进程的vm进行增强:org.example.agent.AgentTest的vm进程, pid=25216
之后可以看到 org.example.agent.AgentTest#main打印的日志中多了记录方法运行开始和结束的内容:
process!
process result: success
=============================>
process!
process result: success
=============================>
process!
process result: success
=============================>
agentmain start!
retransformClasses start, class: org.example.agent.AgentTest
retransformClasses end, class: org.example.agent.AgentTest
agentmain end!
AgentTest.process start.
process!
AgentTest.process end.
process result: success
=============================>
AgentTest.process start.
process!
AgentTest.process end.
process result: success
=============================>
AgentTest.process start.
process!
AgentTest.process end.
process result: success
=============================>
AgentTest.process start.
process!
AgentTest.process end.
process result: success
=============================>
.....
可以看到动态增强了。
Java Agent原理与实战
2533

被折叠的 条评论
为什么被折叠?



