浅谈Java Agent
在JDK5以后,官方提供了一种新特性 Java Agent,也叫Java 探针技术,它可以帮助我们构建一个和主程序独立的代理程序,通过代理程序可以在不修改主程序代码的情况下对主程序进行增强,如实现性能监测、日志记录、热加载等,很多著名的软件/工具都使用了这个技术,如arthas、skywalking等
下面通过示例来学习如何使用Java Agent
关于Java Agent的一些简要说明
- 通过
-javaagent:agent.jar
参数加载agent程序,可以多次使用以加载多个agent - Java Agent有两个方法,一个是
premain(String [, Instrumentation])
,一个是agentmain(String[, Instrumentation])
,Instrumentation
参数可选 premain
方法会在主程序JVM启动前执行,因此可以在此方法中进行一些初始化工作;agentmain
方法会在主程序JVM启动后执行,但是不能使用-javaagent
参数来加载,而是要使用VirtualMachine
的loadAgent
方法来进行加载Instrumentation
接口提供了在程序运行期间对程序进行动态调整的能力,比如修改字节码、替换class
开始动手
前提:
创建两个maven项目,一个是主程序,一个是agent
主程序
Main.java
1
2
3
4
5public class Main {
public static void main(String[] args) {
System.out.println("hello");
}
}pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>com.codecho.demo.Main</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
agent程序
LogAgent.java
1
2
3
4
5
6
7
8
9public class LogAgent {
// 主程序启动前,代理先执行,如果代理抛出异常,主程序无法正常启动
// 参数 Instrumentation 可选
public static void premain(String args, Instrumentation instrumentation) {
System.out.println("LogAgent#premain executed, args: " + args);
}
}pom.xml(这里的配置是为了生成MANIFEST.MF文件,里面有premain class的信息)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>com.codecho.agent.LogAgent</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
测试premain
使用
maven package
得到主程序和agent的jar包通过
-javaagent
参数加载agent(这里我把两个jar包放到同一目录了)1
2
3
4
5# 不指定参数
java -javaagent:log-agent-1.0-SNAPSHOT.jar -jar agent-demo-1.0-SNAPSHOT.jar
# 指定参数
java -javaagent:log-agent-1.0-SNAPSHOT.jar=param1,param2,param3 -jar agent-demo-1.0-SNAPSHOT.jar
测试agentmain
修改agent程序的
LogAgent.java
和pom.xml
LogAgent.java
添加
agentmain
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class LogAgent {
// 主程序启动前,代理先执行,如果代理抛出异常,主程序无法正常启动
// 参数 Instrumentation 可选
public static void premain(String args, Instrumentation instrumentation) {
System.out.println("LogAgent#premain executed, args: " + args);
}
// 主程序启动后,代理后执行
// 参数 Instrumentation 可选
public static void agentmain(String args, Instrumentation instrumentation) {
System.out.println("LogAgent#agentmain executed, args: " + args);
}
}pom.xml
将
Premain-Class
改为Agent-Class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Agent-Class>com.codecho.agent.LogAgent</Agent-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
修改主程序的
Main.java
和pom.xml
,新增Agent.java
Main.java
因为
agentmain
方法是在主程序JVM启动后执行,因此这里使用输入流保证main方法不会马上结束1
2
3
4
5
6
7public class Main {
public static void main(String[] args) throws IOException {
System.in.read();
}
}pom.xml
1
2
3
4
5
6
7
8
9
10<!--引入jdk的tools-->
<dependencies>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${JAVA_HOME}\lib\tools.jar</systemPath>
</dependency>
</dependencies>Attach.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Attach {
public static void main(String[] args) {
VirtualMachine vm = null;
try {
// 这里的24816是Main主程序的进程号,运行Main.java后使用jps命令查看pid
vm = VirtualMachine.attach("24816");
vm.loadAgent("D:/IdeaWorkspace/log-agent/target/log-agent-1.0-SNAPSHOT.jar", "hello");
} catch (AttachNotSupportedException | IOException | AgentLoadException | AgentInitializationException e) {
throw new RuntimeException(e);
}
}
}
先运行Main主程序,通过
jps
查看主程序pid,再运行Attach程序
测试Instrumentation#addTransformer方法(作用在premain方法)
修改agent程序的
LogAgent.java
和pom.xml
LogAgent.java
修改
premain
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31public class LogAgent {
public static void premain(String args, Instrumentation instrumentation) {
System.out.println("LogAgent#premain executed, args: " + args);
instrumentation.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
// 当前类为com/codecho/demo/TrafficService时,才替换class
if (!className.equals("com/codecho/demo/TrafficService")) {
return classfileBuffer;
}
System.out.println("replace class: " + className);
ByteArrayOutputStream bos;
// 类加载前替换新的class
try (FileInputStream fis = new FileInputStream("D:/IdeaWorkspace/agent-demo/target/classes/com/codecho/demo/TrafficService.class")) {
bos = new ByteArrayOutputStream();
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
bos.write(buffer, 0, buffer.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
return bos.toByteArray();
}
});
}
}pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>com.codecho.agent.LogAgent</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
修改主程序的
Main.java
,新增TrafficService.java
TrafficService
1
2
3
4
5
6
7public class TrafficService {
public String transport() {
return "take the train";
}
}Main.java
1
2
3
4
5
6
7
8public class Main {
public static void main(String[] args) {
String transport = new TrafficService().transport();
System.out.println("transport: " + transport);
}
}
单独运行
Main.java
,查看输出1
2PS D:\IdeaWorkspace\agent-demo\target> java -jar agent-demo-1.0-SNAPSHOT.jar
transport: take the train修改
TrafficService.java
并编译1
2
3
4
5
6
7public class TrafficService {
public String transport() {
return "take the plane";
}
}通过
-javaagent
参数加载agent,查看输出
测试Instrumentation#retransformClasses方法(作用在agentmain方法)
修改agent程序
LogAgent.java
和pom.xml
LogAgent.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40public class LogAgent {
// 主程序启动前,代理先执行,如果代理抛出异常,主程序无法正常启动
// 参数 Instrumentation 可选
public static void premain(String args, Instrumentation instrumentation) {
System.out.println("LogAgent#premain executed, args: " + args);
}
// 主程序启动后,代理后执行
// 参数 Instrumentation 可选
public static void agentmain(String args, Instrumentation instrumentation) throws ClassNotFoundException, UnmodifiableClassException {
System.out.println("LogAgent#agentmain executed, args: " + args);
instrumentation.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
// 当前类为com/codecho/demo/TrafficService时,才替换class
if (!className.equals("com/codecho/demo/TrafficService")) {
return classfileBuffer;
}
ByteArrayOutputStream bos;
// 类加载前替换新的class
try (FileInputStream fis = new FileInputStream("D:/IdeaWorkspace/agent-demo/target/classes/com/codecho/demo/TrafficService.class")) {
bos = new ByteArrayOutputStream();
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
bos.write(buffer, 0, buffer.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
return bos.toByteArray();
}
}, true);
instrumentation.retransformClasses(Class.forName("com.codecho.demo.TrafficService", false, ClassLoader.getSystemClassLoader()));
}
}pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Agent-Class>com.codecho.agent.LogAgent</Agent-Class>
<!--不加此配置会导致load失败-->
<!--Agent JAR loaded but agent failed to initialize-->
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
修改主程序的
Main.java
Main.java
通过死循环判断class是否重新加载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class Main {
public static void main(String[] args) {
for (;;) {
String transport = new TrafficService().transport();
System.out.println("transport: " + transport);
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}TrafficService.java
1
2
3
4
5
6
7public class TrafficService {
public String transport() {
return "take the bus";
}
}
先运行Main主程序,再修改
TrafficService
的transport
方法,通过jps
查看主程序pid,再运行Attach程序可以看到
TrafficService#transport
的输出从bus变为subway,表明TrafficService
类确实重新加载了
总结
- Java Agent是JDK提供的一种技术,可以对Java程序进行增强、监测、分析等
- Java Agent主要有
premain
和agentmain
两个入口方法,Instrumentation
接口可以让我们在程序运行期间动态地更改字节码、替换class等 - 可以通过
Javassist
或ASM
等工具来方便地操作字节码