1.java agent简介

java agent来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。

例如当前的覆盖率工具都是使用的这种方式。

核心类如下:

agent例子 java java agent技术_jar

提供了两种方式来使用该注入接口。

  • 在程序启动的时候,通过– javaagent参数来加载agent程序,此时使用premian方法加载
  • 程序已经启动,通过启动agent程序连接到JVM,此时使用agentmain 方法加载

2.实例代码介绍

本实例通过简单的xml配置文件来配置需要注入的类和方法,来完成统计打印方法的耗时,其中使用javassist实现class类的二进制更改。

public class MyAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        process("premain-" + agentArgs, inst);
    }

    public static void agentmain(String agentArgs, Instrumentation inst) {
        process("agentmain-" + agentArgs, inst);
    }

    private static void process(String agentArgs, Instrumentation inst) {
        System.out.println(agentArgs);

        TransformMethodProvider provider = new TransformMethodProvider();
        Map<String, List<String>> result = provider.getTransformMethod(
                "D:\\Program Files\\eclipse\\workspace\\com.huawei.ozl.java.gent\\src\\main\\resources\\conf\\test.xml");
        inst.addTransformer(new MonitorTransformer(result), true);
    }
}
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

        // 先判断下现在加载的class的包路径是不是需要监控的类,通过instrumentation进来的class路径用‘/’分割
        if (needMonitorMaps.containsKey(className)) {
            List<String> needMonitorList = needMonitorMaps.get(className);
            // 将‘/’替换为‘.’m比如monitor/agent/Mytest替换为monitor.agent.Mytest
            className = className.replace("/", ".");
            CtClass ctclass = null;
            try {
                // 用于取得字节码类,必须在当前的classpath中,使用全称 ,这部分是关于javassist的知识
                ClassPool parent = new ClassPool();
                parent.insertClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));

                ctclass = parent.get(className);
                // 循环一下,看看哪些方法需要加时间监测
                for (CtMethod ctmethod : ctclass.getMethods()) {
                    String methodName = ctmethod.getName();
                    if (canMatch(needMonitorList, methodName)) {
                        String nname = methodName + "$impl";
                        CtMethod mnew = CtNewMethod.copy(ctmethod, nname, ctclass, null);
                        ctclass.addMethod(mnew);
                        String type = ctmethod.getReturnType().getName();
                        StringBuffer body = new StringBuffer();
                        body.append("{\nlong start = System.currentTimeMillis();\n");
                        if (!"void".equals(type)) {
                            body.append(type + " result = ");
                        }
                        body.append(nname + "($$);\n");
                        body.append("System.out.println(\"Call to method " + methodName
                                + " took \" +\n (System.currentTimeMillis()-start) + " + "\" ms.\");\n");
                        if (!"void".equals(type)) {
                            body.append("return result;\n");
                        }
                        body.append("}");
                        ctmethod.setBody(body.toString());
                    }
                }
                return ctclass.toBytecode();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (CannotCompileException e) {
                e.printStackTrace();
            } catch (NotFoundException e) {
                e.printStackTrace();
            }
        }
        return classfileBuffer;
    }

3.使用javaagent参数启动程序

格式如下:-javaagent:<jarpath>[=<options>]

如果使用脚本启动tomcat,那么使用,在启动脚本$tomcat_home/bin/setenv.sh中修改如下:
CATALINA_OPTS="$CATALINA_OPTS -javaagent:/path/to/YourJar.jar"
如果使用eclipse中启动tomcat,那么在启动参数中添加如下:
-javaagent:D:\temp\javaAgent/com.huawei.ozl.java.gent-1.0.0.jar=destfile=/tmp/aaa

agent例子 java java agent技术_jar_02

1.agent程序的classloader

当使用agent方式启动的时候在premian方法出,其classLoader为appclassloader,其中classloader的classpath就是我们的agent程序所需要的classpath。当我们打断点的时候可以看出classpath如下:

agent例子 java java agent技术_java_03

而继续跑到main方法以后的classLoader信息如下:

agent例子 java java agent技术_java_04

4.使用agent连接jvm

开发者可以在 main 函数开始执行以后,再启动自己的 Instrumentation 程序。跟 premain 函数一样, 开发者可以编写一个含有“agentmain”函数的 Java 类:

  • public static void agentmain (String agentArgs, Instrumentation inst); [1]
  • public static void agentmain (String agentArgs); [2]

同样,[1] 的优先级比 [2] 高,将会被优先执行。
跟 premain 函数一样,开发者可以在 agentmain 中进行对类的各种操作。其中的 agentArgs 和 Inst 的用法跟 premain 相同。
与“Premain-Class”类似,开发者必须在 manifest 文件里面设置“Agent-Class”来指定包含 agentmain 函数的类。

