写点什么

Java agent 还不了解的程序员该反省一下了 (1)

用户头像
极客good
关注
发布于: 刚刚

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/


接下来,我们编译下并运行下测试代码,这里为了测试简单,我将编译后的 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 方法的返回值。


下面来看下 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 的文章。通过在类加载时植入我们监控的代码后,下面我们来看看,经过 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));


}


}



agentmain


=============


上面的 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 植入采集方法入参和返回值的代码,并通过 Sender 将信息通过 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(class


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


Name.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) {

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
Java agent还不了解的程序员该反省一下了(1)