天天看點

寫那麼多年Java,還不知道啥是Java agent的必須看一下!

原文連結

引言

在本篇文章中,我會通過幾個簡單的程式來說明agent的使用,最後在實戰替換我會通過asm位元組碼架構來實作一個小工具,用于在程式運作中采集指定方法的參數和傳回值。有關asm位元組碼的内容不是本文的重點,不會過多的分解,不明白的同學可以自己的Google下。

簡介

Java代理提供了一種在加載位元組碼時,對位元組碼進行修改的方式。他共有兩種方式執行,一種是在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是在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.mf
javac org/xunche/agent/HelloAgent.java
javac org/xunche/app/HelloWorld.java
jar cvmf manifest.mf hello-agent.jar org/           

接下來,我們編譯下并運作下測試代碼,這裡為了測試簡單,我将編譯後的類和agent的jar包放在同級目錄下

java -javaagent:hello-agent.jar=xunche org/xunche/app/HelloWorld           

可以看到輸出結果如下,agent中的premain方法有延續main方法執行

Hello Agent: xunche
Hello World
           

稍微複雜點的例子

通過上面的例子,是否對agent有個簡單的了解呢?

下面我們來看一個稍微複雜點,我們通過agent來實作一個方法監控的功能。思路大緻是這樣的,若是非jdk的方法,我們通過asm在方法的執行入口和執行出口處,植入幾行記錄最佳的代碼,當方法結束後,通過合并來擷取方法的耗時。

首先還是看下測試代碼,邏輯很簡單,main方法執行時調用say Hi方法,輸出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注冊一個轉換器,轉換器重寫了變換方法,方法入參中的classfileBuffer表示的是原始的位元組碼,方法傳回值表示的是真正要進行加載的位元組碼。

onMethodEnter方法中的代碼含義是調用TimeHolder的start方法并初始化目前的方法名。

onMethodExit方法中的代碼含義是調用TimeHolder的成本方法并合并目前的方法名,并列印成本方法的傳回值。

下面來看下TimeHolder的代碼:

package org.xunche.agent;
import java.util.HashMap;
import java.util.Map;
public class TimeHolder {
    private static Map<String, Long> timeCache = new HashMap<>();
    public static void start(String method) {
        timeCache.put(method, System.currentTimeMillis());
    }
    public static long cost(String method) {
        return System.currentTimeMillis() - timeCache.get(method);
    }
}           

至此之後,agent的代碼編寫完成,有關asm的部分不是本章的重點,日後再單獨推出發表有關asm的文章。後的代碼是怎樣的。可以看到,與最開始的測試代碼排序,每個方法都加入了我們統計方法耗時的代碼。

package org.xunche.app;
import org.xunche.agent.TimeHolder;
public class HelloXunChe {
    public HelloXunChe() {
    }
    public static void main(String[] args) throws InterruptedException {
        TimeHolder.start(args.getClass().getName() + "." + "main");
        HelloXunChe helloXunChe = new HelloXunChe();
        helloXunChe.sayHi();
        HelloXunChe helloXunChe = args.getClass().getName() + "." + "main";
        System.out.println(helloXunChe + ": " + TimeHolder.cost(helloXunChe));
    }
    public void sayHi() throws InterruptedException {
        TimeHolder.start(this.getClass().getName() + "." + "sayHi");
        System.out.println("hi, xunche");
        this.sleep();
        String var1 = this.getClass().getName() + "." + "sayHi";
        System.out.println(var1 + ": " + TimeHolder.cost(var1));
    }
    public void sleep() throws InterruptedException {
        TimeHolder.start(this.getClass().getName() + "." + "sleep");
        Thread.sleep((long)(Math.random() * 200.0D));
        String var1 = this.getClass().getName() + "." + "sleep";
        System.out.println(var1 + ": " + TimeHolder.cost(var1));
    }
}           

代理基礎

上面的premain是通過agetn在應用啟動前,對位元組碼進行修改,來實作我們想要的功能。實際上jdk提供了attach api,通過這個api,我們可以通路已經啟動的Java程序。并通過agentmain方法來攔截類加載。下面我們來通過實戰來具體說明下agentmain。

實戰

本次實戰的目标是實作一個小工具,其目标是能遠端采集已經在運作中的Java程序的方法調用資訊。聽起來像不像BTrace,實際上 BTrace也是 這麼實作的。隻不過因為時間關系,本次的實戰代碼寫的比較簡陋,大家不必關注細節,看下實作的思路就好。

