我的天,你工作5年了,连Java agent都不知道...上篇

# 引言

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

# 简介

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

在介绍 agent 之前,先给大家简单说下 Instrumentation 。它是 JDK1.5 提供的 API ,用于拦截类加载事件,并对字节码进行修改,它的主要方法如下:

publicinterfaceInstrumentation{//注册一个转换器,类加载事件会被注册的转换器所拦截voidaddTransformer(ClassFileTransformer transformer,booleancanRetransform);//重新触发类加载voidretransformClasses(Class<?>... classes)throwsUnmodifiableClassException;//直接替换类的定义voidredefineClasses(ClassDefinition... definitions)throwsClassNotFoundException, UnmodifiableClassException;}

# premain

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

java-javaagent:agent.jar=xunche HelloWorld

premain 共提供以下 2 种重载方法, Jvm 启动时会先尝试使用第一种方法,若没有会使用第二种方法:

publicstaticvoidpremain(String agentArgs, Instrumentation inst);publicstaticvoidpremain(String agentArgs);

一个简单的例子

下面我们通过一个程序来简单说明下 premain 的使用,首先我们准备下测试代码,测试代码比较简单,运行 main 方法并输出 hello world 。

package org.xunche.app;publicclassHelloWorld{publicstaticvoidmain(String[] args){System.out.println("Hello World");    }}

接下来我们看下 agent 的代码,运行 premain 方法并输出我们传入的参数。

package org.xunche.agent;publicclassHelloAgent{publicstaticvoidpremain(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.mfjavacorg/xunche/agent/HelloAgent.javajavacorg/xunche/app/HelloWorld.javajarcvmf manifest.mf hello-agent.jar org/

接下来,我们编译下并运行下测试代码,这里为了测试简单,我将编译后的 class 和 agent 的 jar 包放在了同级目录下

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

可以看到输出结果如下,agent中的premain方法有限于main方法执行

Hello Agent: xuncheHelloWorld

稍微复杂点的例子

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

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

首先还是看下测试代码,逻辑很简单, main 方法执行时调用 sayHi 方法,输出 hi ,  xunche ,并随机睡眠一段时间。

packageorg.xunche.app;publicclassHelloXunChe{publicstaticvoidmain(String[] args)throwsInterruptedException{HelloXunChe helloXunChe =newHelloXunChe();        helloXunChe.sayHi();    }publicvoidsayHi()throwsInterruptedException{System.out.println("hi, xunche");        sleep();    }publicvoidsleep()throwsInterruptedException{Thread.sleep((long) (Math.random() *200));    }}

接下来我们借助 asm 来植入我们自己的代码,在 jvm 加载类的时候,为类的每个方法加上统计方法调用耗时的代码,代码如下,这里的 asm 我使用了 jdk 自带的,当然你也可以使用官方的 asm 类库。

packageorg.xunche.agent;importjdk.internal.org.objectweb.asm.*;importjdk.internal.org.objectweb.asm.commons.AdviceAdapter;importjava.lang.instrument.ClassFileTransformer;importjava.lang.instrument.Instrumentation;importjava.security.ProtectionDomain;publicclassTimeAgent{publicstaticvoidpremain(String args, Instrumentation instrumentation){instrumentation.addTransformer(newTimeClassFileTransformer());    }privatestaticclassTimeClassFileTransformerimplementsClassFileTransformer{@Overridepublicbyte[] 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或者执行异常会执行原来的字节码returnnull;            }System.out.println("loaded class: "+ className);ClassReader reader =newClassReader(classfileBuffer);ClassWriter writer =newClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);reader.accept(newTimeClassVisitor(writer), ClassReader.EXPAND_FRAMES);returnwriter.toByteArray();        }    }publicstaticclassTimeClassVisitorextendsClassVisitor{publicTimeClassVisitor(ClassVisitor classVisitor){super(Opcodes.ASM5, classVisitor);        }@OverridepublicMethodVisitorvisitMethod(intmethodAccess, String methodName, String methodDesc, String signature, String[] exceptions){            MethodVisitor methodVisitor = cv.visitMethod(methodAccess, methodName, methodDesc, signature, exceptions);returnnewTimeAdviceAdapter(Opcodes.ASM5, methodVisitor, methodAccess, methodName, methodDesc);        }    }publicstaticclassTimeAdviceAdapterextendsAdviceAdapter{privateString methodName;protectedTimeAdviceAdapter(intapi, MethodVisitor methodVisitor,intmethodAccess, String methodName, String methodDesc){super(api, methodVisitor, methodAccess, methodName, methodDesc);this.methodName = methodName;        }@OverrideprotectedvoidonMethodEnter(){//在方法入口处植入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);        }@OverrideprotectedvoidonMethodExit(inti){//在方法出口植入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 的代码:

packageorg.xunche.agent;importjava.util.HashMap;importjava.util.Map;publicclassTimeHolder{privatestaticMap timeCache =newHashMap<>();publicstaticvoidstart(String method){        timeCache.put(method, System.currentTimeMillis());    }publicstaticlongcost(String method){returnSystem.currentTimeMillis() - timeCache.get(method);    }}

至此,agent 的代码编写完成,有关 asm 的部分不是本章的重点,日后再单独推出一篇有关 asm 的文章。通过在类加载时植入我们监控的代码后,下面我们来看看,经过 asm 修改后的代码是怎样的。可以看到,与最开始的测试代码相比,每个方法都加入了我们统计方法耗时的代码。

packageorg.xunche.app;importorg.xunche.agent.TimeHolder;publicclassHelloXunChe{publicHelloXunChe(){    }publicstaticvoidmain(String[] args)throwsInterruptedException{TimeHolder.start(args.getClass().getName() +"."+"main");HelloXunChe helloXunChe =newHelloXunChe();        helloXunChe.sayHi();HelloXunChe helloXunChe = args.getClass().getName() +"."+"main";System.out.println(helloXunChe +": "+ TimeHolder.cost(helloXunChe));    }publicvoidsayHi()throwsInterruptedException{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));    }publicvoidsleep()throwsInterruptedException{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 时指定需要采集的类和方法

服务端开启一个端口,接受目标进程的请求信息

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容