初探 Java agent

作者:微信小助手

发布时间:2020-02-20T20:23:59


引言



在本篇文章中,我会通过几个简单的程序来说明 agent 的使用,最后在实战环节我会通过 asm 字节码框架来实现一个小工具,用于在程序运行中采集指定方法的参数和返回值。 有关 asm 字节码的内容不是本文的重点,不会过多的阐述,不明白的同学可以自己 google 下。


简介



Java agent 提供了一种在加载字节码时,对字节码进行修改的方式。 他共有两种方式执行,一种是在 main 方法执行之前,通过 premain 来实现,另一种是在程序运行中,通过 attach api 来实现。

在介绍 agent 之前,先给大家简单说下 Instrumentation 。 它是 JDK1.5 提供的 API ,用于拦截类加载事件,并对字节码进行修改,它的主要方法如下:
  
public interface Instrumentation {    //注册一个转换器,类加载事件会被注册的转换器所拦截     void addTransformer(ClassFileTransformer transformer, boolean canRetransform);    //重新触发类加载     void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;    //直接替换类的定义     void redefineClasses(ClassDefinition... definitions) throws  ClassNotFoundException, UnmodifiableClassException;}


premain



premain 是在 main 方法之前运行的方法,也是最常见的 agent 方式。 运行时需要将 agent 程序打包成 jar 包,并在启动时添加命令来执行,如下文所示:
  
java -javaagent:agent.jar=xunche HelloWorld

premain 共提供以下 2 种重载方法, Jvm 启动时会先尝试使用第一种方法,若没有会使用第二种方法:
  
public static void premain(String agentArgs, Instrumentation inst);public static void premain(String agentArgs);

一个简单的例子


下面我们通过一个程序来简单说明下 premain 的使用,首先我们准备下测试代码,测试代码比较简单,运行 main 方法并输出 hello world 。
  
package org.xunche.app;public class HelloWorld {    public static void main(String[] args) {        System.out.println("Hello World");    }}
接下来我们看下 agent 的代码,运行 premain 方法并输出我们传入的参数。
  
package org.xunche.agent;public class HelloAgent {  public static void premain(String args) {    System.out.println("Hello Agent:  " + args);  }}

为了能够 agent 能够运行,我们需要将 META-INF/MANIFEST.MF 文件中的 Premain- Class 为我们编写的 agent 路径,然后通过以下方式将其打包成 jar 包,当然你也可以使用 idea 直接导出 jar 包。
  
echo 'Premain-Class: org.xunche.agent.HelloAgent' > manifest.mfjavac org/xunche/agent/HelloAgent.javajavac org/xunche/app/HelloWorld.javajar cvmf manifest.mf hello-agent.jar org/

接下来,我们编译下并运行下测试代码,这里为了测试简单,我将编译后的 class 和 agent 的 jar 包放在了同级目录下
  
java -javaagent:hello-agent.jar=xunche org/xunche/app/HelloWorld
可以看到输出结果如下,agent中的premain方法有限于main方法执行

  
Hello Agent: xuncheHello World


稍微复杂点的例子


通过上面的例子,是否对 agent 有个简单的了解呢?

下面我们来看个稍微复杂点,我们通过 agent 来实现一个方法监控的功能。 思路大致是这样的,若是非 jdk 的方法,我们通过 asm 在方法的执行入口和执行出口处,植入几行记录时间戳的代码,当方法结束后,通过时间戳来获取方法的耗时。

首先还是看下测试代码,逻辑很简单, main 方法执行时调用 sayHi 方法,输出 hi ,  xunche ,并随机睡眠一段时间。
  
package org.xunche.app;public class HelloXunChe {    public static void main(String[] args) throws InterruptedException {        HelloXunChe helloXunChe = new HelloXunChe();        helloXunChe.sayHi();    }    public void sayHi() throws InterruptedException {        System.out.println("hi, xunche");        sleep();    }    public void sleep() throws InterruptedException {        Thread.sleep((long) (Math.random() * 200));    }}

接下来我们借助 asm 来植入我们自己的代码,在 jvm 加载类的时候,为类的每个方法加上统计方法调用耗时的代码,代码如下,这里的 asm 我使用了 jdk 自带的,当然你也可以使用官方的 asm 类库。
  
package org.xunche.agent;import jdk.internal.org.objectweb.asm.*;import jdk.internal.org.objectweb.asm.commons.AdviceAdapter;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.Instrumentation;import java.security.ProtectionDomain;public class TimeAgent {    public static void premain(String args, Instrumentation instrumentation) {        instrumentation.addTransformer(new TimeClassFileTransformer());    }    private static class TimeClassFileTransformer implements ClassFileTransformer {        @Override        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {            if (className.startsWith("java") || className.startsWith("jdk") || className.startsWith("javax") || className.startsWith("sun") || className.startsWith("com/sun")|| className.startsWith("org/xunche/agent")) {                //return null或者执行异常会执行原来的字节码                return null;            }            System.out.println("loaded class: " + className);            ClassReader reader = new ClassReader(classfileBuffer);            ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);            reader.accept(new TimeClassVisitor(writer), ClassReader.EXPAND_FRAMES);            return writer.toByteArray();        }    }    public static class TimeClassVisitor extends ClassVisitor {        public TimeClassVisitor(ClassVisitor classVisitor) {            super(Opcodes.ASM5, classVisitor);        }        @Override        public MethodVisitor visitMethod(int methodAccess, String methodName, String methodDesc, String signature, String[] exceptions) {            MethodVisitor methodVisitor = cv.visitMethod(methodAccess, methodName, methodDesc, signature, exceptions);            return new TimeAdviceAdapter(Opcodes.ASM5, methodVisitor, methodAccess, methodName, methodDesc);        }    }    public static class TimeAdviceAdapter extends AdviceAdapter {        private String methodName;        protected TimeAdviceAdapter(int api, MethodVisitor methodVisitor, int methodAccess, String methodName, String methodDesc) {            super(api, methodVisitor, methodAccess, methodName, methodDesc);            this.methodName = methodName;        }        @Override        protected void onMethodEnter() {            //在方法入口处植入            if ("<init>".equals(methodName)|| "<clinit>".equals(methodName)) {                return;            }            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");            mv.visitInsn(DUP);            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);            mv.visitVarInsn(ALOAD, 0);            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);            mv.visitLdcInsn(".");            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);            mv.visitLdcInsn(methodName);            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);            mv.visitMethodInsn(INVOKESTATIC, "org/xunche/agent/TimeHolder", "start", "(Ljava/lang/String;)V", false);        }        @Override        protected void onMethodExit(int i) {            //在方法出口植入            if ("<init>".equals(methodName) || "<clinit>".equals(methodName)) {                return;            }            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");            mv.visitInsn(DUP);            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);            mv.visitVarInsn(ALOAD, 0);            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);            mv.visitLdcInsn(".");            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);            mv.visitLdcInsn(methodName);            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);            mv.visitVarInsn(ASTORE, 1);            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");            mv.visitInsn(DUP);            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);            mv.visitVarInsn(ALOAD, 1);            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);            mv.visitLdcInsn(": ");            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);            mv.visitVarInsn(ALOAD, 1);            mv.visitMethodInsn(INVOKESTATIC, "org/xunche/agent/TimeHolder", "cost", "(Ljava/lang/String;)J", false);            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);        }    }}
上述的代码略长, asm 的部分可以略过。 我们通过 instrumentation.addTransformer 注册一个转换器,转换器重写了 transform 方法,方法入参中的 classfileBuffer 表示的是原始的字节码,方法返回值表示的是真正要进行加载的字节码。

onMethodEnter 方法中的代码含义是调用 TimeHolder 的 start 方法并传入当前的方法名。

onMethodExit 方法中的代码含义是调用 TimeHolder 的 cost 方法并传入当前的方法名,并打印 cost 方法的返回值。