Java Agent 介绍
Java Agent 是一种允许开发者在 JVM 运行时通过修改类的字节码来动态增强 Java 应用程序的工具。它基于 Instrumentation
接口,可以使用 ClassFileTransformer
来拦截和修改字节码。在 Java Agent 中,Instrumentation.addTransformer()
可以用来添加一个字节码转换器,它将在类加载时对字节码进行操作。
我们平时接触到的很多地方都用到了这个 Java Agent :
- 各个 Java IDE 的调试功能,例如 eclipse、IntelliJ IDEA;
- 热部署功能,例如 JRebel、XRebel、 spring-loaded;
- 各种线上诊断工具,例如 Btrace、Greys,还有阿里的 Arthas;
- 各种性能分析工具,例如 Visual VM、JConsole 等;
Java Agent 最终以 jar 包的形式存在,我们也只能以调用 jar 包的方式去调用它。
Java Agent 快速入门
接下来就来实现一个简单的 Java Agent,基于 Java 1.8,主要实现两点简单的功能:
1、打印当前加载的所有类的名称;
2、监控一个特定的方法,在方法中动态插入简单的代码并获取方法返回值;
在方法中插入代码用到了字节码修改技术,字节码修改技术主要有 javassist、ASM。这个例子中用的是 javassist,所以需要引入相关的 依赖:
1 | <dependency> |
编写入口类和自定义转换器
入口类需要实现 premain 和 agentmain 两个方法。这两个方法的运行时机不一样。这要从 Java Agent 的使用方式来说了,Java Agent 有两种启动方式,一种是以 JVM 启动参数 -javaagent:xxx.jar 的形式随着 JVM 一起启动,这种情况下,会调用 premain 方法,并且是在主进程的 main 方法之前执行。另外一种是以 loadAgent 方法动态 attach 到目标 JVM 上,这种情况下,会执行 agentmain 方法。
代码实现如下:
1 | package com.miaoji; |
可以看到 premain 和 agentmain 两个方法都有参数 agentArgs 和 inst,其中 agentArgs 是我们启动 Java Agent 时带进来的参数,比如 -javaagent:xxx.jar [agentArgs]
。而参数 Instrumentation inst 是 Java 开放出来的专门用于字节码修改和程序监控的实现。我们要实现的打印已加载类和修改字节码也就是基于它来实现的。其中 inst.getAllLoadedClasses()一个方法就实现了获取所有已加载类的功能。
这里 inst.addTransformer() 方法是用来添加字节码转换器的,其中传入了一个自定义的转换器 MyTransformer 对象。
MyTransformer 类的定义如下:
1 | package com.miaoji; |
以上代码的逻辑就是当碰到加载的类是 Person 的时候,在其中的 test 方法开始时插入一条打印语句,打印内容是”动态插入的打印语句”,在 test 方法结尾处,打印返回值,其中 $_ 就是返回值,这是 javassist 里特定的标示符。
编写 MANIFEST.MF 配置文件
在目录 resources/META-INF/ 下创建文件名为 MANIFEST.MF 的文件,在其中加入如下的配置内容:
1 | Manifest-Version: 1.0 |
设置打包方式
Java Agent 是以 jar 包的形式存在,所以最后一步就是将上面的内容打到一个 jar 包里。
在 pom 文件中加入以下配置:
1 | <build> |
用 manifestFile 标签指定 MANIFEST.MF 所在路径,指定打包方式为包含依赖的 jar 包:jar-with-dependencies 。
然后运行如下命令即可打包:
1 | mvn assembly:assembly |
打包成功后会在 target 目录下生成对应的 jar 包。
编写测试类
我们先编写一个测试类,这个测试类的逻辑就是循环不断地读取键盘输入,并在输入数字 1 的时候,调用 person.test() 方法:
1 | import java.util.Scanner; |
以及定义一个 Person 类:
1 | public class Person { |
命令行方式运行
1 | java -javaagent:"C:\Users\miaoj\Documents\Java安全代码实验\JavaAgentTest\target\JavaAgentTest-1.0-SNAPSHOT-jar-with-dependencies.jar" -cp target/classes com.miaoji.RunJvm |
输出结果:
可以看到在最开始首先执行了 premain 方法打印了 “premain” ,中间输出了很多被加载的类,在 RunJvm main 方法的前后执行了自定义转换器 MyTransformer 中的 javassist 操作。
动态 attach 方式运行
这是另一种运行方式,在项目运行过程中运行 Java Agent ,这会触发 agentmain 方法。
用下面的代码去实现:
1 | package com.miaoji; |
其中用到的 com.sun.tools.attach.VirtualMachine 类需要 tools.jar 依赖,IDEA 默认不会导入,我们需要手动导入:
导入完成后,就可以开始了。
先运行 RunJvm 主函数,接着运行 AttachAgent ,回到 RunJvm 运行结果下面输入 1 ,便可以触发相同的效果了:
Java Agent 基本使用流程总结
- 创建 Java Agent 类:
- 定义一个类,包含
premain
方法(静态加载)和/或agentmain
方法(动态加载)。 - 这些方法是 Java Agent 的入口,用来接收传递的参数和
Instrumentation
对象。
- 定义一个类,包含
- 实现字节码转换:
- 使用
Instrumentation
接口注册字节码转换器,通过ClassFileTransformer
实现类加载时修改字节码的逻辑。
- 使用
- 配置
MANIFEST.MF
文件:- 在 JAR 包的
META-INF/
目录下的MANIFEST.MF
文件中,添加Premain-Class
(静态加载)或Agent-Class
(动态加载)条目,指明 Java Agent 的入口类。
- 在 JAR 包的
- 打包 Java Agent:
- 将 Java Agent 类及其依赖打包为 JAR 文件,确保
MANIFEST.MF
文件配置正确。
- 将 Java Agent 类及其依赖打包为 JAR 文件,确保
- 加载 Java Agent:
- 静态加载:在 JVM 启动时,通过
-javaagent
参数指定 Java Agent 的 JAR 文件。 - 动态加载:使用 Attach API,附加到已经运行的 JVM 进程,动态加载 Java Agent。
- 静态加载:在 JVM 启动时,通过
Java Agent 知识汇总
常用类
Instrumentation
java.lang.instrument.Instrumentation 是 Java Agent 的核心接口,允许 Java Agent 操作类定义,修改字节码等。它提供了操作类和监控 JVM 的各种方法。它是 premain 和 agentmain 方法的参数。
- 常用方法:
void addTransformer(ClassFileTransformer transformer)
:添加一个类文件转换器,在类加载时进行字节码修改。void redefineClasses(ClassDefinition... definitions)
:允许重新定义(redefine)已经加载的类,直接用新的字节码替换现有类定义,而不会触发类的重新加载。不能改变原类的签名、父类、接口,且字段、方法的结构也必须保持一致,但可以修改方法体的具体实现。void retransformClasses(Class<?>... classes)
:允许对类进行重新转换,但它会触发类加载器重新加载。同样不能改变类的结构(例如类的签名、字段、接口、父类等),但可以修改方法体的具体实现。Class[] getAllLoadedClasses()
:获取 JVM 中加载的所有类。long getObjectSize(Object object)
:获取某个对象的大小。boolean isModifiableClass(Class<?> theClass)
:检查一个类是否可以修改。
ClassFileTransformer
java.lang.instrument.ClassFileTransformer 是用于在类加载时转换字节码的接口。通过实现这个接口,Agent 可以在类加载时对字节码进行修改。前面的自定义转换器就继承了这个类并重写了 transform 方法。
- 常用方法:
byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)
:这个方法在类加载时调用,允许你对类的字节码进行修改,返回修改后的字节码。
VirtualMachine
com.sun.tools.attach.VirtualMachine 是 Java Attach API 的核心类,允许 Java Agent 动态附加到正在运行的 JVM 上。
- 常用方法:
static VirtualMachine attach(String pid)
:根据 JVM 进程 ID,附加到运行中的 JVM。void loadAgent(String agentJar)
:加载 Java Agent JAR 文件到已附加的 JVM。void detach()
:从目标 JVM 分离。
ClassDefinition
java.lang.instrument.ClassDefinition 类用于定义要重新加载的类。
- 常用方法:
ClassDefinition(Class<?> theClass, byte[] theClassFile)
:构造一个类定义,指定要重新加载的类和它的字节码。
常用方法
premain 方法
这是 Java Agent 在 JVM 启动时的入口方法,类似于
main
方法。方法签名:
1
public static void premain(String agentArgs, Instrumentation inst);
agentArgs
:传递给 Agent 的参数,可以是命令行参数等。inst
:Instrumentation
对象,提供修改类字节码、获取类信息等功能。
示例:
1
2
3public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new MyTransformer());
}
agentmain 方法
用于在 JVM 运行时动态加载 Agent,使用 Attach API 时调用。
方法签名:
1
public static void agentmain(String agentArgs, Instrumentation inst);
- 类似
premain
,agentmain
方法在 JVM 运行时动态调用,允许你动态加载 Agent。
- 类似
transform 方法
这是
ClassFileTransformer
接口中的方法,用于修改类字节码。方法签名:
1
2public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException;classfileBuffer
:类的字节码,可以通过修改这个字节数组来改变类的行为。
执行流程
Java Agent 的执行流程简要为:
- 启动方式:
- 静态:JVM 启动时,调用
premain(String agentArgs, Instrumentation inst)
。 - 动态:JVM 运行时,调用
agentmain(String agentArgs, Instrumentation inst)
。
- 静态:JVM 启动时,调用
- 注册类文件转换器:
- 在
premain
或agentmain
方法中,使用Instrumentation#addTransformer
注册ClassFileTransformer
。
- 在
- 类文件转换器执行:
- 当类加载时,
ClassFileTransformer
的transform
方法被调用,修改类的字节码。
- 当类加载时,
已加载类的操作:
- 使用retransformClasses
或redefineClasses
修改已加载类的字节码(遵循类的结构限制)。