具體的實作思路如下:

agent對指定類的方法進行位元組碼的修改,采集方法的入參和傳回值。并通過socket将請求和傳回發送到服務端

服務端通過attach api通路運作中的Java程序,并加載agent,使agent程式能對目标程序實施

服務端加載agent時指定需要采集的類和方法

服務端開啟一個端口,接受目标程序的請求資訊

老規矩,先看測試代碼,測試代碼很簡單,每隔100ms運作一次sayHi方法,并随機随身睡覺。

package org.xunche.app;
public class HelloTraceAgent {
    public static void main(String[] args) throws InterruptedException {
        HelloTraceAgent helloTraceAgent = new HelloTraceAgent();
        while (true) {
            helloTraceAgent.sayHi("xunche");
            Thread.sleep(100);
        }
    }
    public String sayHi(String name) throws InterruptedException {
        sleep();
        String hi = "hi, " + name + ", " + System.currentTimeMillis();
        return hi;
    }
    public void sleep() throws InterruptedException {
        Thread.sleep((long) (Math.random() * 200));
    }
}
           

接下看agent代碼,思路同等監控方法耗時差不多,在方法出口處,通過asm植入采集方法入參和傳回值的代碼,并通過發件人将資訊通過socket發送到服務端,代碼如下:

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.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
public class TraceAgent {
    public static void agentmain(String args, Instrumentation instrumentation) throws ClassNotFoundException, UnmodifiableClassException {
        if (args == null) {
            return;
        }
        int index = args.lastIndexOf(".");
        if (index != -1) {
            String className = args.substring(0, index);
            String methodName = args.substring(index + 1);
            //目标代碼已經加載,需要重新觸發加載流程,才會通過注冊的轉換器進行轉換
            instrumentation.addTransformer(new TraceClassFileTransformer(className.replace(".", "/"), methodName), true);
            instrumentation.retransformClasses(Class.forName(className));
        }
    }
    public static class TraceClassFileTransformer implements ClassFileTransformer {
        private String traceClassName;
        private String traceMethodName;
        public TraceClassFileTransformer(String traceClassName, String traceMethodName) {
            this.traceClassName = traceClassName;
            this.traceMethodName = traceMethodName;
        }
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
            //過濾掉Jdk、agent、非指定類的方法
            if (className.startsWith("java") || className.startsWith("jdk") || className.startsWith("javax") || className.startsWith("sun")
                    || className.startsWith("com/sun") || className.startsWith("org/xunche/agent") || !className.equals(traceClassName)) {
                //return null會執行原來的位元組碼
                return null;
            }
            ClassReader reader = new ClassReader(classfileBuffer);
            ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
            reader.accept(new TraceVisitor(className, traceMethodName, writer), ClassReader.EXPAND_FRAMES);
            return writer.toByteArray();
        }
    }
    public static class TraceVisitor extends ClassVisitor {
        private String className;
        private String traceMethodName;
        public TraceVisitor(String className, String traceMethodName, ClassVisitor classVisitor) {
            super(Opcodes.ASM5, classVisitor);
            this.className = className;
            this.traceMethodName = traceMethodName;
        }
        @Override
        public MethodVisitor visitMethod(int methodAccess, String methodName, String methodDesc, String signature, String[] exceptions) {
            MethodVisitor methodVisitor = cv.visitMethod(methodAccess, methodName, methodDesc, signature, exceptions);
            if (traceMethodName.equals(methodName)) {
                return new TraceAdviceAdapter(className, methodVisitor, methodAccess, methodName, methodDesc);
            }
            return methodVisitor;
        }
    }
    private static class TraceAdviceAdapter extends AdviceAdapter {
        private final String className;
        private final String methodName;
        private final Type[] methodArgs;
        private final String[] parameterNames;
        private final int[] lvtSlotIndex;
        protected TraceAdviceAdapter(String className, MethodVisitor methodVisitor, int methodAccess, String methodName, String methodDesc) {
            super(Opcodes.ASM5, methodVisitor, methodAccess, methodName, methodDesc);
            this.className = className;
            this.methodName = methodName;
            this.methodArgs = Type.getArgumentTypes(methodDesc);
            this.parameterNames = new String[this.methodArgs.length];
            this.lvtSlotIndex = computeLvtSlotIndices(isStatic(methodAccess), this.methodArgs);
        }
        @Override
        public void visitLocalVariable(String name, String description, String signature, Label start, Label end, int index) {
            for (int i = 0; i < this.lvtSlotIndex.length; ++i) {
                if (this.lvtSlotIndex[i] == index) {
                    this.parameterNames[i] = name;
                }
            }
        }
        @Override
        protected void onMethodExit(int opcode) {
            //排除構造方法和靜态代碼塊
            if ("<init>".equals(methodName) || "<clinit>".equals(methodName)) {
                return;
            }
            if (opcode == RETURN) {
                push((Type) null);
            } else if (opcode == LRETURN || opcode == DRETURN) {
                dup2();
                box(Type.getReturnType(methodDesc));
            } else {
                dup();
                box(Type.getReturnType(methodDesc));
            }
            Type objectType = Type.getObjectType("java/lang/Object");
            push(lvtSlotIndex.length);
            newArray(objectType);
            for (int j = 0; j < lvtSlotIndex.length; j++) {
                int index = lvtSlotIndex[j];
                Type type = methodArgs[j];
                dup();
                push(j);
                mv.visitVarInsn(ALOAD, index);
                box(type);
                arrayStore(objectType);
            }
            visitLdcInsn(className.replace("/", "."));
            visitLdcInsn(methodName);
            mv.visitMethodInsn(INVOKESTATIC, "org/xunche/agent/Sender", "send", "(Ljava/lang/Object;[Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V", false);
        }
        private static int[] computeLvtSlotIndices(boolean isStatic, Type[] paramTypes) {
            int[] lvtIndex = new int[paramTypes.length];
            int nextIndex = isStatic ? 0 : 1;
            for (int i = 0; i < paramTypes.length; ++i) {
                lvtIndex[i] = nextIndex;
                if (isWideType(paramTypes[i])) {
                    nextIndex += 2;
                } else {
                    ++nextIndex;
                }
            }
            return lvtIndex;
        }
        private static boolean isWideType(Type aType) {
            return aType == Type.LONG_TYPE || aType == Type.DOUBLE_TYPE;
        }
        private static boolean isStatic(int access) {
            return (access & 8) > 0;
        }
    }
}           