public static void main(String[] args) throws Exception, IOException {
        String targetVmPid = args[0];
        String jmxAgent = "D:\\Program Files\\eclipse\\workspace\\com.huawei.ozl.java.gent\\target\\com.huawei.ozl.java.gent-1.0.0.jar";

        new Thread(new AttachThread(jmxAgent, targetVmPid)).start();

    }

    static class AttachThread implements Runnable {

        private final String targetVmPid;

        private final String jar;

        public AttachThread(String attachJar, String targetVmPid) {
            this.targetVmPid = targetVmPid; // 记录程序启动时的 VM 集合
            jar = attachJar;
        }

        public void run() {
            try {
                // Attach到被监控的JVM进程上
                VirtualMachine virtualmachine = VirtualMachine.attach(targetVmPid);

                // 让JVM加载jmx Agent
                virtualmachine.loadAgent(jar, "destfile=/tmp/aaa");

                // Detach
                Thread.sleep(60 * 1000);
                virtualmachine.detach();
            } catch (Exception e) {
                // ignore
            }
        }
    }

运行会发现报UnsupportedOperationException异常

Caused by: java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method
at sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)
at sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:144)
at com.huawei.ozl.java.gent.MyAgent.agentmain(MyAgent.java:31)
... 6 more</br>
因为JVM不支持新增方法,retransformClasses方法部分注释如下:
The retransformation may change method bodies, the constant pool and attributes. The retransformation must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance. These restrictions maybe be lifted in future versions</br>

因此为了验证该能力,我们对agentmain的ClassFileTransformer重新修改

private void insertTimeCode2(CtClass ctclass, CtMethod ctmethod) throws CannotCompileException, NotFoundException {
        ctmethod.addLocalVariable("startMs", CtClass.longType);
        ctmethod.insertBefore("startMs = System.currentTimeMillis();");
        ctmethod.insertAfter("{final long endMs = System.currentTimeMillis();"
                \+ "System.out.println(\"Executed in ms: \" + (endMs-startMs));}");
    }

对于注入同时可以使用asm。参考示例中的AsmClassFileTransformer

5.Btrace介绍

BTrace可以动态地跟踪java运行程序,将跟踪字节码注入到运行类中,对运行代码侵入较小,对性能上的影响可以忽略不计。

1.VisualVM Btrace使用

使用jdk自带的VisualVM安装BTrace Workbench插件

选择工具菜单-->菜单选项---->在可用插件中选择BTrace Workbench安装即可。

agent例子 java java agent技术_agent例子 java_05


如果需要作用到某个jvm中,需要选择即可:

agent例子 java java agent技术_java_06

输入如下的脚本:

/* BTrace Script Template */
import java.util.Date;

import com.sun.btrace.BTraceUtils;
import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.*;

@BTrace
public class Btrace {

  @OnMethod(
      clazz = "com.example.client.resource.BookResource",
      method = "getBooks",
      location = @Location(Kind.RETURN)
  )
  public static void sayHello(@Duration long duration) {//单位是纳秒,要转为毫秒
      println("调用堆栈");
      println("duration:" + (duration / 1000000 )+ " ms");
  }
}

然后选择start按钮,即可看到输出日志。另外当我们成功调用函数的时候,业务在输出台打印日志:

agent例子 java java agent技术_jar_07


具体BTrace语法可以参考官方网站

2.命令行方式使用BTrace

  • 下载最新版(1.3,JDK7以上):https://github.com/jbachorik/btrace/releases/tag/v1.3
  • 命令格式:btrace [-p <port>] [-cp <classpath>] <pid> <btrace-script>
    下载好BTrace以后在bin目录下执行命令,btrace-script可以为java源码或者编译后的class文件(建议使用class文件,java有事会校验报错,使用btrace-client.jar编译即可)
    查看btracec.bat,其启动main方法为:com.sun.btrace.client.Main,那么我们就从该方法开始分析

3. BTrace原理分析

agent例子 java java agent技术_开发工具_08

com.sun.btrace.client.Client.compile方法实现:

public byte[] compile(String fileName, String classPath,
            PrintWriter err, String includePath) {
        byte[] code = null;
        File file = new File(fileName);
        if (fileName.endsWith(".java")) {
            Compiler compiler = new Compiler(includePath);
            classPath += File.pathSeparator + System.getProperty("java.class.path");
            if (debug) {
                debugPrint("compiling " + fileName);
            }
            Map<String, byte[]> classes = compiler.compile(file,
                    err, ".", classPath);
            ...
            code = classes.get(name);
           ...
        } else if (fileName.endsWith(".class")) {
            code = new byte[(int) file.length()];
            try {
                FileInputStream fis = new FileInputStream(file);
              ...
                try {
                    fis.read(code);
                } finally {
                    fis.close();
                }
              ...
            } catch (IOException exp) {
               ...
            }
        } else {
         ...
        }
        return code;
    }

