Java Agent是一种特殊的 Java 程序-本质是一个jar包,它能够在 JVM 加载类文件之前或运行时对类字节码进行检测和转换。这种技术基于Java Instrumentation API,允许开发人员在类被加载到 JVM 时修改其字节码。
其运行机制:Agent 拦截类加载→ 修改该类字节码→ JVM 加载修改后的字节码→ 执行增强后的功能
其核心价值是:无侵入增强原代码能力 - 不修改源代码即可添加功能;运行时洞察 - 深入了解应用运行状态;自动化治理 - 自动执行代码质量和安全检查;开发效率 - 加速调试和问题定位过程;运维支持 - 提供生产环境诊断和修复能力等能力。
其主要作用是:
类加载时转换:在类被加载到JVM之前,可以修改类的字节码。
运行时类重定义:在类已经加载后,可以重新转换类的字节码。
监控和性能分析:例如,可以用于收集方法执行时间、内存使用情况等。
日志增强:在方法执行前后自动添加日志。
AOP(面向切面编程):实现如事务管理、安全检查等横切关注点。
热修复:在不重启JVM的情况下,修复已经加载的类。
假设我们的应用程序是一个已经建好的房子,Java Agent 就像是一个神奇的装修队,他们能在不拆墙、不动结构的情况下,给房子增加新功能,只是其增强都是在内存中完成,原始代码毫发无损。
类正常加载过程(无Agent干预):
源代码编译:Java源代码(.java文件)被编译器(javac)编译成字节码,存储在.class文件中。
类加载请求:当程序运行时,JVM需要用到某个类时,会发起类加载请求。
加载(Loading):由类加载器(ClassLoader)查找并读取.class文件,将字节码数据加载到JVM内存中。
连接(Linking):连接阶段又分为三个子步骤:
1.验证(Verification):检查字节码是否符合JVM规范,确保代码不会危害JVM的安全。
2.准备(Preparation):为类的静态变量分配内存并设置初始值(默认值,如0、null等)。
3.解析(Resolution):将符号引用转换为直接引用(将常量池中的引用替换为指向具体内存地址的指针)。 初始化(Initialization):执行类的静态初始化代码(静态变量赋值和静态代码块)。
类可用:类加载完成,程序可以创建该类的实例或调用该类的静态方法。如下流程:
1. 源代码编写 2. 编译为字节码 (.java → .class) 3. 类加载器读取 .class 文件 4. 字节码验证(安全性检查) 5. 准备阶段(分配内存空间) 6. 解析阶段(符号引用→直接引用) 7. 初始化阶段(执行静态代码块) 8. 类加载完成,可供使用
使用Agent之后的类加载过程:
源代码编译:同样,源代码被编译成字节码,存储在.class文件中。
类加载请求:程序运行时,JVM需要用到某个类,发起类加载请求。
加载(Loading):类加载器查找并读取.class文件,将字节码数据加载到JVM内存中。
Agent拦截:在类被加载到JVM并正式被连接之前,Java Agent会拦截这个类的字节码。 Agent中注册的ClassFileTransformer会接收到类的字节码。 Transformer可以对字节码进行修改,然后返回修改后的字节码。
连接(Linking):同样包括验证、准备、解析三个子步骤,但验证的是修改后的字节码。
1.验证:JVM验证修改后的字节码,确保其安全性及符合规范。
2.准备:为静态变量分配内存并设置初始值。
3.解析:将符号引用转换为直接引用。
初始化(Initialization):执行类的静态初始化代码。 类可用:类加载完成,此时类已经是经过Agent修改的版本,程序使用修改后的类。如下流程:
1. 源代码编写 2. 编译为字节码 (.java → .class) 3. 类加载器读取 .class 文件 4. 【关键拦截点】Agent 拦截类加载请求 5. Agent 获取原始字节码 6. Agent 修改字节码(添加监控、日志、安全等逻辑) 7. 返回修改后的字节码给 JVM 8. 字节码验证(验证修改后的字节码) 9. 准备阶段(分配内存空间) 10. 解析阶段(符号引用→直接引用) 11. 初始化阶段(执行静态代码块)
关键区别:有Agent:在类加载的连接阶段之前,Agent有机会修改字节码,从而改变类的代码逻辑。
原始字节码准备加载
↓
【Agent 拦截点】
↓
Agent 接收原始字节码
↓
Agent 执行字节码转换
↓
Agent 返回修改后字节码
↓
【拦截结束】
↓
JVM 继续正常加载流程(但使用的是修改后的字节码)
Java Agent本身是一个JAR包文件,其中包含一个或多个类,但必须有一个类作为代理的入口点。这个入口类需要包含特定的方法(如premain或agentmain):其中包含:
如何使用:
打包代理:将代理类和清单文件打包成JAR。 使用代理:通过JVM参数-javaagent指定代理JAR,或者在运行时通过Attach API动态加载。
概念了解,直接操作一下。
准备阶段:
首先需要一个待被修改的服务端。使用idea新建一个项目:AgentT项目结构如下:

再来一个等待被修改字节码的类:就叫Test.java类,代码如下,就打印一句话
package my.example;
public class Test {
public void printTest(){
System.out.println("Test执行中~~~~~!");
}
}
新建一个程序入口:MyAgent.java,代码如下,创建Test对象,每一秒钟调用一次它的printTest()方法。
package my.example;
public class MyAgent {
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
while (true) {
test.printTest();
Thread.sleep(1000);
}
}
}
运行之后持续输出一句话-如图:

这是正常执行,那么在不更改源代码的情况下,让它执行其他代码逻辑就是Agent的要完成的工作!
首先通过Maven再创建一个新的项目:AgentJar
在maven的pom.xml配置文件中引入对应的依赖包:项目结构如下图:

项目依赖以下:
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId><!-- 插件引入 -->
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef><!-- 打包规范 -->
</descriptorRefs>
<archive>
<manifestEntries><!-- 这些配置决定了 Java Agent 的能力范围和运行方式 -->
<Premain-Class>PremainAgent</Premain-Class><!--Premain方式 **JVM 启动时加载 Agent 会执行此类的方法,应用启动前的字节码增强、性能监控初始化*/ -->
<Agent-Class>AgentMainTest</Agent-Class><!-- AgentMain方式在目标 JVM 运行时,运行时诊断、热补丁、动态分析,执行此类方法 -->
<Can-Redefine-Classes>true</Can-Redefine-Classes><!-- 需要修改已加载类逻辑的场景 -->
<Can-Retransform-Classes>true</Can-Retransform-Classes><!-- 需要动态调整字节码转换策略的场景-->
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals><goal>single</goal></goals>
</execution>
</executions>
</plugin>
</plugins>
实现premain方法
根据理论中的实现条件一一实现:
要实现premain必须有个代理类 ,其方法通常为: public static void premain(String agentArgs, Instrumentation inst) 或者 public static void premain(String agentArgs),当使用java -javaagent:your-agent.jar启动时,JVM就会调用AgentClass中的premain方法。
1.根据配置文件设置,代理类的类名为这个PremainAgent:新建这个代理类PremainAgent.java:
import java.lang.instrument.Instrumentation;
public class PremainAgent {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new TestTransFormer());//拦截类加载,注册新得字节码
}
}
2.再创建其使用到的字节码转换器TestTransFormer.java类:代码如下
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.ProtectionDomain;
public class TestTransFormer implements ClassFileTransformer {
@Override
//transform 是一个由 JVM 自动调用的回调方法。重写该方法,主要返回修改后得类字节码,类加载时/类重新转换时JVM 自动调用每个ClassFileTransformer 的 transform 方法
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.equals("my/example/Test")){//如果className等于my/example/Test这个class就执行下面得代码,将字节码文件返回
/*调用下面的readFile方法,传入要修改的类文件路径,让其返回字节码,Test.java类必须与被修改的服务器的项目内路径,比如my.examplie下的Test.java双方保持一致
路径为与被修改的服务器的同类名的Class文件绝对路径。*/
return readFile("D:\\com.first\\AgentJar\\target\\classes\\my\\example\\Test.class");
}
return new byte[0];
}
public static byte[] readFile(String file){//用于根据文件路径,将其转换成字节码,是修改之后的同名的类的字节码
try {
Path filePath = Paths.get(file);
return Files.readAllBytes(filePath);
} catch (IOException e) {
throw new RuntimeException("读取失败 " + file, e);
}
}
}
主要是方法transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer);transform 方法是 ClassFileTransformer 接口的核心方法,负责在类加载时拦截并修改字节码:
各个参数作用:ClassLoader loader - 类加载器,表示哪个类加载器正在加载这个类
String className -类名,作用:当前正在被转换的类的完全限定名
Class<?> classBeingRedefined -被重新定义的类:null :类正在第一次加载,非 null :类正在被重新定义或重新转换
ProtectionDomain protectionDomain -保护域,作用:包含类的安全相关信息。
byte[ ] classfileBuffer -类文件缓冲区,包含原始类文件的完整字节码
3.然后创建一个用来修改准备阶段创建Test类--该类必须与准备阶段的项目路径保持一致(被修改的类路径)。这里都必须是my.example下:带入如下,但是方法里面执行的逻辑变了:
package my.example;
public class Test {
public void printTest(){
System.out.println("新的Test执行了~~~~~!");
}
}
最终项目结构如下:两个不同的项目文件结构:

premain执行
编译字节码:首先就需要得到字节码文件:通过终端这里 mvn compile 编译字节码文件:得到Test.class字节码文件。该文件是我们的与服务端同名的Test.java文件生成的,方法中的内容已经被修改了:
编译为class文件:mvn compile如下图:

然后将项目打包成jar文件:mvn clean package,将在Target目录下生成jar文件:

实现方式所有得准备工作都做完了。前面知道premain的方式执行:
所以在被修改的项目下执行:java -javaagent:D:\com.first\AgentJar\target\AgentJar-1.0-SNAPSHOT-jar-with-dependencies.jar my.example.MyAgent