Java agent是java命令的一个参数。参数javaagent可以用于指定一个jar包。
当Java虚拟机启动时,在执行main函数之前,JVM会先运行-javaagent所指定jar包内Premain-Class这个类的premain方法。
使用java agent 需要几个步骤:
premain()方法包括两个参数:
public static void premain(String agentArgs, Instrumentation inst); //【1】
public static void premain(String agentArgs); //【2】
其中,【1】和【2】同时存在时,【1】会优先被执行,而【2】则会被忽略。
具体使用如下代码:
import java.lang.instrument.Instrumentation;
public class MyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("this is an agent.");
System.out.println("args:" + agentArgs + "\n");
}
}
首先,需要在代码的resources目录下添加META-INF/MANIFEST.MF文件。其目的是指定Premain-Class的类。
Manifest-Version: 1.0
Premain-Class: agent.MyAgent
Can-Redefine-Classes: true
pom.xml文件中添加打包插件和打包的相关配置,指定Premain-Class:
maven-assembly-plugin
false
jar-with-dependencies
true
PreMainAgent
PreMainAgent
true
true
make-assembly
package
single
注释:maven-assembly-plugin是maven的一个组装插件
在PreMainAgent中要指定实现premain()方法的类
jar-with-dependencies依赖打包,jar包会包含所有依赖
true自动生成MANIFEST.MF文件
执行mvn clean package,就能生成一个my-agent.jar
使用如下设置运行Agent
-javaagent: 文件位置 [=参数]
备注:如果是运行多个agent,进行罗列即可。-javaagent: 文件位置 [=参数] -javaagent: 文件位置 [=参数],其中的参数就是premain函数中的agentArgs。
①首先,新建一个测试类
public class AgentTest {
public static void main(String[] args) {
System.out.println("this is main");
}
}
②命令行运行:java -javaagent: 文件位置 [=参数]
idea运行:如果是Idea中,在VM options中配置。
-javaagent: 文件位置 [=参数]
this is an agent.
args:first
this is main
Byte Buddy是开源的、基于Apache2.0许可证的库,它致力于解決字节码操作和instrumentation API的复杂性,ByteBuddy所声称的目标是将显式的字节码操作隐藏在一个类型安全的领域特定语言背后,通过使用ByteBuddy,任何熟java编程语言的人都有望非常容易地进行字节码操作,ByteBuddy提供了额外的API来生成javaagent,可以轻松的増强我们已有的代码
例如之前我们要对方法的调用时间来进行统计,是利用spring的AOP技术来进行统计的,但是由于探针并没有集成spring类似的框架,所以不能利用这个技术来实现,这就需要对字节码进行增强,增强后,通过拦截器进行拦截,从而达到统计方法时间的目的。
引入ByteBuddy的依赖
net.bytebuddy
byte-buddy
1.9.2
net.bytebuddy
byte-buddy-agent
1.9.2
在premain处进行处理,通过AgentBuilder方法,生成一个Agent。这里有两点需要特别说明:其一是在AgentBuilder.type处,这里可以指定需要拦截的类;其二是在builder.method处,这里可以指定需要拦截的方法。当然其API支持各种isStatic、isPublic等等一系列方式。
public class MyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("this is an perform monitor agent.");
AgentBuilder.Transformer transformer = new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder> transform(DynamicType.Builder> builder,
TypeDescription typeDescription,
ClassLoader classLoader) {
return builder
.method(ElementMatchers.any()) // 拦截任意方法
.intercept(MethodDelegation.to(TimeInterceptor.class)); // 委托
}
};
new AgentBuilder
.Default()
.type(ElementMatchers.nameStartsWith("com.example.demo")) // 指定需要拦截的类
.transform(transformer)
.with(listener)
.installOn(inst);
}
}
上一步实现Transformer的过程中,委托了一个TimeInterceptor.class。下面是其实现方式,整个的try语句是原有的代码执行,我们在之前打了时间戳,并在其结束后,计算并打印了其调用耗时。
public class TimeInterceptor {
@RuntimeType
public static Object intercept(@Origin Method method,
@SuperCall Callable> callable) throws Exception {
long start = System.currentTimeMillis();
try {
// 原有函数执行
return callable.call();
} finally {
System.out.println(method + ": took " + (System.currentTimeMillis() - start) + "ms");
}
}
}
这里需要注意的是,我们定义的包路径要和Agent中定义的相同,否则Agent无法Hook到这个类及其方法。
package com.example.demo;
public class AgentTest {
private void fun1() throws Exception {
System.out.println("this is fun 1.");
Thread.sleep(500);
}
private void fun2() throws Exception {
System.out.println("this is fun 2.");
Thread.sleep(500);
}
public static void main(String[] args) throws Exception {
AgentTest test = new AgentTest();
test.fun1();
test.fun2();
}
}
可以看到,我们的Agent成功Hook并增强了其调用方法。
this is an perform monitor agent.
this is fun 1.
private void com.example.demo.AgentTest.fun1() throws java.lang.Exception: took 501ms
this is fun 2.
private void com.example.demo.AgentTest.fun2() throws java.lang.Exception: took 500ms
public static void com.example.demo.AgentTest.main(java.lang.String[]) throws java.lang.Exception: took 1001ms
Agent的构造比较简单,我们只需要在premain方法中,加入一个线程池。其功能是每隔5秒输出一次内存信息和GC信息。
public class MyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("this is an perform monitor agent.");
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(new Runnable() {
public void run() {
Metric.printMemoryInfo();
Metric.printGCInfo();
}
}, 0, 5000, TimeUnit.MILLISECONDS);
}
}
public class Metric {
private static final long MB = 1048576L;
public static void printMemoryInfo() {
MemoryMXBean memory = ManagementFactory.getMemoryMXBean();
MemoryUsage headMemory = memory.getHeapMemoryUsage();
//head堆、初始(M)、最大(上限)(M)、当前(已使用)(M)、提交的内存(已申请)(M)、使用率
String info = String.format("\ninit: %s\t max: %s\t used: %s\t committed: %s\t use rate: %s\n",
headMemory.getInit() / MB + "MB",
headMemory.getMax() / MB + "MB", headMemory.getUsed() / MB + "MB",
headMemory.getCommitted() / MB + "MB",
headMemory.getUsed() * 100 / headMemory.getCommitted() + "%"
);
System.out.print(info);
MemoryUsage nonheadMemory = memory.getNonHeapMemoryUsage();
non-head非堆、初始(M)、最大(上限)(M)、当前(已使用)(M)、提交的内存(已申请)(M)、使用率
info = String.format("init: %s\t max: %s\t used: %s\t committed: %s\t use rate: %s\n",
nonheadMemory.getInit() / MB + "MB",
nonheadMemory.getMax() / MB + "MB", nonheadMemory.getUsed() / MB + "MB",
nonheadMemory.getCommitted() / MB + "MB",
nonheadMemory.getUsed() * 100 / nonheadMemory.getCommitted() + "%"
);
System.out.println(info);
}
public static void printGCInfo() {
//垃圾收集器、名称、次数、时间、内存区名称
List garbages = ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean garbage : garbages) {
String info = String.format("name: %s\t count:%s\t took:%s\t pool name:%s",
garbage.getName(),
garbage.getCollectionCount(),
garbage.getCollectionTime(),
Arrays.deepToString(garbage.getMemoryPoolNames()));
System.out.println(info);
}
}
}
public class AgentTest {
public static void main(String[] args) throws Exception {
boolean is = true;
while (is) {
List
init: 256MB max: 3641MB used: 2MB committed: 245MB use rate: 1%
init: 2MB max: 0MB used: 5MB committed: 7MB use rate: 72%
name: PS Scavenge count:0 took:0 pool name:[PS Eden Space, PS Survivor Space]
name: PS MarkSweep count:0 took:0 pool name:[PS Eden Space, PS Survivor Space, PS Old Gen]
init: 256MB max: 3641MB used: 8MB committed: 245MB use rate: 3%
init: 2MB max: 0MB used: 5MB committed: 7MB use rate: 77%
name: PS Scavenge count:0 took:0 pool name:[PS Eden Space, PS Survivor Space]
name: PS MarkSweep count:0 took:0 pool name:[PS Eden Space, PS Survivor Space, PS Old Gen]