com.sun.btrace.compiler.Compiler.compile

private Map<String, byte[]> compile(MemoryJavaFileManager manager,
            Iterable<? extends JavaFileObject> compUnits,
            Writer err, String sourcePath, String classPath) {
        // to collect errors, warnings etc.
        DiagnosticCollector<JavaFileObject> diagnostics =
                new DiagnosticCollector<JavaFileObject>();

        // javac options
        List<String> options = new ArrayList<String>();
        options.add("-Xlint:all");
        options.add("-g:lines");
        options.add("-deprecation");
        options.add("-source");
        options.add("1.6");
        options.add("-target");
        options.add("1.6");
        if (sourcePath != null) {
            options.add("-sourcepath");
            options.add(sourcePath);
        }

        if (classPath != null) {
            options.add("-classpath");
            options.add(classPath);
        }

        // create a compilation task
        JavacTask task =
                (JavacTask) compiler.getTask(err, manager, diagnostics,
                options, null, compUnits);  //JDK javax.tools.JavaCompiler
        Verifier btraceVerifier = new Verifier();
        task.setTaskListener(btraceVerifier);

        // we add BTrace Verifier as a (JSR 269) Processor
        List<Processor> processors = new ArrayList<Processor>(1);
        processors.add(btraceVerifier);
        task.setProcessors(processors);
                ...
    }

com.sun.btrace.client.Client.attach

{
String agentPath = "/btrace-agent.jar";
            String tmp = Client.class.getClassLoader().getResource("com/sun/btrace").toString();
            tmp = tmp.substring(0, tmp.indexOf("!"));
            tmp = tmp.substring("jar:".length(), tmp.lastIndexOf("/"));
            agentPath = tmp + agentPath;
            agentPath = new File(new URI(agentPath)).getAbsolutePath();
            attach(pid, agentPath, null, null);
}
 public void attach(String pid, String agentPath, String sysCp, String bootCp) throws IOException {
        try {
            VirtualMachine vm = null;
            vm = VirtualMachine.attach(pid);
            Properties serverVmProps = vm.getSystemProperties();
            int serverPort = Integer.parseInt(serverVmProps.getProperty("btrace.port", "-1"));
            if (serverPort != -1) {
                if (serverPort != port) {
                    throw new IOException("Can not attach to PID " + pid + " on port " + port + ". There is already a BTrace server active on port " + serverPort + "!");
                }
            } else {
                if (!isPortAvailable(port)) {
                    throw new IOException("Port " + port + " unavailable.");
                }
            }
           ...//构造参数
            vm.loadAgent(agentPath, agentArgs);
        } catch (RuntimeException re) {
            throw re;
        } catch (IOException ioexp) {
            throw ioexp;
        } catch (Exception exp) {
            throw new IOException(exp.getMessage());
        }
    }

com.sun.btrace.agent.Main.startServer

//-- Internals only below this point
    private static void startServer() {
        int port = BTRACE_DEFAULT_PORT;
        String p = argMap.get("port");
        if (p != null) {
            try {
                port = Integer.parseInt(p);
            } catch (NumberFormatException exp) {
                error("invalid port assuming default..");
            }
        }
        ServerSocket ss;
        try {
            if (isDebug()) debugPrint("starting server at " + port);
            System.setProperty("btrace.port", String.valueOf(port));
            if (scriptOutputFile != null && scriptOutputFile.length() > 0) {
                System.setProperty("btrace.output", scriptOutputFile);
            }
            ss = new ServerSocket(port);
        } catch (IOException ioexp) {
            ioexp.printStackTrace();
            return;
        }

        while (true) {
            try {
                if (isDebug()) debugPrint("waiting for clients");
                Socket sock = ss.accept();
                if (isDebug()) debugPrint("client accepted " + sock);
                Client client = new RemoteClient(inst, sock);
                registerExitHook(client);         //服务端注册shutdownHook,通知客户端关闭
                handleNewClient(client);
            } catch (RuntimeException re) {
                if (isDebug()) debugPrint(re);
            } catch (IOException ioexp) {
                if (isDebug()) debugPrint(ioexp);
            }
        }
    }

com.sun.btrace.client.Main.registerExitHook(Client)

private static void registerExitHook(final Client client) {
        if (isDebug()) debugPrint("registering shutdown hook");
        Runtime.getRuntime().addShutdownHook(new Thread(
            new Runnable() {
                public void run() {
                    if (! exiting) {
                        try {
                            if (isDebug()) debugPrint("sending exit command");
                            client.sendExit(0);          //发送退出命令,服务端接收后退出while循环
                        } catch (IOException ioexp) {
                            if (isDebug()) debugPrint(ioexp.toString());
                        }
                    }
                }
            }));
    }