SpringLocalVariableTableParameterNameNameDiscoverer,注意的同學可以自己研究下。接下來看下Sender中級代碼:

public class Sender {
    private static final int SERVER_PORT = 9876;
    public static void send(Object response, Object[] request, String className, String methodName) {
        Message message = new Message(response, request, className, methodName);
        try {
            Socket socket = new Socket("localhost", SERVER_PORT);
            socket.getOutputStream().write(message.toString().getBytes());
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private static class Message {
        private Object response;
        private Object[] request;
        private String className;
        private String methodName;
        public Message(Object response, Object[] request, String className, String methodName) {
            this.response = response;
            this.request = request;
            this.className = className;
            this.methodName = methodName;
        }
        @Override
        public String toString() {
            return "Message{" +
                    "response=" + response +
                    ", request=" + Arrays.toString(request) +
                    ", className='" + className + '\'' +
                    ", methodName='" + methodName + '\'' +
                    '}';
        }
    }
}
           

Sender中的代碼不複雜,一看就懂,就不多說了。下面我們來看下服務端的代碼,服務端要實作開啟一個端口監聽,接受請求資訊,并使用attach api加載agent。

package org.xunche.app;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class TraceAgentMain {
    private static final int SERVER_PORT = 9876;
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        new Server().start();
        //attach的程序
        VirtualMachine vm = VirtualMachine.attach("85241");
        //加載agent并指明需要采集資訊的類和方法
        vm.loadAgent("trace-agent.jar", "org.xunche.app.HelloTraceAgent.sayHi");
        vm.detach();
    }
    private static class Server implements Runnable {
        @Override
        public void run() {
            try {
                ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
                while (true) {
                    Socket socket = serverSocket.accept();
                    InputStream input = socket.getInputStream();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(input));
                    System.out.println("receive message:" + reader.readLine());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        public void start() {
            Thread thread = new Thread(this);
            thread.start();
        }
    }
}
           

運作上面的程式,可以看到服務端收到了org.xunche.app.HelloTraceAgent.sayHi的請求和傳回資訊。

receive message:Message{response=hi, xunche, 1581599464436, request=[xunche], className='org.xunche.app.HelloTraceAgent', methodName='sayHi'}           

小結

和通過agentmain實作了一個收集運作時方法調用資訊的小工具,當然根據篇幅和時間問題,代碼寫的比較随意,大家多體會實際上,agent的作用遠不止文章中介紹的這些,像 BTrace,arm,springloaded等中也都有用到agent。

轉載自Hollis