Java Agent简介
JavaAgent 是 JDK1.5 之后引入的新特性,此特性为用户提供了在 JVM 将字节码文件读入内存之后,JVM 使用对应的字节流在 Java 堆中生成一个 Class 对象之前,用户可以对其字节码进行修改的能力,从而 JVM 也将会使用用户修改过之后的字节码进行 Class 对象的创建。Java Agent可以去实现字节码插桩、动态跟踪分析等。
Java Agent运行模式
共两种运行模式:
- 启动Java程序时添加
-javaagent(Instrumentation API实现方式)
或-agentpath/-agentlib(JVMTI的实现方式)
参数; - 在1.6版本新增了attach(附加方式)方式,可以对运行中的
Java进程
插入Agent
;
方式一中要在启动前去指定需要加载的Agent文件,而方式二可以在Java程序运行后根据进程ID进行动态注入Agent到JVM里面去。
Java Agent概念
Javaagent是java命令的一个参数。参数javaagent可以用于指定一个jar包,并且对该jar包有2个要求:
- 这个jar包的MANIFEST.MF文件必须指定Premain-Class项。
- Premain-Class指定的那个类必须实现premain()方法。
premain 方法,从字面上理解,就是运行在 main 函数之前的的类。当Java虚拟机启动时,在执行 main 函数之前,JVM会先运行-javaagent所指定jar包内Premain-Class这个类的premain方法 。普通的Java类是以main方法作为程序入口点,而Java Agent则将premain
(Agent模式)和agentmain
(Attach模式)作为了Agent程序的入口。
在命令行输入java可以看到相应的参数,其中有和java agent相关的:
1 | -agentlib:<libname>[=<选项>] 加载本机代理库 <libname>, 例如 -agentlib:hprof |
在上面-javaagent
参数中提到了参阅java.lang.instrument
,这是在rt.jar
中定义的一个包,该路径下有两个重要的类:
该包提供了一些工具帮助开发人员在 Java 程序运行时,动态修改系统中的 Class 类型。其中,使用该软件包的一个关键组件就是 Javaagent。从名字上看,似乎是个 Java 代理之类的,而实际上,他的功能更像是一个Class 类型的转换器,他可以在运行时重新接受外部请求,对Class类型进行修改。
从本质上讲,Java Agent 是一个遵循一组严格约定的常规 Java 类。 上面说到 javaagent命令要求指定的类中必须要有premain()方法,并且对premain方法的签名也有要求,签名必须满足以下两种格式:
1 | public static void premain(String agentArgs, Instrumentation inst) |
JVM 会优先加载 带Instrumentation
签名的方法,加载成功忽略第二种,如果第一种没有,则加载第二种方法。
Instrumentation接口
接口中的方法描述大致如下:
1 | public interface Instrumentation { |
ClassFileTransformer接口
java.lang.instrument.ClassFileTransformer
是一个转换类文件的代理接口,我们可以在获取到Instrumentation
对象后通过addTransformer
方法添加自定义类文件转换器。当有新的类被JVM
加载时,JVM
会自动回调用我们自定义的Transformer
类的transform
方法,传入该类的transform
信息(类名、类加载器、类字节码
等),我们可以根据传入的类信息决定是否需要修改类字节码,修改完字节码后我们将新的类字节码返回给JVM
,JVM
会验证类和相应的修改是否合法,如果符合类加载要求JVM
会加载我们修改后的类字节码。
查看一下该接口
只有一个transform
方法
1 | ClassLoader loader 定义要转换的类加载器;如果是引导加载器,则为 null |
重写**transform
**方法注意事项:
ClassLoader
如果是被Bootstrap ClassLoader(引导类加载器)
所加载那么loader
参数的值是空。- 修改类字节码时需要特别注意插入的代码在对应的
ClassLoader
中可以正确的获取到,否则会报ClassNotFoundException
,比如修改java.io.FileInputStream(该类由Bootstrap ClassLoader加载)
时插入了我们检测代码,那么我们将必须保证FileInputStream
能够获取到我们的检测代码类。 JVM
类名的书写方式路径方式:java/lang/String
而不是我们常用的类名方式:java.lang.String
。- 类字节必须符合
JVM
校验要求,如果无法验证类字节码会导致JVM
崩溃或者VerifyError(类验证错误)
。 - 如果修改的是
retransform
类(修改已被JVM
加载的类),修改后的类字节码不得新增方法
、修改方法参数
、类成员变量
。 addTransformer
时如果没有传入retransform
参数(默认是false
)就算MANIFEST.MF
中配置了Can-Redefine-Classes: true
而且手动调用了retransformClasses
方法也一样无法retransform
。- 卸载
transform
时需要使用创建时的Instrumentation
实例。
Java Agent实现
使用javaagent
需要几个步骤:
- 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。
- 创建一个Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己确定。
- 将 premain 的类和 MANIFEST.MF 文件打成 jar 包。
- 使用参数 -javaagent: jar包路径 启动要代理的方法。
实现javaagent你需要搭建两个工程,一个工程是用来承载javaagent类,单独的打成jar包;一个工程是javaagent需要去代理的类。
JVM启动前运行
工程目录结构如下:
1 | ├── java |
创建一个包含premain方法的类:
1 | package org.agent; |
上面就是我实现的一个类,实现了带Instrumentation参数的premain()方法。调用addTransformer()方法对启动时所有的类进行拦截。
然后在 resources 目录下新建目录:META-INF,在该目录下新建文件:MANIFREST.MF:
1 | Manifest-Version: 1.0 |
MANIFREST.MF文件的作用
Premain-Class :包含 premain 方法的类(类的全路径名)
Agent-Class :包含 agentmain 方法的类(类的全路径名)
Boot-Class-Path :设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。(可选)
Can-Redefine-Classes :true表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes :true 表示能重转换此代理所需的类,默认值为 false (可选)
Can-Set-Native-Method-Prefix: true表示能设置此代理所需的本机方法前缀,默认值为 false(可选)
即在该文件中主要定义了程序运行相关的配置信息,程序运行前会先检测该文件中的配置项。
也可以用maven去配置
1 | <build> |
编译成jar包后,再建立一个项目,配置加入-javaagent参数,-javaagent:out\Agent1-1.0-SNAPSHOT.jar
编写一个main方法
1 | public class AgentTest { |
运行结果,这里可以看到打印了JVM加载的所有类。
上面的输出结果我们能够发现:
- 执行main方法之前会加载所有的类,包括系统类和自定义类;
- 在ClassFileTransformer中会去拦截系统类和自己实现的类对象;
- 如果你有对某些类对象进行改写,那么在拦截的时候抓住该类使用字节码编译工具即可实现。
transform测试
1 | import javassist.*; |
JVM启动后运行
上面介绍的Instrumentation是在JDK1.5中提供的,开发者只能在main加载之前添加手脚,在Java SE 6的Instrumentation当中,提供了一个新的代理操作方法:agentmain,可以在main函数开始运行之后再运行。
跟premain函数一样,开发者可以编写一个含有agentmain函数的Java类:
1 | // 采用attach机制,被代理的目标程序VM有可能很早之前已经启动,当然其所有类已经被加载完成,这个时候需要借助Instrumentation#retransformClasses(Class<?>... classes)让对应的类可以重新转换,从而激活重新转换的类执行ClassFileTransformer列表中的回调 |
同样,agentmain方法中带Instrumentation参数的方法也比不带优先级更高。开发者必须在manifest文件里面设置“Agent-Class”来指定包含agentmain函数的类。
在Java JDK6以后实现启动后加载Instrument
的是Attach api
。存在于com.sun.tools.attach
里面有两个重要的类。
来查看一下该包中的内容,这里有两个比较重要的类,分别是VirtualMachine
和VirtualMachineDescriptor
VirtualMachine
VirtualMachine
可以来实现获取系统信息,内存dump、现成dump、类信息统计(例如JVM加载的类)。里面配备有几个方法LoadAgent,Attach 和 Detach 。下面来看看这几个方法的作用
Attach :从 JVM 上面解除一个代理等方法,可以实现的功能可以说非常之强大 。该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上
loadAgent:向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理。
Detach:从 JVM 上面解除一个代理(agent)
VirtualMachineDescriptor
则是一个描述虚拟机的容器类,配合VirtualMachine
类完成各种功能。
attach实现动态注入的原理
通过VirtualMachine类的attach(pid)方法,便可以attach到一个运行中的java进程上,之后便可以通过loadAgent(agentJarPath)来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法。
既然是两个进程之间通信那肯定的建立起连接,VirtualMachine.attach动作类似TCP创建连接的三次握手,目的就是搭建attach通信的连接。而后面执行的操作,例如vm.loadAgent,其实就是向这个socket写入数据流,接收方target VM会针对不同的传入数据来做不同的处理。
agentmain测试
工程结构和 上面premain的测试一样,编写AgentMainTest,然后使用maven插件打包 生成MANIFEST.MF。
1 | package org.agent; |
打包生成jar包,编写测试main方法。的步骤是:从一个attach JVM去探测目标JVM,如果目标JVM存在则向它发送agent.jar。我测试写的简单了些,找到当前JVM并加载agent.jar。
1 | import com.sun.tools.attach.VirtualMachine; |
list()方法会去寻找当前系统中所有运行着的JVM进程,通过vmd.displayName()看到当前系统都有哪些JVM进程在运行。因为main函数执行起来的时候进程名为当前类名,所以通过这种方式可以去找到当前的进程id。
执行结果
Instrumentation的局限性
大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,或者笼统说就是类重定义(Class Redefine)的功能,但是有以下的局限性:
- premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
- 类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses()方法,此方法有以下限制: