常见的 Java Agent 是指一些可以被 Java Virtual Machine 动态加载的程序,它可以通过 Instrumentation API 拦截 Java 类的加载,修改字节码,或者对 JVM 的某些事件进行响应并采集数据等。下面将讲述 Java Agent 的使用攻略。
准备工作
在使用 Java Agent 之前,需要安装 JDK,并配置好环境变量。接着下载及安装 Java Agent 工具包,比如使用比较广泛的工具包 ByteBuddy:
$ curl -L -O https://repo1.maven.org/maven2/net/bytebuddy/byte-buddy-agent/1.10.22/byte-buddy-agent-1.10.22.jar
修改字节码
Java Agent 最常见的用法是修改字节码。下面通过修改 com.example.HelloWorld
类中的 greet
方法为例进行说明:
public class HelloWorld {
public void greet(String name) {
System.out.println("Hello " + name + "!");
}
}
- 实现一个 Java Agent
public class GreetingAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("GreetingAgent is started");
new AgentBuilder.Default().type(ElementMatchers.named("com.example.HelloWorld"))
.transform((builder, type, classLoader, module) -> builder.visit(Advice
.to(GreetingInterceptor.class)
.on(ElementMatchers.named("greet"))))
.installOn(inst);
}
public static class GreetingInterceptor {
@Advice.OnMethodEnter
public static void enter(@Advice.Argument(0) String name) {
System.out.println("Before greet " + name);
}
}
}
上述代码中,我们实现的 GreetingAgent
是一个代理程序,它持有 com.example.HelloWorld
类的实例,当它需要在该类的某个方法被调用时,会调用其中的一个拦截器 GreetingInterceptor
,以此修改方法中的行为。在这个例子中,我们在 greet
方法中实现了一个拦截器,当 greet
方法被调用时,我们会输出额外的内容,即输出 “Before greet”。
- 编译并运行 Java Agent
将上述代码编译成 GreetingAgent.jar
文件,然后将它作为 Java Agent,通过 JVM 参数来加载,在指定的类路径上启动 com.example.HelloWorld
类。
$ javac GreetingAgent.java
$ java -javaagent:./GreetingAgent.jar -cp . com.example.HelloWorld Alice
运行结果如下:
GreetingAgent is started
Before greet Alice
Hello Alice!
监控 JVM 事件
通过 Java Agent,还可以监控 JVM 中的事件,然后产生相应的操作。比如监控方法执行时间、锁的使用情况等。
public class TimingAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("TimingAgent is started");
Class<?>[] classes = inst.getAllLoadedClasses();
for (Class<?> clazz : classes) {
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (Modifier.isStatic(method.getModifiers()) || Modifier.isNative(method.getModifiers())) {
continue;
}
try {
inst.addTransformer(new TimingTransformer(method.getName()), true);
inst.retransformClasses(clazz);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static class TimingTransformer implements ClassFileTransformer {
private String methodName;
public TimingTransformer(String methodName) {
this.methodName = methodName;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className.equals("com/example/HelloWorld")) {
ClassReader reader = new ClassReader(classfileBuffer);
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
TimingClassVisitor visitor = new TimingClassVisitor(writer, methodName);
reader.accept(visitor, 0);
return writer.toByteArray();
}
return null;
}
}
public static class TimingClassVisitor extends ClassVisitor {
private String methodName;
public TimingClassVisitor(ClassWriter classWriter, String methodName) {
super(Opcodes.ASM7, classWriter);
this.methodName = methodName;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if (!name.equals(methodName)) {
return mv;
}
return new TimingMethodVisitor(mv, access, name, descriptor);
}
}
public static class TimingMethodVisitor extends AdviceAdapter {
private String methodName;
private long startTime = 0;
protected TimingMethodVisitor(MethodVisitor mv, int access, String name, String descriptor) {
super(Opcodes.ASM7, mv, access, name, descriptor);
this.methodName = name;
}
@Override
protected void onMethodEnter() {
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Starting method: " + methodName);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitFieldInsn(Opcodes.PUTSTATIC, "com/example/HelloWorld", "startTime", "J");
}
@Override
protected void onMethodExit(int opcode) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitFieldInsn(Opcodes.GETSTATIC, "com/example/HelloWorld", "startTime", "J");
mv.visitInsn(Opcodes.LSUB);
mv.visitVarInsn(Opcodes.LSTORE, 1);
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Execution time of " + methodName + ": ");
mv.visitVarInsn(Opcodes.LLOAD, 1);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;J)V", false);
}
}
}
上述代码中,我们实现的 TimingAgent
是一个代理程序,它通过监控 HelloWorld
类的所有方法实现了方法的执行时间计算。在这个例子中,我们通过添加字节码的方式在 HelloWorld
类中注入了对该类中所有方法的监控机制。当方法开始调用时,我们记录下开始时间,方法执行完成后,我们再记录下结束时间,两个时间的差即为方法的执行时间。
$ javac TimingAgent.java
$ java -javaagent:./TimingAgent.jar -cp . com.example.HelloWorld Alice
运行结果如下:
Starting method: greet
Hello Alice!
Execution time of greet: 42000