Java代理之Java Agent分析
1 Java Agent
1.1 简介
1.1.1 定义
Java Agent 是一种用于在 Java 应用启动或运行过程中对其进行监控、修改或增强的机制。它利用 Java Instrumentation API,可以在应用启动时或运行中动态加载代码,对目标应用的字节码进行操作。这种机制非常适合应用监控、性能分析、调试、代码注入和安全性增强等任务。
简单来说,Java Agent 就是运行在 Java 虚拟机(JVM)上的一种工具,能在程序运行时对其进行监控、修改甚至重定义。它的作用和 AOP(面向切面编程)有点类似,但更加底层,直接作用在 JVM 层面。可以理解为它是全局的 AOP,能在类加载、方法执行等时刻动态插手程序行为。
Java Agent 的核心其实就是 Instrumentation 接口:
- 加载类前注册一个
ClassFileTransformer - 重新转换已加载类(如果支持)
- 获取所有已加载的类
- 判断 JVM 是否支持
redefine、retransform
1.1.2 与代理区别
Java Agent 可以说是一种“代理”工具,但它的代理作用和一般的代理(例如 Java 中的 Proxy 类)有些不同。Java Agent 主要是通过修改类字节码的方式,实现在不直接修改原始代码的情况下对程序的运行行为进行增强或拦截。
Java Agent 和普通代理的区别:
- 字节码层面的代理:
Java Agent是在类加载时,通过字节码操作来修改类的定义,因此属于低层次的代理。这不同于使用Java动态代理或CGLIB代理,它不需要在代码中显式调用代理方法。 - 无侵入性:
Java Agent能够在应用启动或运行时注入代理逻辑,不需要修改原始代码。比如 APM 工具的 Java Agent 就能自动为应用添加性能监控,无需在每个方法中手动添加监控代码。 - 全局作用:
Java Agent可以对JVM中的所有类进行代理操作(包括 JDK 自带类),并不是针对某个对象或接口的代理。代理逻辑可以应用于整个JVM中加载的所有类,适用范围更广。
与普通代理的对比
| 特性 | Java Agent | Java 动态代理 / CGLIB 代理 |
|---|---|---|
| 代理方式 | 字节码操作 | 接口或子类方法拦截 |
| 实现时机 | JVM 启动时 / 运行时注入 | 编码时指定代理逻辑 |
| 侵入性 | 无侵入,自动加载 | 需要在代码中显式调用代理类 |
| 作用范围 | 全局所有类 | 某个对象或接口 |
| 典型用途 | 性能监控、日志注入、调试等 | 业务逻辑中的代理模式 |
1.1.3 主要功能和用途
主要作用:
- 性能监控:可以捕获应用程序的性能数据,比如方法调用次数、执行时间、内存消耗等,生成性能报告。例如,常见的
APM(应用性能监控)工具如 New Relic、Dynatrace 等都使用了Java Agent技术。 - 字节码增强:在类加载时修改类的字节码,比如添加日志、修改方法逻辑、实现代码注入等。
Java Agent可以在应用运行时拦截并修改方法,使其在不改变原始代码的情况下增加额外功能。 - 动态调试:在不重启应用的情况下动态附加
Java Agent,可以实时监控或调试生产环境中的问题。 - 应用安全性:可以为应用增加安全性检查,例如在方法调用前加入权限验证,或在检测到异常行为时触发报警。
- 测试增强:可以利用 Java Agent 对应用内部行为进行模拟或监控,增强自动化测试或集成测试的功能。
1.2 原理和模式
Java Agent 使用 java.lang.instrument.Instrumentation 接口来对类的字节码进行修改。其基本流程如下:
- 创建代理类:编写一个含有
premain或agentmain方法的代理类。premain 用于在应用启动时加载,agentmain 用于在应用运行时动态附加。 - 实现字节码操作:在代理类中,通过
Instrumentation对象,可以拦截和修改字节码,比如用 Java ASM 或 Javassist 等字节码工具来修改类文件。 - 打包和运行:将代理类打包为 jar 并设置清单文件中的 Premain-Class 或 Agent-Class 属性,使 Java 在启动时加载该代理。
Java Agent 主要有两种模式:Premain模式和Agentmain模式:
Premain模式:在程序启动前就能注入
这种模式通常是我们在程序启动时就注入Agent,常见于应用启动时的初始化操作。
例如:在程序启动时,配置一些监控、日志、性能分析工具。通过这种方式,Agent 可以在应用的生命周期中,从一开始就进行干预。
使用场景:初始化操作、性能监控、日志收集等。Agentmain模式:动态注入Agent
这种模式是指在程序启动后,动态地将Agent注入到正在运行的JVM中。在主程序已经启动并且运行的过程中,也可以通过一些工具(比如 attach API)把 Agent 加入到 JVM 中。
这种方式主要用于热更新和动态调试。
使用场景:热部署、动态调整配置、动态监控等。
1.3 使用实现
1.3.1 Premain 模式
1.3.1.1 创建Agent类
首先,我们需要创建一个 Java 类,通常这个类会有一个静态方法 premain,它会在主程序启动前被执行。
import java.lang.instrument.Instrumentation;
public class MyAgent {
// premain方法会在main方法之前执行
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("Java Agent initialized!");
// 注册一个类的转换器
inst.addTransformer(new MyClassFileTransformer());
}
static class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
// 这里可以对字节码进行修改
System.out.println("Transforming class: " + className);
return classfileBuffer; // 返回修改后的字节码
}
}
}
1.3.1.2 配置Maven
我们需要通过 Maven 配置项目的构建方式,将这个 Agent 类打包成一个 JAR 文件。关键在于 MANIFEST.MF 文件中的配置,需要指定 Agent 类的入口点。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>com.example.MyAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
或者 Jar 包打包时需要在 MANIFEST.MF 中加上:
Premain-Class: com.example.AgentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true
1.3.1.3 启动程序时指定
打包好之后,我们只需在启动程序时通过 -javaagent 参数来指定这个 Agent JAR 文件。例如:
java -javaagent:/path/to/myagent.jar -jar myapp.jar
参数说明:
-javaagent:参数后面跟的是一个Java代理的JAR文件路径。这个代理可以在应用程序启动之前或运行期间对字节码进行修改或增强,常用于性能监控、日志记录等功能。/path/to/myagent.jar:Java代理的 JAR 文件的完整路径-jar myapp.jar:指定了要运行的主应用程序的 JAR 文件路径
1.3.2 Agentmain模式
假如要在程序运行时动态注入一个 Java Agent,可以使用 Agentmain 模式。这种方式可以在程序启动之后,通过附加到一个已经在运行的 JVM 来注入代码。运行时加载 agent,用的是 tools.jar 的 VirtualMachine.attach()。
1.3.2.1 通过 Attach API 动态注入
这种方式依赖于 Attach API,它允许在程序运行时,将一个新的 Agent 附加到正在运行的 JVM 上。
import com.sun.tools.attach.*;
public class AgentAttacher {
public static void main(String[] args) throws Exception {
String pid = args[0]; // 获取目标进程的PID
String agentJarPath = args[1]; // 要注入的Agent路径
// 获取目标JVM的虚拟机进程
VirtualMachine vm = VirtualMachine.attach(pid);
// 向目标JVM进程注入Agent
vm.loadAgent(agentJarPath);
vm.detach(); // 注入后断开与目标JVM的连接
}
}
这段代码通过 VirtualMachine.attach(pid) 连接到目标 JVM 进程,然后通过 loadAgent() 方法将 Java Agent 动态注入。这里的 pid 就是目标 JVM 进程的 ID,你可以通过工具(如 jps)来获取。
1.3.2.2 示例
比如下面类启动后,会不断打印出100这个数字,我们通过Attach功能使之打印出50这个数字
主程序
public class PrintNumTest{
public static void main(String[] args)throws InterruptedException {
while (true) {
System.out.println(getNum());
Thread.sleep(3000);
}
}
private static int getNum(){
return100;
}
}
定义一个ClassFileTransformer,使用ASM框架修改getNum()方法
public class PrintNumTransformer implements ClassFileTransformer{
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if ("com/example/aop/agent/PrintNumTest".equals(className)) {
System.out.println("asm");
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new TransformPrintNumVisitor(Opcodes.ASM7, cw);
cr.accept(cv, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
return cw.toByteArray();
}
return classfileBuffer;
}
}
public class TransformPrintNumVisitor extends ClassVisitor{
public TransformPrintNumVisitor(int api, ClassVisitor classVisitor){
super(Opcodes.ASM7, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions){
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
if (name.equals("getNum")) {
return new TransformPrintNumAdapter(api, mv, access, name, descriptor);
}
return mv;
}
}
public class TransformPrintNumAdapter extends AdviceAdapter{
protected TransformPrintNumAdapter(int api, MethodVisitor methodVisitor, int access, String name, String descriptor){
super(api, methodVisitor, access, name, descriptor);
}
@Override
protected void onMethodEnter(){
super.visitIntInsn(BIPUSH, 50);
super.visitInsn(IRETURN);
}
}
public class PrintNumAgent{
public static void agentmain(String agentArgs, Instrumentation inst)throws UnmodifiableClassException {
System.out.println("agentmain");
inst.addTransformer(new PrintNumTransformer(), true);
Class[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class allLoadedClass : allLoadedClasses) {
if (allLoadedClass.getSimpleName().equals("PrintNumTest")) {
System.out.println("Reloading: " + allLoadedClass.getName());
inst.retransformClasses(allLoadedClass);
break;
}
}
}
}
maven 配置说明
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.1</version>
<configuration>
<descriptorRefs>
<!--将应用的所有依赖包都打到jar包中。如果依赖的是 jar 包,jar 包会被解压开,平铺到最终的 uber-jar 里去。输出格式为 jar-->
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
// 指定agentmain所在的类
<Agent-CLass>com.example.aop.agent.PrintNumAgent</Agent-CLass>
<Premain-Class>com.example.aop.agent.PrintNumAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
因为是跨进程通信,Attach的发起端是一个独立的java程序,这个java程序会调用VirtualMachine.attach方法开始合目标JVM进行跨进程通信
public class MyAttachMain{
public static void main(String[] args)throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
VirtualMachine virtualMachine = VirtualMachine.attach(args[0]);
try {
virtualMachine.loadAgent("/Users/zhangxiaobin/IdeaProjects/aop-demo/target/aop-0.0.1-SNAPSHOT-jar-with-dependencies.jar");
} finally {
virtualMachine.detach();
}
}
}
使用jps查询到PrintNumTest的进程id,再用下面的命令执行MyAttachMain类
java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/lib/tools.jar:/Users/zhangxiaobin/IdeaProjects/aop-demo/target/aop-0.0.1-SNAPSHOT-jar-with-dependencies.jar com.example.aop.agent.MyAttachMain 49987
1.4 Instrumentation接口
Instrumentation 是 Java Agent 的核心接口,它提供了修改和操作 JVM 中加载的类的能力。通过 Instrumentation,可以修改类字节码、重定义已有类,甚至能在类加载时插手,动态地修改类行为。
1.4.1 核心功能
Instrumentation 的核心功能:
Instrumentation 接口的功能非常丰富,以下是一些关键功能及其用途:
- 修改类定义:
可以在类加载前,通过ClassFileTransformer对类字节码进行修改。
使用redefineClasses方法在类已经加载后重新定义该类,这样可以在运行时修改类的行为。 - 添加和移除
ClassFileTransformer:
ClassFileTransformer是一个用于修改类字节码的接口。通过Instrumentation,可以将一个ClassFileTransformer添加到 JVM 中,监控或更改所有类的字节码。
可以使用addTransformer方法将ClassFileTransformer添加到Instrumentation实例中,之后每次加载类时都会触发 transform 方法进行字节码修改。 - 获取对象大小:
使用getObjectSize(Object object)可以获取某个对象的大小,主要用于内存分析工具中。它可以精确地获取Java对象在内存中的占用空间。 - 动态代理:
Instrumentation可以在运行时创建动态代理类,这样可以为现有的对象添加新的方法或行为。代理类可以拦截方法调用,实现方法增强。 - 检索所有加载的类:
getAllLoadedClasses()方法可以返回 JVM 中所有已经加载的类,方便进行全局监控或分析。 - 检测类是否已加载:
使用isModifiableClass(Class<?> theClass)方法可以检查某个类是否可以修改,以避免对不支持的类进行重新定义而导致错误。 - 添加类卸载事件处理器:
Instrumentation提供了类卸载的通知支持,可以用来监控类的卸载事件。可以用于记录对象的生命周期,监控资源的使用情况等。
1.4.2 典型用法
以下是一些 Instrumentation 的常见用法场景:
- 性能监控工具(APM):
可以通过ClassFileTransformer修改类字节码,添加方法进入和退出的时间记录,从而计算方法的执行时间,并汇总性能数据。 - 内存监控:
可以通过getObjectSize方法估算内存中对象的实际大小,结合类加载监控来分析内存泄露等问题。 - 调试和测试工具:
可以对类的行为进行修改,注入调试信息或测试代码。
例如,在测试时可以通过redefineClasses修改类定义,不用重启应用来验证新代码的逻辑。 - 安全增强:
在类加载时对字节码进行检查或修改,防止某些不安全的操作或方法被调用,提高程序的安全性。
1.4.3 操作示例
以下是一个例子,展示了如何使用 Instrumentation 来修改类的字节码:
import java.lang.instrument.*;
public class MyClassTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className.equals("com/example/MyClass")) {
// 这里可以使用 Javassist 或 ASM 等库来修改字节码
System.out.println("Transforming MyClass...");
// 返回修改后的字节码
return modifiedClassBytecode;
}
return null;
}
}
Java Agent 最常见的应用之一是性能监控。举个例子,我们可以通过 Agent 动态地修改类的字节码,来插入一些监控代码,记录方法执行时间、内存使用等信息。通过这种方式,我们无需修改现有代码,只需通过 Agent 即可实现监控。
比如,要监控某个方法的执行时间,可以在方法的入口和出口插入日志代码,记录执行时间:
public class MyClass {
public void myMethod() {
long start = System.currentTimeMillis();
// 方法逻辑
long end = System.currentTimeMillis();
System.out.println("Method executed in " + (end - start) + " ms");
}
}
通过 Agent 插入这个监控代码,可以动态获取到该方法的执行时间,无需修改源代码。

浙公网安备 33010602011771号