JavaAgent 是一个 Java 运行时工具,它可以在 JVM 启动时,以代理的方式加载到 JVM 中。它可以通过在类加载过程中对字节码进行修改,来实现对应用程序行为的动态监测和改变。

JavaAgent能干什么

JavaAgent 可以实现以下功能:

监测应用程序的性能和行为,例如统计方法执行次数、记录方法调用栈、计算方法执行时间等。

修改应用程序的字节码,例如在方法调用前后加入日志、修改方法参数等。

实现应用程序的自动化测试和调试,例如自动执行测试用例、记录测试结果、生成代码覆盖率报告等。

改变应用程序的行为,例如动态注入依赖、修改配置信息、修改方法返回值等。

实现应用程序的安全监测和防护,例如检测 SQL 注入、XSS 攻击等。

JavaAgent 可以使用 Java 的 Instrumentation API 来实现字节码修改和类加载器的转换等功能,同时也可以使用开源的字节码工具,例如 ASM、Javassist 等来实现字节码修改。

一个最简单的例子

一个最简单的 JavaAgent 的例子是实现对一个特定的方法进行计数,可以通过 JavaAgent 在该方法执行前和执行后打印日志,以便统计方法执行的次数。

首先,需要定义一个 Java 类,实现 Java Agent 的 premain 方法。premain 方法是 JavaAgent 必须实现的方法,它会在 JVM 启动时被调用。

import java.lang.instrument.Instrumentation;

public class SimpleJavaAgent {

  public static void premain(String agentArgs, Instrumentation inst) {
    inst.addTransformer(new SimpleTransformer());
  }

}

然后,需要定义一个实现了 ClassFileTransformer 接口的类,来修改目标方法的字节码。在本例中,我们使用了 ASM 工具来修改字节码。

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class SimpleTransformer implements ClassFileTransformer {

  @Override
  public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
      ProtectionDomain protectionDomain, byte[] classfileBuffer)
      throws IllegalClassFormatException {
    if (className.equals("com/example/MyClass")) { // 目标类名
      ClassReader reader = new ClassReader(classfileBuffer);
      ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
      ClassVisitor visitor = new ClassVisitor(Opcodes.ASM5, writer) {
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature,
            String[] exceptions) {
          MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
          if (name.equals("myMethod")) { // 目标方法名
            return new MethodVisitor(Opcodes.ASM5, mv) {
              @Override
              public void visitCode() {
                super.visitCode();
                mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("Entering method myMethod");
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
              }
              @Override
              public void visitInsn(int opcode) {
                if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
                  mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                  mv.visitLdcInsn("Exiting method myMethod");
                  mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                }
                super.visitInsn(opcode);
              }
            };
          }
          return mv;
        }
      };
      reader.accept(visitor, ClassReader.EXPAND_FRAMES);
      return writer.toByteArray();
    }
    return classfileBuffer;
  }

}

在这个示例中,我们使用了 ASM 来修改目标类的字节码。在 SimpleTransformer 类的 transform 方法中,我们先判断目标类名是否为 com/example/MyClass,如果是,则使用 ClassReader 读取类的字节码,并通过 ClassWriter 创建一个新的类,然后创建一个 ClassVisitor 对象来访问该类的方法.

要运行上述的 JavaAgent 例子,需要执行以下步骤:

1 编写上述两个类的源代码,并将它们编译成字节码文件。

2 将编译好的字节码文件打成一个 jar 包,并在 MANIFEST.MF 文件中添加如下内容:

Premain-Class: SimpleJavaAgent

这里的 SimpleJavaAgent 是实现了 premain 方法的类的名称。

3 在启动应用程序时,加上 -javaagent 参数,指定刚才打好的 jar 包路径。例如:

java -javaagent:./simple-java-agent.jar -jar myapp.jar

这样,JavaAgent 就会在 JVM 启动时被加载,并在应用程序运行时监测指定的方法,记录方法的执行次数。在方法执行前后打印日志,以便统计方法执行的次数。

请注意,这个例子是一个非常简单的示例,实际上 JavaAgent 可以实现的功能远远不止于此,具体实现方式也可能因为使用的工具不同而有所不同。

文章目录