JavaAgent入门
概述
JavaAgent 是JDK 1.5 以后引入的,也可以叫做Java代理。
JavaAgent 是运行在 main方法之前的拦截器,它内定的方法名叫 premain ,也就是说先执行 premain 方法然后再执行 main 方法。
主要功能如下:
- 可以在加载class文件之前做拦截,对字节码做修改;
- 可以在运行期对已加载类的字节码做变更;
- 获取所有已经加载过的类;
- 获取所有已经初始化过的类;
- 获取某个对象的大小;
- 将某个jar加入到Bootstrap classpath中作为高优先级被BootstrapClassloader加载;
- 将某个jar加入到classpath中供AppClassloader取加载;
按照加载时机可以分为两种:
- 程序启动前
- 程序启动后
使用
程序启动前的Agent
程序启动前的Agent是指通过java启动参数-javaagent:配置的方式
java -javaagent:D:/workspace/Java/TestAgent/target/TestAgent-1.0-SNAPSHOT.jar -jar xx.jar
1. 提供一个入口类
实现以下方法中的其中一个
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
JVM 会优先加载 1 签名的方法,加载成功忽略 2,如果1 没有,加载 2 方法。这个逻辑在sun.instrument.InstrumentationImpl#loadClassAndStartAgent中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21private void loadClassAndStartAgent(String var1, String var2, String var3) throws Throwable {
ClassLoader var4 = ClassLoader.getSystemClassLoader();
Class var5 = var4.loadClass(var1);
Method var6 = null;
NoSuchMethodException var7 = null;
boolean var8 = false;
try {
var6 = var5.getDeclaredMethod(var2, String.class, Instrumentation.class);
var8 = true;
} catch (NoSuchMethodException var13) {
var7 = var13;
}
if (var6 == null) {
try {
var6 = var5.getDeclaredMethod(var2, String.class);
} catch (NoSuchMethodException var12) {
}
}
// ...
选择实现 public static void premain(String agentArgs, Instrumentation inst) 方法
Instrumentation 是操作字节码的入口,提供了一下的方法
- addTransformer(ClassFileTransformer transformer)
添加一个class转换器,ClassFileTransformer类的transform方法返回的byte[]就是虚拟机实际去加载类的字节码,通过这个方法你可以在类加载器进行动态修改. - retransformClasses
类重新加载一次,会触发addTransformer,用于处理已经加载的类 - redefineClasses
类重新加载一次,不会触发addTransformer,用于重新加载原来的类
1 | public class PerMain { |
2. 配置jar 包的MANIFEST.MF
在META-INF目录添加MANIFEST.MF文件,添加一下内容
其中Premain-Class的值配置上面实现那两个方法之一的类。
1 | Manifest-Version: 1.0 |
maven的配置方式:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.6</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>priv.chow.test.agent.premain.PerMain</mainClass>
</manifest>
<manifestEntries>
<premain-class>priv.chow.test.agent.premain.PerMain</premain-class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
3. 使用agent
比如我要运行jar包abc.jar,然后要将agent用在这个jar包中
那么在启动的时候配置
-javaagent:
[=options]
options指的是参数会传到方法的agentArgs参数中
1 | java -javaagent:D:/workspace/Java/TestAgent/target/TestAgent-1.0-SNAPSHOT.jar -jar abc.jar |
或者传参的方式1
java -javaagent:D:/workspace/Java/TestAgent/target/TestAgent-1.0-SNAPSHOT.jar=xxxx -jar abc.jar
程序运行中的Agent
运行时设置Agent依赖com.sun.tools.attach.VirtualMachine类的attach api,这是jdk1.6时提供的api。
这个api其实是JVM进程之间的的沟通桥梁,底层通过socket进行通信,JVM A可以发送一些指令给JVM B,B收到指令之后,可以执行对应的逻辑,比如在命令行中经常使用的jstack、jcmd、jps等,很多都是基于这种机制实现的。
com.sun.tools在jdk的tools.jar中需要拷贝到jre下的lib目录中。
public static List
list() 列出当前主机中的jvm进程
public static VirtualMachine attach(String var0)
连接某个jvm,var0: jvm进程的pid
public abstract Properties getSystemProperties()
获得jvm的System配置
public static VirtualMachine attach(VirtualMachineDescriptor var0)
连接某个jvm
public void loadAgent(String var1)
让jvm进程加载Agent,var1: agent路径
public void loadAgent(String var1, String var2)
让jvm进程加载Agent,var1:agent路径,var2:options 虚拟机参数
public abstract void detach()
断开连接
1. 提供一个入口类
实现以下方法其中一个
public static void agentmain(String arg)
public static void agentmain(String arg, Instrumentation inst)
1 | public class AgentMain { |
2. 配置jar 包的MANIFEST.MF
与程序启动的Agent不同,在程序运行中指定的Agent必须要有Agent-Class属性
1 | Manifest-Version: 1.0 |
或者maven1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.6</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>priv.chow.test.agent.AgentMain</mainClass>
</manifest>
<manifestEntries>
<Agent-Class>priv.chow.test.agent.AgentMain</Agent-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
3. 通过程序指定某个进程加载Agent
向pid为13324的进程加载Agent
1 | public class AgentAttach { |
以下是loadAgent的异常值,如果是0则为成功
private static final int JNI_ENOMEM = -4;
private static final int ATTACH_ERROR_BADJAR = 100;
private static final int ATTACH_ERROR_NOTONCP = 101;
private static final int ATTACH_ERROR_STARTFAIL = 102;