常见的Java Agent有哪些?

  • Post category:Java

常见的 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 + "!");
  }
}
  1. 实现一个 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”。

  1. 编译并运行 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