Java Agent 技术简介
Java Agent 直译为 Java 代理,也常常被称为 Java 探针技术。
Java Agent 是在 JDK1.5 引入的,是一种可以动态修改 Java 字节码的技术。Java 中的类编译后形成字节码被 JVM 执行,在 JVM 在执行这些字节码之前获取这些字节码的信息,并且通过字节码转换器对这些字节码进行修改,以此来完成一些额外的功能。
Java Agent 是一个不能独立运行 jar 包,它通过依附于目标程序的 JVM 进程,进行工作。启动时只需要在目标程序的启动参数中添加-javaagent 参数添加 ClassFileTransformer 字节码转换器,相当于在main方法前加了一个拦截器。
Java Agent 功能介绍
Java Agent 主要有以下功能
- Java Agent 能够在加载 Java 字节码之前拦截并对字节码进行修改;
- Java Agent 能够在 Jvm 运行期间修改已经加载的字节码;
Java Agent 的应用场景
- IDE 的调试功能,例如 Eclipse、IntelliJ IDEA ;
- 热部署功能,例如 JRebel、XRebel、spring-loaded;
- 各种线上诊断工具,例如 Btrace、Greys,还有阿里的 Arthas;
- 各种性能分析工具,例如 Visual VM、JConsole 等;
- 全链路性能检测工具,例如 Skywalking、Pinpoint等;
关键术语介绍
-
「 JVMTI 」 就是JVM Tool Interface,是 JVM 暴露出来给用户扩展使用的接口集合,JVMTI 是基于事件驱动的,JVM每执行一定的逻辑就会触发一些事件的回调接口,通过这些回调接口,用户可以自行扩展
JVMTI是实现 Debugger、Profiler、Monitor、Thread Analyser 等工具的统一基础,在主流 Java 虚拟机中都有实现
形象地说,JVMTI是Java虚拟机提供的一整套后门。通过这套后门可以对虚拟机方方面面进行监控,分析。甚至干预虚拟机的运行。
-
「 JVMTIAgent 」 是一个动态库,利用JVMTI暴露出来的一些接口来干一些我们想做、但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:
- Agent_OnLoad函数,如果agent是在启动时加载的,通过JVM参数设置
- Agent_OnAttach函数,如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用Agent_OnAttach函数
- Agent_OnUnload函数,在agent卸载时调用
-
「 instrument」 实现了Agent_OnLoad和Agent_OnAttach两方法,也就是说在使用时,agent既可以在启动时加载,也可以在运行时动态加载。其中启动时加载还可以通过类似-javaagent:jar包路径的方式来间接加载instrument agent,运行时动态加载依赖的是JVM的attach机制,通过发送load命令来加载agent
-
「 javaagent 」 依赖于instrument的JVMTIAgent(Linux下对应的动态库是libinstrument.so),还有个别名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),专门为Java语言编写的插桩服务提供支持的
-
「JVM Attach」 是指 JVM 提供的一种进程间通信的功能,能让一个进程传命令给另一个进程,并进行一些内部的操作,比如进行线程 dump,那么就需要执行 jstack 进行,然后把 pid 等参数传递给需要 dump 的线程来执行
Instrument介绍
核心方法
| 方法名 | 作用 |
|---|---|
| void addTransformer(ClassFileTransformer transformer, boolean canRetransform) | 注册ClassFileTransformer实例,注册多个会按照注册顺序进行调用 |
| void addTransformer(ClassFileTransformer transformer) | 也就是上面方法canRetransform为false时, 表示通过ClassFileTransformer实例重定义的类不能进行回滚 |
| boolean removeTransformer(ClassFileTransformer transformer) | 移除ClassFileTransformer实例 |
| boolean isRetransformClassesSupported() | 返回当前JVM配置是否支持类重新转换 |
| void retransformClasses(Class<?>... classes) | 重新转换类, 根据已经存在的字节码文件, 就行修改后再替换 |
| boolean isRedefineClassesSupported() | 返回当前JVM配置是否支持重定义类(修改类的字节码) |
| void redefineClasses(ClassDefinition... definitions) | 重新定义类, 以自己提供的字节码文件替换已存在的class文件 |
redefineClasses(重定义)
void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException, UnmodifiableClassException;
类加载过程中, 类加载器的defineClass()方法会将二进制文件读取得到class对象, redefineClasses就是可控的再次进行这一过程. 对于已经加载的类, 该方法通过使用提供的新的字节码文件进行替换, 现有的类文件字节不会被使用,就像从源头进行重新编译以进行修复和继续调试时一样。(只是修改类内存中的字节码,并不会重新做初始化动作) 参考:www.cnblogs.com/duanxz/p/49…
重定义的类的特点
- 该方法对一组class进行操作,以便同时允许多个相互依赖的类的更改,如A类的重新定义可能需要重新定义B类。
- 重新定义一个类并不会导致它的初始化器被运行。 静态变量的值将保持在调用之前。重新定义的类的实例不受影响。
- 重定义可能会更改方法体、常量池和属性。重定义不得添加、移除、重命名字段或方法;不得更改方法签名、继承关系。
- 类文件字节不会被检查,验证和安装,直到应用转换为止,如果结果字节错误,则此方法将抛出异常。如果此方法抛出异常,则不会重新定义任何类。
- 如果重定义的方法有活动的堆栈帧,那么这些活动的帧将继续运行原方法的字节码。将在新的调用上使用此重定义的方法。
- 如果此方法抛出异常,则不会重定义任何类。
1、
redefineClasses方法通过修改 JVM 内部的Class对象数据来实现类的重新定义。当你使用redefineClasses方法时,实际上是在更新 JVM 中已经加载的类的字节码表示。这种操作不涉及重新加载类,而是直接修改现有的Class对象的数据结构。
2、 Class 对象不变:虽然Class对象的数据(如方法字节码)被更新,但Class对象本身在 JVM 中的标识(如类加载器和类的唯一性)保持不变。这意味着类的静态字段、类加载器、包信息等不会因为重新定义而改变。
示例代码
以下是一个简单的示例,展示如何使用 redefineClasses 方法:
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class MyAgent {
public static void agentmain(String agentArgs, Instrumentation inst) {
try {
// 假设我们已经生成了新的字节码
byte[] newBytecode = ...;
// 创建 ClassDefinition
ClassDefinition def = new ClassDefinition(MyClass.class, newBytecode);
// 重新定义类
inst.redefineClasses(def);
} catch (ClassNotFoundException | UnmodifiableClassException e) {
e.printStackTrace();
}
}
}
在这个示例中,MyClass 是需要重新定义的类,而 newBytecode 是它的新字节码。这个过程通常需要在 JVM 启动时通过代理实现。
retransformClasses(重定义)
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
retransformClasses 是 Java Instrumentation API 中的一部分,用于在运行时重新转换已经加载的类。与 redefineClasses 不同,retransformClasses 允许对类进行重新转换,而不需要提供新的字节码。相反,它使用预先注册的 ClassFileTransformer 来修改类的字节码。以下是对 retransformClasses 的详细介绍:
-
重新转换类:
retransformClasses使得已经加载的类可以被重新转换。通过调用此方法,JVM 会重新调用每个注册的ClassFileTransformer,并将原始字节码提供给它们进行转换。
-
不需要提供新的字节码:
- 与
redefineClasses需要提供新的字节码不同,retransformClasses使用现有的ClassFileTransformer逻辑来对类进行转换。
- 与
-
支持多次转换:
- 一个类可以被多次重新转换,每次转换时都会调用所有注册的
ClassFileTransformer。
- 一个类可以被多次重新转换,每次转换时都会调用所有注册的
代码示例
1. 创建目标类
首先,创建一个简单的目标类,称为 HelloWorld:
public class HelloWorld {
public void sayHello() {
System.out.println("Hello, World!");
}
public static void main(String[] args) {
HelloWorld helloWorld = new HelloWorld();
helloWorld.sayHello();
}
}
2. 实现 ClassFileTransformer
接下来,实现一个 ClassFileTransformer,用于修改 HelloWorld 类的方法:
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class SimpleTransformer implements ClassFileTransformer {
@Override
public byte[] transform(
ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
if ("HelloWorld".equals(className.replace("/", "."))) {
System.out.println("Transforming HelloWorld class");
// 这里可以使用 ASM 或 Javassist 来修改字节码
// 为了简单起见,假设我们只是打印一条消息
// 实际应用中需要返回修改后的字节码
// 返回未修改的字节码
return classfileBuffer;
}
return null;
}
}
3. 创建 Java Agent
创建一个 Java Agent 来注册 ClassFileTransformer 并调用 retransformClasses:
import java.lang.instrument.Instrumentation;
public class TransformerAgent {
public static void premain(String agentArgs, Instrumentation inst) {
SimpleTransformer transformer = new SimpleTransformer();
inst.addTransformer(transformer, true);
try {
// Retransform the HelloWorld class
Class<?> targetClass = Class.forName("HelloWorld");
inst.retransformClasses(targetClass);
} catch (Exception e) {
e.printStackTrace();
}
}
}
4. 创建 MANIFEST.MF
创建一个 MANIFEST.MF 文件,用于指定代理类:
Premain-Class: TransformerAgent
5. 编译并打包
编译所有类,并将它们打包到一个 JAR 中:
javac HelloWorld.java SimpleTransformer.java TransformerAgent.java
jar cmf MANIFEST.MF agent.jar HelloWorld.class SimpleTransformer.class TransformerAgent.class
6. 运行程序
使用 Java Agent 运行程序:
java -javaagent:agent.jar -cp . HelloWorld
Transformer 简介
ClassFileTransformer 是 Java Instrumentation API 的一个接口,用于在类被加载到 JVM 时动态修改其字节码。通过实现这个接口,我们可以在类加载的过程中插入、修改或删除字节码,以实现各种功能,如性能监控、日志记录、代码增强等。
ClassFileTransformer 接口
ClassFileTransformer 接口只有一个方法需要实现:
byte[] transform(
ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException;
方法参数详解
-
ClassLoader loader:- 加载当前类的类加载器。如果类是由引导类加载器加载的,则为
null。
- 加载当前类的类加载器。如果类是由引导类加载器加载的,则为
-
String className:- 类的名称,使用内部格式表示(例如,
java/lang/String)。
- 类的名称,使用内部格式表示(例如,
-
Class<?> classBeingRedefined:- 如果这是一个重定义或重新转换的类,这是被重定义或重新转换的类对象;否则为
null。
- 如果这是一个重定义或重新转换的类,这是被重定义或重新转换的类对象;否则为
-
ProtectionDomain protectionDomain:- 当前类的保护域。
-
byte[] classfileBuffer:- 类的字节码,格式为 Class 文件格式。
返回值
- 返回修改后的字节码。如果返回
null,表示不对类进行修改。 - 如果修改后的字节码无效,可能会导致
ClassFormatError或其他错误。
异常
-
IllegalClassFormatException:- 如果字节码格式不正确,可以抛出这个异常。
触发时机
ClassFileTransformer 的触发时机主要与类的加载和重转换过程有关。以下是具体的触发时机:
1. 类首次加载时
- 当 JVM 首次加载某个类时,如果已经注册了
ClassFileTransformer,则会触发transform方法。 - 这是最常见的触发时机,适用于在类加载时进行字节码修改的场景。
2. 类重转换时
- 如果使用
Instrumentation的retransformClasses方法,ClassFileTransformer也会被触发。 retransformClasses允许你在类已经加载后再次修改其字节码。这种情况下,transform方法会在重转换时被调用。- 注意:在重转换过程中,不能改变类的结构(如添加或删除方法、字段),只能修改方法的实现。
3. 类重定义时
- 使用
Instrumentation的redefineClasses方法可以重定义类,这会触发transform方法。 - 与重转换不同,重定义可以改变类的结构,但需要提供完整的修改后的字节码。
触发机制的注意事项
-
类加载顺序:
ClassFileTransformer只会在类首次加载或重转换时触发,因此在类已经加载后注册的Transformer不会对已加载的类生效,除非通过retransformClasses显式重转换。 -
多次触发:如果多个
ClassFileTransformer被注册,那么每个 transformer 都会在类加载或重转换时依次触发。后续的 transformer 接收到的是前一个 transformer 修改后的字节码。 -
性能影响:频繁的字节码转换可能会影响性能,尤其是在大规模应用中,因此使用时需要谨慎。
通过了解这些触发时机,你可以更有效地使用 ClassFileTransformer 来实现动态字节码修改。
使用步骤
-
实现
ClassFileTransformer:- 创建一个类实现
ClassFileTransformer接口,并实现transform方法。
- 创建一个类实现
-
注册 Transformer:
- 使用
Instrumentation实例的addTransformer方法注册你的ClassFileTransformer。
- 使用
-
修改字节码:
- 在
transform方法中,使用字节码操作库(如 ASM、Javassist)来修改字节码。
- 在
触发transform时机
示例
以下是一个简单的 ClassFileTransformer 示例,展示如何在类被加载时打印类名:
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class SimpleTransformer implements ClassFileTransformer {
@Override
public byte[] transform(
ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("Loading class: " + className);
// 这里没有进行实际的字节码修改,直接返回原始字节码
return classfileBuffer;
}
}
retransformClasses 和 redefineClasses 区别
Instrumentation 接口提供了两种主要方法来动态修改已经加载的类:retransformClasses 和 redefineClasses。这两者的区别主要在于它们对类结构的影响和使用场景:
retransformClasses
- 功能:允许对已经加载的类进行重转换。
- 限制:不能改变类的结构。具体来说,不能添加或删除类的字段、方法,不能改变方法的签名或类的继承结构。
- 使用场景:适用于需要修改方法实现或插入代码逻辑的场景,而不需要改变类的结构。
- 触发机制:当调用
retransformClasses时,JVM 会再次调用注册的ClassFileTransformer的transform方法,以便对类进行重转换。
redefineClasses
- 功能:允许对已经加载的类进行重定义。
- 灵活性:可以改变类的结构,包括添加或删除字段、方法,改变方法的签名等。
- 使用场景:适用于需要对类结构进行更改的场景,如添加新的方法或字段。
- 实现复杂度:使用
redefineClasses需要提供完整的类字节码(包括所有方法、字段),因为它会完全替换现有的类定义。 - 触发机制:
redefineClasses不依赖于ClassFileTransformer,而是直接通过提供完整的字节码来实现类的重定义。
选择使用哪种方法
- 如果你的需求仅仅是修改方法的实现(例如插入日志、性能监控代码),而不改变类的结构,
retransformClasses是更合适的选择。 - 如果需要更改类的结构(例如添加新的方法或字段),则必须使用
redefineClasses。
在使用这两种方法时,需要注意性能影响和可能的兼容性问题,特别是在涉及到复杂的类结构或在生产环境中使用时。确保在测试环境中充分验证修改后的类行为,以避免引入潜在的错误或不兼容性。
Attach
Instrument能够在jvm运行时加载, 就是通过Attach的能力, 因此这里简单介绍一下.
Attach是什么?
Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM “附着”(Attach)代理工具程序的。
Attach的功能
简单来说attach是一种进程间通信的工具. 可以用于jvm进程间通信 ,能让一个进程传命令给另一个进程, 命令另一个进程进行一些操作.
Attach机制可以对目标进程收集很多信息,如内存dump,线程dump,类信息统计(比如加载的类及大小以及实例个数等),动态加载agent,获取系统属性等等。
典型的应用例如jstack工具:
当我们需要某一个正在运行的jvm进程的线程使用情况时, 会运行jstack进程, 然后告诉它目标进程的pid, jstack就会利用attach机制在进程间进行通信, 完成相应数据的获取.
参考、
这个很好 很全面:www.cnblogs.com/crazymakerc…
链接基本概念及demo:juejin.cn/post/708602…
链接javaAgent原理及demo说明:www.cnblogs.com/rickiyang/p…
SPI 热部署:juejin.cn/post/725806…
参考:
JVM插庄之二:Java agent基础原理
Java 动态调试技术原理及实践
# Java—JavaAgent探针
Agent 内存马的攻防之道
字节码增强宝藏