com.sun.btrace.client.Client.submit(String, byte[], String[], CommandListener)

public void submit(String fileName, byte[] code, String[] args,
            CommandListener listener) throws IOException {
        if (sock != null) {
            throw new IllegalStateException();
        }
        submitDTrace(fileName, code, args, listener);
        try {
            if (debug) {
                debugPrint("opening socket to " + port);
            }
            long timeout = System.currentTimeMillis() + 5000;
            while (sock == null && System.currentTimeMillis() <= timeout) {
                try {
                    sock = new Socket("localhost", port);
                } catch (ConnectException e) {
                    if (debug) {
                        debugPrint("server not yet available; retrying ...");
                    }
                    Thread.sleep(20);
                }
            }
            oos = new ObjectOutputStream(sock.getOutputStream());
            if (debug) {
                debugPrint("sending instrument command");
            }
            WireIO.write(oos, new InstrumentCommand(code, args));
            ois = new ObjectInputStream(sock.getInputStream());
            if (debug) {
                debugPrint("entering into command loop");
            }
            commandLoop(listener);           //while(true)监听服务端发过来的请求
        } catch (UnknownHostException uhe) {
            throw new IOException(uhe);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

处理注入代码:com.sun.btrace.agent.Main.handleNewClient(Client)

private static void handleNewClient(final Client client) {
    serializedExecutor.submit(new Runnable() {

        public void run() {
            try {
                if (isDebug()) debugPrint("new Client created " + client);
                if (client.shouldAddTransformer()) {
                    client.registerTransformer();
                    Class[] classes = inst.getAllLoadedClasses();                  //关键API
                    ArrayList<Class> list = new ArrayList<Class>();
                    if (isDebug()) debugPrint("filtering loaded classes");
                    for (Class c : classes) {
                        if (inst.isModifiableClass(c) &&
                            client.isCandidate(c)) {
                            if (isDebug()) debugPrint("candidate " + c + " added");
                            list.add(c);
                        }
                    }
                    list.trimToSize();
                    int size = list.size();
                    if (isDebug()) debugPrint("added as ClassFileTransformer");
                    if (size > 0) {
                        classes = new Class[size];
                        list.toArray(classes);
                        client.startRetransformClasses(size);
                        if (isDebug()) {
                            for(Class c : classes) {
                                try {
                                    inst.retransformClasses(c);
                                } catch (VerifyError e) {
                                    debugPrint("verification error: " + c.getName());
                                }
                            }
                        } else {
                            inst.retransformClasses(classes);              //关键API
                        }
                        client.skipRetransforms();
                    }
                }
                client.getRuntime().send(new OkayCommand());
            } catch (UnmodifiableClassException uce) {
                if (isDebug()) {
                    debugPrint(uce);
                }
                client.getRuntime().send(new ErrorCommand(uce));
            }
        }
    });

}

执行注入逻辑:com.sun.btrace.agent.Client.instrument(Class, String, byte[])

private byte[] instrument(Class clazz, String cname, byte[] target) {
    byte[] instrumentedCode;
    try {
        ClassWriter writer = InstrumentUtils.newClassWriter(target);
        Cla***eader reader = new Cla***eader(target);
        Instrumentor i = new Instrumentor(clazz, className,  btraceCode, onMethods, writer);
        InstrumentUtils.accept(reader, i);
        if (Main.isDebug() && !i.hasMatch()) {
            Main.debugPrint("*WARNING* No method was matched for class " + cname); // NOI18N
        }
        instrumentedCode = writer.toByteArray();
    } catch (Throwable th) {
        Main.debugPrint(th);
        return null;
    }
    Main.dumpClass(className, cname, instrumentedCode);
    return instrumentedCode;
}

我们的脚本中经常会使用到com.sun.btrace.BTraceUtils.print方法,其内部会使用BTraceRuntime的send方法,

public void send(Command cmd) {
    try {
        boolean speculated = specQueueManager.send(cmd);
        if (! speculated) {
            queue.put(cmd);         使用阻塞队列
        }
    } catch (InterruptedException ie) {
        ie.printStackTrace();
    }
}

在BTraceRuntime新建的时候会创建yield独立的线程消费队列

this.cmdThread = new Thread(new Runnable() {
        public void run() {
            try {
                BTraceRuntime.enter();
                while (true) {
                    Command cmd = queue.take();            \\消费队列,并把信息发送到客户端
                    cmdListener.onCommand(cmd);
                    if (cmd.getType() == Command.EXIT) {
                        return;
                    }
                }
            } catch (InterruptedException ignored) {
            } catch (IOException ignored) {
            } finally {
                runtimes.remove(className);
                queue.clear();
                specQueueManager.clear();
                BTraceRuntime.leave();
                disabled = true;
            }
        }
    });
    cmdThread.setDaemon(true);
    cmdThread.start();

END!