概述

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
21
private 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
2
3
4
5
6
7
8
9
10
11
12
13
public class PerMain {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println(JSON.toJSONString(className));
return classfileBuffer;
}
});
}
public static void main(String[] args) {

}
}

2. 配置jar 包的MANIFEST.MF

在META-INF目录添加MANIFEST.MF文件,添加一下内容

其中Premain-Class的值配置上面实现那两个方法之一的类。

1
2
3
4
Manifest-Version: 1.0
Premain-Class: priv.chow.test.agent.premain.PerMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true

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
2
3
4
5
6
7
8
9
10
11
12
13
public class AgentMain {

public static void agentmain(String arg, Instrumentation inst)
throws Exception {
inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println(className);
//System.out.println(this.getClass().getClassLoader());
return classfileBuffer;
}
});
}
}

2. 配置jar 包的MANIFEST.MF

与程序启动的Agent不同,在程序运行中指定的Agent必须要有Agent-Class属性

1
2
3
4
Manifest-Version: 1.0
Agent-Class: priv.chow.test.agent.AgentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true

或者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.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AgentAttach {

public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
System.out.println(VirtualMachine.list());
String pid = "13324";
VirtualMachine vm = VirtualMachine.attach(pid);
Properties props = vm.getSystemProperties();
System.out.println(props);
System.out.println(vm.getAgentProperties());

String agent = "D:/workspace/Java/TestAgent/target/TestAgent-1.0-SNAPSHOT.jar";
// load agent into target VM
vm.loadAgent(agent, "com.sun.management.jmxremote.port=5000");

// detach
vm.detach();
}
}

以下是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;