当前位置:   article > 正文

手把手教你实现一个Java Agent_自定义java agent

自定义java agent

故事的小黄花

团队中有同事在做性能优化相关的工作,因为公司基础设施不足,同事在代码中写了大量的代码统计某个方法的耗时,大概的代码形式就是

  1. @Override
  2. public void method(Req req) {
  3.     StopWatch stopWatch = new StopWatch();
  4.     stopWatch.start("某某方法-耗时统计");
  5.     method()
  6.     stopWatch.stop();
  7.     log.info("查询耗时分布:{}", stopWatch.prettyPrint());
  8. }

这样的代码非常多,侵入性很大,联想到之前学习的Java Agent技术,可以无侵入式地解决这类问题,所以做了一个很小很小的demo

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

Instrumentation

在了解Agent之前需要先看看Instrumentation

JDK从1.5版本开始引入了java.lang.instrument包,该包提供了一些工具帮助开发人员实现字节码增强,Instrumentation接口的常用方法如下

  1. public interface Instrumentation {
  2.     /**
  3.      * 注册Class文件转换器,转换器用于改变Class文件二进制流的数据
  4.      *
  5.      * @param transformer          注册的转换器
  6.      * @param canRetransform       设置是否允许重新转换
  7.      */
  8.     void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
  9.     /**
  10.      * 移除一个转换器
  11.      *
  12.      * @param transformer          需要移除的转换器
  13.      */
  14.     boolean removeTransformer(ClassFileTransformer transformer);
  15.   
  16.     /**
  17.      * 在类加载之后,重新转换类,如果重新转换的方法有活跃的栈帧,那些活跃的栈帧继续运行未转换前的方法
  18.      *
  19.      * @param 重新转换的类数组
  20.      */
  21.     void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
  22.   
  23.     /**
  24.      * 当前JVM配置是否支持重新转换
  25.      */
  26.     boolean isRetransformClassesSupported();
  27.     /**
  28.      * 获取所有已加载的类
  29.      */
  30.     @SuppressWarnings("rawtypes")
  31.     Class[] getAllLoadedClasses();
  32. }
  33. public interface ClassFileTransformer {
  34.     // className参数表示当前加载类的类名,classfileBuffer参数是待加载类文件的字节数组
  35.     // 调用addTransformer注册ClassFileTransformer以后,后续所有JVM加载类都会被它的transform方法拦截
  36.     // 这个方法接收原类文件的字节数组,在这个方法中做类文件改写,最后返回转换过的字节数组,由JVM加载这个修改过的类文件
  37.     // 如果transform方法返回null,表示不对此类做处理,如果返回值不为null,JVM会用返回的字节数组替换原来类的字节数组
  38.     byte[] transform(  ClassLoader         loader,
  39.                 String              className,
  40.                 Class<?>            classBeingRedefined,
  41.                 ProtectionDomain    protectionDomain,
  42.                 byte[]              classfileBuffer)
  43.         throws IllegalClassFormatException;
  44. }

Instrumentation有两种使用方式

  1. 在JVM启动的时候添加一个Agent jar包

  2. JVM运行以后在任意时刻通过Attach API远程加载Agent的jar包

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

Agent

使用Java Agent需要借助一个方法,该方法的方法签名如下

  1. public static void premain (String agentArgs, Instrumentation instrumentation) {
  2. }

从字面上理解,就是运行在main()函数之前的类。在Java虚拟机启动时,在执行main()函数之前,会先运行指定类的premain()方法,在premain()方法中对class文件进行修改,它有两个入参

  1. agentArgs:启动参数,在JVM启动时指定

  2. instrumentation:上文所将的Instrumentation的实例,我们可以在方法中调用上文所讲的方法,注册对应的Class转换器,对Class文件进行修改

如下图,借助Instrumentation,JVM启动时的处理流程是这样的:JVM会执行指定类的premain()方法,在premain()中可以调用Instrumentation对象的addTransformer方法注册ClassFileTransformer。当JVM加载类时会将类文件的字节数组传递给ClassFileTransformer的transform方法,在transform方法中对Class文件进行解析和修改,之后JVM就会加载转换后的Class文件

hevc?url=https%3A%2F%2Fmmbiz.qpic.cn%2Fmmbiz_jpg%2F6mychickmupWCiaMBopOHnGSIduDesUiaMFEVwvqMRibkzoaUQpJLMboibNXaWfkZwXeBPm9ibYVSzgRpKoML5M1uI1Q%2F640%3Fwx_fmt%3Dother%26tp%3Dwxpic%26wxfrom%3D5%26wx_lazy%3D1%26wx_co%3D1&type=jpg

JVM启动时的处理流程

那我们需要做的就是写一个转换Class文件的ClassFileTransformer,下面用一个计算函数耗时的小例子看看Java Agent是怎么使用的

  1. public class MyClassFileTransformer implements ClassFileTransformer {
  2.     @Override
  3.     public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
  4.         if ("com/example/aop/agent/MyTest".equals(className)) {
  5.             // 使用ASM框架进行字节码转换
  6.             ClassReader cr = new ClassReader(classfileBuffer);
  7.             ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
  8.             ClassVisitor cv = new TimeStatisticsVisitor(Opcodes.ASM7, cw);
  9.             cr.accept(cv, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
  10.             return cw.toByteArray();
  11.         }
  12.         return classfileBuffer;
  13.     }
  14. }
  15. public class TimeStatisticsVisitor extends ClassVisitor {
  16.     public TimeStatisticsVisitor(int api, ClassVisitor classVisitor) {
  17.         super(Opcodes.ASM7, classVisitor);
  18.     }
  19.     @Override
  20.     public MethodVisitor visitMethod(int accessString name, String descriptor, String signature, String[] exceptions) {
  21.         MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
  22.         if (name.equals("<init>")) {
  23.             return mv;
  24.         }
  25.         return new TimeStatisticsAdapter(api, mv, access, name, descriptor);
  26.     }
  27. }
  28. public class TimeStatisticsAdapter extends AdviceAdapter {
  29.     protected TimeStatisticsAdapter(int api, MethodVisitor methodVisitor, int accessString name, String descriptor) {
  30.         super(api, methodVisitor, access, name, descriptor);
  31.     }
  32.     @Override
  33.     protected void onMethodEnter() {
  34.         // 进入函数时调用TimeStatistics的静态方法start
  35.         super.visitMethodInsn(Opcodes.INVOKESTATIC, "com/example/aop/agent/TimeStatistics""start""()V"false);
  36.         super.onMethodEnter();
  37.     }
  38.     @Override
  39.     protected void onMethodExit(int opcode) {
  40.         // 退出函数时调用TimeStatistics的静态方法end
  41.         super.onMethodExit(opcode);
  42.         super.visitMethodInsn(Opcodes.INVOKESTATIC, "com/example/aop/agent/TimeStatistics""end""()V"false);
  43.     }
  44. }
  45. public class TimeStatistics {
  46.     public static ThreadLocal<Long> t = new ThreadLocal<>();
  47.     public static void start() {
  48.         t.set(System.currentTimeMillis());
  49.     }
  50.     public static void end() {
  51.         long time = System.currentTimeMillis() - t.get();
  52.         System.out.println(Thread.currentThread().getStackTrace()[2+ " spend: " + time);
  53.     }
  54. }
  55. public class AgentMain {
  56.     // premain()函数中注册MyClassFileTransformer转换器
  57.     public static void premain (String agentArgs, Instrumentation instrumentation) {
  58.         System.out.println("premain方法");
  59.         instrumentation.addTransformer(new MyClassFileTransformer(), true);
  60.     }
  61. }
  62. <build>
  63.   <plugins>
  64.     <plugin>
  65.       <groupId>org.apache.maven.plugins</groupId>
  66.       <artifactId>maven-assembly-plugin</artifactId>
  67.       <version>3.1.1</version>
  68.       <configuration>
  69.         <descriptorRefs>
  70.           <!--将应用的所有依赖包都打到jar包中。如果依赖的是 jar 包,jar 包会被解压开,平铺到最终的 uber-jar 里去。输出格式为 jar-->
  71.           <descriptorRef>jar-with-dependencies</descriptorRef>
  72.         </descriptorRefs>
  73.         <archive>
  74.           <manifestEntries>
  75.             // 指定premain()的所在方法
  76.             <Agent-CLass>com.example.aop.agent.AgentMain</Agent-CLass>
  77.             <Premain-Class>com.example.aop.agent.AgentMain</Premain-Class>
  78.             <Can-Redefine-Classes>true</Can-Redefine-Classes>
  79.             <Can-Retransform-Classes>true</Can-Retransform-Classes>
  80.           </manifestEntries>
  81.         </archive>
  82.       </configuration>
  83.       <executions>
  84.         <execution>
  85.           <phase>package</phase>
  86.           <goals>
  87.             <goal>single</goal>
  88.           </goals>
  89.         </execution>
  90.       </executions>
  91.     </plugin>
  92.     <plugin>
  93.       <groupId>org.apache.maven.plugins</groupId>
  94.       <artifactId>maven-compiler-plugin</artifactId>
  95.       <version>3.1</version>
  96.       <configuration>
  97.         <source>${maven.compiler.source}</source>
  98.         <target>${maven.compiler.target}</target>
  99.       </configuration>
  100.     </plugin>
  101.   </plugins>
  102. </build>

使用命令行执行下面的测试类

  1. java -javaagent:/Users/zhangxiaobin/IdeaProjects/aop-demo/target/aop-0.0.1-SNAPSHOT-jar-with-dependencies.jar com.example.aop.agent.MyTest
  2. public class MyTest {
  3.     public static void main(String[] args) throws InterruptedException {
  4.         Thread.sleep(3000);
  5.     }
  6. }

计算出了某个方法的耗时

hevc?url=https%3A%2F%2Fmmbiz.qpic.cn%2Fmmbiz_jpg%2F6mychickmupWCiaMBopOHnGSIduDesUiaMFLILUrY5eqEqmXNT2lQ8vySBXJ5bEuHlv05kFMiabibBgvVsictDv1eNyQ%2F640%3Fwx_fmt%3Dother%26tp%3Dwxpic%26wxfrom%3D5%26wx_lazy%3D1%26wx_co%3D1&type=jpg

计算出某个方法的耗时

Attach

在上面的例子中,我们只能在JVM启动时指定一个Agent,这种方式局限在main()方法执行前,如果我们想在项目启动后随时随地地修改Class文件,要怎么办呢?这个时候需要借助Java Agent的另外一个方法,该方法的签名如下

  1. public static void agentmain (String agentArgs, Instrumentation inst) {
  2. }

agentmain()的参数与premain()有着同样的含义,但是agentmain()是在Java Agent被Attach到Java虚拟机上时执行的,当Java Agent被attach到Java虚拟机上,Java程序的main()函数一般已经启动,并且程序很可能已经运行了相当长的时间,此时通过Instrumentation.retransformClasses()方法,可以动态转换Class文件并使之生效,下面用一个小例子演示一下这个功能

下面的类启动后,会不断打印出100这个数字,我们通过Attach功能使之打印出50这个数字

  1. public class PrintNumTest {
  2.     public static void main(String[] args) throws InterruptedException {
  3.         while (true) {
  4.             System.out.println(getNum());
  5.             Thread.sleep(3000);
  6.         }
  7.     }
  8.     private static int getNum() {
  9.         return 100;
  10.     }
  11. }

依然是定义一个ClassFileTransformer,使用ASM框架修改getNum()方法

  1. public class PrintNumTransformer implements ClassFileTransformer {
  2.     @Override
  3.     public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
  4.         if ("com/example/aop/agent/PrintNumTest".equals(className)) {
  5.             System.out.println("asm");
  6.             ClassReader cr = new ClassReader(classfileBuffer);
  7.             ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
  8.             ClassVisitor cv = new TransformPrintNumVisitor(Opcodes.ASM7, cw);
  9.             cr.accept(cv, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
  10.             return cw.toByteArray();
  11.         }
  12.         return classfileBuffer;
  13.     }
  14. }
  15. public class TransformPrintNumVisitor extends ClassVisitor {
  16.     public TransformPrintNumVisitor(int api, ClassVisitor classVisitor) {
  17.         super(Opcodes.ASM7, classVisitor);
  18.     }
  19.     @Override
  20.     public MethodVisitor visitMethod(int accessString name, String descriptor, String signature, String[] exceptions) {
  21.         MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
  22.         if (name.equals("getNum")) {
  23.             return new TransformPrintNumAdapter(api, mv, access, name, descriptor);
  24.         }
  25.         return mv;
  26.     }
  27. }
  28. public class TransformPrintNumAdapter extends AdviceAdapter {
  29.     protected TransformPrintNumAdapter(int api, MethodVisitor methodVisitor, int accessString name, String descriptor) {
  30.         super(api, methodVisitor, access, name, descriptor);
  31.     }
  32.     @Override
  33.     protected void onMethodEnter() {
  34.         super.visitIntInsn(BIPUSH, 50);
  35.         super.visitInsn(IRETURN);
  36.     }
  37. }
  38. public class PrintNumAgent {
  39.     public static void agentmain (String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
  40.         System.out.println("agentmain");
  41.         inst.addTransformer(new PrintNumTransformer(), true);
  42.         Class[] allLoadedClasses = inst.getAllLoadedClasses();
  43.         for (Class allLoadedClass : allLoadedClasses) {
  44.             if (allLoadedClass.getSimpleName().equals("PrintNumTest")) {
  45.                 System.out.println("Reloading: " + allLoadedClass.getName());
  46.                 inst.retransformClasses(allLoadedClass);
  47.                 break;
  48.             }
  49.         }
  50.     }
  51. }
  52. <build>
  53.   <plugins>
  54.     <plugin>
  55.       <groupId>org.apache.maven.plugins</groupId>
  56.       <artifactId>maven-assembly-plugin</artifactId>
  57.       <version>3.1.1</version>
  58.       <configuration>
  59.         <descriptorRefs>
  60.           <!--将应用的所有依赖包都打到jar包中。如果依赖的是 jar 包,jar 包会被解压开,平铺到最终的 uber-jar 里去。输出格式为 jar-->
  61.           <descriptorRef>jar-with-dependencies</descriptorRef>
  62.         </descriptorRefs>
  63.         <archive>
  64.           <manifestEntries>
  65.             // 指定agentmain所在的类
  66.             <Agent-CLass>com.example.aop.agent.PrintNumAgent</Agent-CLass>
  67.             <Premain-Class>com.example.aop.agent.PrintNumAgent</Premain-Class>
  68.             <Can-Redefine-Classes>true</Can-Redefine-Classes>
  69.             <Can-Retransform-Classes>true</Can-Retransform-Classes>
  70.           </manifestEntries>
  71.         </archive>
  72.       </configuration>
  73.       <executions>
  74.         <execution>
  75.           <phase>package</phase>
  76.           <goals>
  77.             <goal>single</goal>
  78.           </goals>
  79.         </execution>
  80.       </executions>
  81.     </plugin>
  82.     <plugin>
  83.       <groupId>org.apache.maven.plugins</groupId>
  84.       <artifactId>maven-compiler-plugin</artifactId>
  85.       <version>3.1</version>
  86.       <configuration>
  87.         <source>${maven.compiler.source}</source>
  88.         <target>${maven.compiler.target}</target>
  89.       </configuration>
  90.     </plugin>
  91.   </plugins>
  92. </build>

因为是跨进程通信,Attach的发起端是一个独立的java程序,这个java程序会调用VirtualMachine.attach方法开始合目标JVM进行跨进程通信

  1. public class MyAttachMain {
  2.     public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
  3.         VirtualMachine virtualMachine = VirtualMachine.attach(args[0]);
  4.         try {
  5.             virtualMachine.loadAgent("/Users/zhangxiaobin/IdeaProjects/aop-demo/target/aop-0.0.1-SNAPSHOT-jar-with-dependencies.jar");
  6.         } finally {
  7.             virtualMachine.detach();
  8.         }
  9.     }
  10. }

使用jps查询到PrintNumTest的进程id,再用下面的命令执行MyAttachMain类

java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/lib/tools.jar:/Users/zhangxiaobin/IdeaProjects/aop-demo/target/aop-0.0.1-SNAPSHOT-jar-with-dependencies.jar com.example.aop.agent.MyAttachMain 49987

可以清楚地看到打印的数字变成了50

hevc?url=https%3A%2F%2Fmmbiz.qpic.cn%2Fmmbiz_jpg%2F6mychickmupWCiaMBopOHnGSIduDesUiaMFJaqH5ZGictlP36mrZkQq0E7Sn8dlKibiaEZibRbicrYBwHZibFMfFHYwnHhg%2F640%3Fwx_fmt%3Dother%26tp%3Dwxpic%26wxfrom%3D5%26wx_lazy%3D1%26wx_co%3D1&type=jpg

效果

Arthas

以上是我写的小demo,有很多不足之处,看看大佬是怎么写的,arthas的trace命令可以统计方法耗时,如下图

hevc?url=https%3A%2F%2Fmmbiz.qpic.cn%2Fmmbiz_jpg%2F6mychickmupWCiaMBopOHnGSIduDesUiaMFcl9bYcrpmRoricAyicaMc7M213FBiagzLQP6fMlvHGMqexVCuoyGxegFw%2F640%3Fwx_fmt%3Dother%26tp%3Dwxpic%26wxfrom%3D5%26wx_lazy%3D1%26wx_co%3D1&type=jpg

Arthas

搭建调试环境

Arthas debug需要借助IDEA的远程debug功能,可以参考 https://github.com/alibaba/arthas/issues/222

先写一个可以循环执行的Demo

  1. public class ArthasTest {
  2.     public static void main(String[] args) throws InterruptedException {
  3.         int i = 0;
  4.         while (true) {
  5.             Thread.sleep(2000);
  6.             print(i++);
  7.         }
  8.     }
  9.     public static void print(Integer content) {
  10.         System.out.println("Main print: " + content);
  11.     }
  12. }

命令行执行改demo

java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8000 com.example.aop.agent.ArthasTest

在Arthas源码的项目中设置远程debug

在Arthas源码的项目中设置远程debug

在Arthas源码的项目中设置远程debug

在这个方法com.taobao.arthas.agent334.AgentBootstrap#main任意位置打上断点,切换到刚刚设置的远程debug模式,启动项目

远程debug模式

可以看到刚刚处于Listening的ArthasTest开始执行,启动arthas-boot.jar,就可以看到断点跳进Arthas源码的项目中

hevc?url=https%3A%2F%2Fmmbiz.qpic.cn%2Fmmbiz_jpg%2F6mychickmupWCiaMBopOHnGSIduDesUiaMFDRCT3MxA5tRc2A9HuPicZAKeUtlDlhmUn4qdVDDyklBgGHM0kOqariaw%2F640%3Fwx_fmt%3Dother%26tp%3Dwxpic%26wxfrom%3D5%26wx_lazy%3D1%26wx_co%3D1&type=jpg

跳进Arthas源码的项目中

bytekit

在看trace命令之前需要一点前置知识,使用ASM进行字节码增强,代码逻辑不好修改,理解困难,所以bytekit基于ASM提供了一套简洁的API,让开发人员可以比较轻松地完成字节码增强,我们先来看一个简单的demo,来自https://github.com/alibaba/bytekit

  1. public class SampleInterceptor {
  2.     @AtEnter(inline = false, suppress = RuntimeException.class, suppressHandler = PrintExceptionSuppressHandler.class)
  3.     public static void atEnter(@Binding.This Object object,
  4.                                @Binding.Class Object clazz,
  5.                                @Binding.Args Object[] args,
  6.                                @Binding.MethodName String methodName,
  7.                                @Binding.MethodDesc String methodDesc) {
  8.         System.out.println("atEnter, args[0]: " + args[0]);
  9.     }
  10.     @AtExit(inline = true)
  11.     public static void atExit(@Binding.Return Object returnObject) {
  12.         System.out.println("atExit, returnObject: " + returnObject);
  13.     }
  14.     @AtExceptionExit(inline = true, onException = RuntimeException.class)
  15.     public static void atExceptionExit(@Binding.Throwable RuntimeException ex,
  16.                                        @Binding.Field(name = "exceptionCount") int exceptionCount) {
  17.         System.out.println("atExceptionExit, ex: " + ex.getMessage() + ", field exceptionCount: " + exceptionCount);
  18.     }
  19. }
  • 上文说过,bytekit的宗旨是提供简介的API让开发可以轻松地完成字节码增强,从注解名我们就可以知道@AtEnter是在方法进入时插入,@AtExit是在方法退出时插入,@AtExceptionExit时在发生异常退出时插入

  • inline = true表示方法中的代码直接插入增强方法中,inline = false表示是调用这个方法,有点难理解,我们等下看反编译后的代码

  • 配置了 suppress = RuntimeException.class 和 suppressHandler = PrintExceptionSuppressHandler.class,说明插入的代码会被 try/catch 包围

  • @AtExceptionExit在原方法体范围try-catch指定异常进行处理

这是我们要进行增强的方法

  1. public class Sample {
  2.     private int exceptionCount = 0;
  3.     public String hello(String str, boolean exception) {
  4.         if (exception) {
  5.             exceptionCount++;
  6.             throw new RuntimeException("test exception, str: " + str);
  7.         }
  8.         return "hello " + str;
  9.     }
  10. }
  11. public class SampleMain {
  12.     public static void main(String[] args) throws Exception {
  13.         // 解析定义的 Interceptor类 和相关的注解
  14.         DefaultInterceptorClassParser interceptorClassParser = new DefaultInterceptorClassParser();
  15.         List<InterceptorProcessor> processors = interceptorClassParser.parse(SampleInterceptor.class);
  16.         // 加载字节码
  17.         ClassNode classNode = AsmUtils.loadClass(Sample.class);
  18.         // 对加载到的字节码做增强处理
  19.         for (MethodNode methodNode : classNode.methods) {
  20.             if (methodNode.name.equals("hello")) {
  21.                 MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode);
  22.                 for (InterceptorProcessor interceptor : processors) {
  23.                     interceptor.process(methodProcessor);
  24.                 }
  25.             }
  26.         }
  27.         // 获取增强后的字节码
  28.         byte[] bytes = AsmUtils.toBytes(classNode);
  29.         // 查看反编译结果
  30.         System.out.println(Decompiler.decompile(bytes));
  31.         // 修改Sample
  32.         AgentUtils.reTransform(Sample.class, bytes);
  33.         // 执行sample的方法
  34.         try {
  35.             Sample sample = new Sample();
  36.             sample.hello("3"false);
  37.             sample.hello("4"true);
  38.         } catch (Exception e) {
  39.             e.printStackTrace();
  40.         }
  41.     }
  42. }

这是Sample反编译后的结果,代码量剧增

  1. public class Sample {
  2.     private int exceptionCount = 0;
  3.     /*
  4.      * WARNING - void declaration
  5.      */
  6.     public String hello(String stringboolean bl) {
  7.         try {
  8.             String string2;
  9.             void str;
  10.             void exception;
  11.             try {
  12.                 // @AtEnter 直接调用,inline为false的效果
  13.                 SampleInterceptor.atEnter((Object)this, Sample.class, (Object[])new Object[]{string, new Boolean(bl)}, (String)"hello", (String)"(Ljava/lang/String;Z)Ljava/lang/String;");
  14.             }
  15.             catch (RuntimeException runtimeException) {
  16.                 Class<Sample> clazz = Sample.class;
  17.                 RuntimeException runtimeException2 = runtimeException;
  18.                 System.out.println("exception handler: " + clazz);
  19.                 runtimeException2.printStackTrace();
  20.             }
  21.             if (exception != false) {
  22.                 ++this.exceptionCount;
  23.                 throw new RuntimeException("test exception, str: " + (String)str);
  24.             }
  25.             String string3 = string2 = "hello " + (String)str;
  26.             // @AtExit 代码直接插入
  27.             System.out.println("atExit, returnObject: " + string3);
  28.             return string2;
  29.         }
  30.         catch (RuntimeException runtimeException) {
  31.             int n = this.exceptionCount;
  32.             RuntimeException runtimeException3 = runtimeException;
  33.             // @AtExceptionExit 代码直接插入
  34.             System.out.println("atExceptionExit, ex: " + runtimeException3.getMessage() + ", field exceptionCount: " + n);
  35.             throw runtimeException;
  36.         }
  37.     }
  38. }

有了这个前置知识,我们来看看trace命令

trace

hevc?url=https%3A%2F%2Fmmbiz.qpic.cn%2Fmmbiz_jpg%2F6mychickmupWCiaMBopOHnGSIduDesUiaMF1pbGaNOAANMfVlvgN0t22ezzqoZvUVemiaH44GtRRmmLvVkGXjA8Ntw%2F640%3Fwx_fmt%3Dother%26tp%3Dwxpic%26wxfrom%3D5%26wx_lazy%3D1%26wx_co%3D1&type=jpg

trace

Arthas命令很多,如果是exit、logout、quit、jobs、fg、bg、kill等简单的命令,就会直接执行,如果是trace这种复杂的命令,会专门用一个类写处理的逻辑,如上图,根据名字就可以猜到这个类是处理什么命令的,这么多类的组织形式是模版模式,入口在com.taobao.arthas.core.shell.command.AnnotatedCommand#process,

  1. public abstract class AnnotatedCommand {
  2.     public abstract void process(CommandProcess process);
  3. }
  4. public class TraceCommand extends EnhancerCommand {
  5. }
  6. public abstract class EnhancerCommand extends AnnotatedCommand {
  7.     @Override
  8.     public void process(final CommandProcess process) {
  9.         // ctrl-C support
  10.         process.interruptHandler(new CommandInterruptHandler(process));
  11.         // q exit support
  12.         process.stdinHandler(new QExitHandler(process));
  13.         // start to enhance
  14.         enhance(process);
  15.     }
  16. }

有一些命令都有字节码增强的逻辑,这些逻辑共同封装在了EnhancerCommand这个类中,TraceCommand继承了EnhancerCommand,当trace命令执行的时候,增强的逻辑在EnhancerCommand,我们只看核心代码

  1. com.taobao.arthas.core.command.monitor200.EnhancerCommand#enhance
  2. com.taobao.arthas.core.advisor.Enhancer#enhance(java.lang.instrument.Instrumentation)
  3. public synchronized EnhancerAffect enhance(final Instrumentation inst) throws UnmodifiableClassException {
  4.         ......
  5.         try {
  6.             // 很明显,这里添加了一个文件转换器,注意,此处的转换器为本类
  7.             ArthasBootstrap.getInstance().getTransformerManager().addTransformer(this, isTracing);
  8.             ......
  9.         } catch (Throwable e) {
  10.             logger.error("Enhancer error, matchingClasses: {}", matchingClasses, e);
  11.             affect.setThrowable(e);
  12.         }
  13.         return affect;
  14.     }

根据方法名就可以在本类搜索到,具体代码如下

  1. @Override
  2. public byte[] transform(final ClassLoader inClassLoader, String className, Class<?> classBeingRedefined,
  3.                         ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
  4.     try {
  5.       // 检查classloader能否加载到 SpyAPI,如果不能,则放弃增强
  6.       try {
  7.         if (inClassLoader != null) {
  8.           inClassLoader.loadClass(SpyAPI.class.getName());
  9.         }
  10.       } catch (Throwable e) {
  11.         logger.error("the classloader can not load SpyAPI, ignore it. classloader: {}, className: {}",
  12.                      inClassLoader.getClass().getName(), className, e);
  13.         return null;
  14.       }
  15.       // 这里要再次过滤一次,为啥?因为在transform的过程中,有可能还会再诞生新的类
  16.       // 所以需要将之前需要转换的类集合传递下来,再次进行判断
  17.       if (matchingClasses != null && !matchingClasses.contains(classBeingRedefined)) {
  18.         return null;
  19.       }
  20.       // ClassNode中有各种属性,对应Class文件结构
  21.       // keep origin class reader for bytecode optimizations, avoiding JVM metaspace OOM.
  22.       ClassNode classNode = new ClassNode(Opcodes.ASM9);
  23.       ClassReader classReader = AsmUtils.toClassNode(classfileBuffer, classNode);
  24.       // remove JSR https://github.com/alibaba/arthas/issues/1304
  25.       classNode = AsmUtils.removeJSRInstructions(classNode);
  26.       // 重要代码,生成增强字节码的拦截器
  27.       DefaultInterceptorClassParser defaultInterceptorClassParser = new DefaultInterceptorClassParser();
  28.       final List<InterceptorProcessor> interceptorProcessors = new ArrayList<InterceptorProcessor>();
  29.       interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor1.class));
  30.       interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor2.class));
  31.       interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor3.class));
  32.       if (this.isTracing) {
  33.         // 根据配置判断trace命令是否要跳过计算Java类库的代码的耗时
  34.         if (!this.skipJDKTrace) {
  35.           interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceInterceptor1.class));
  36.           interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceInterceptor2.class));
  37.           interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceInterceptor3.class));
  38.         } else {
  39.           interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceExcludeJDKInterceptor1.class));
  40.           interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceExcludeJDKInterceptor2.class));
  41.           interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceExcludeJDKInterceptor3.class));
  42.         }
  43.       }
  44.       List<MethodNode> matchedMethods = new ArrayList<MethodNode>();
  45.       for (MethodNode methodNode : classNode.methods) {
  46.         if (!isIgnore(methodNode, methodNameMatcher)) {
  47.           matchedMethods.add(methodNode);
  48.         }
  49.       }
  50.       // https://github.com/alibaba/arthas/issues/1690
  51.       if (AsmUtils.isEnhancerByCGLIB(className)) {
  52.         for (MethodNode methodNode : matchedMethods) {
  53.           if (AsmUtils.isConstructor(methodNode)) {
  54.             AsmUtils.fixConstructorExceptionTable(methodNode);
  55.           }
  56.         }
  57.       }
  58.       .......
  59.       for (MethodNode methodNode : matchedMethods) {
  60.         if (AsmUtils.isNative(methodNode)) {
  61.           logger.info("ignore native method: {}",
  62.                       AsmUtils.methodDeclaration(Type.getObjectType(classNode.name), methodNode));
  63.           continue;
  64.         }
  65.         // 先查找是否有 atBeforeInvoke 函数,如果有,则说明已经有trace了,则直接不再尝试增强,直接插入 listener
  66.         if(AsmUtils.containsMethodInsnNode(methodNode, Type.getInternalName(SpyAPI.class), "atBeforeInvoke")) {
  67.           for (AbstractInsnNode insnNode = methodNode.instructions.getFirst(); insnNode != null; insnNode = insnNode
  68.                .getNext()) {
  69.             if (insnNode instanceof MethodInsnNode) {
  70.               final MethodInsnNode methodInsnNode = (MethodInsnNode) insnNode;
  71.               if(this.skipJDKTrace) {
  72.                 if(methodInsnNode.owner.startsWith("java/")) {
  73.                   continue;
  74.                 }
  75.               }
  76.               // 原始类型的box类型相关的都跳过
  77.               if(AsmOpUtils.isBoxType(Type.getObjectType(methodInsnNode.owner))) {
  78.                 continue;
  79.               }
  80.               AdviceListenerManager.registerTraceAdviceListener(inClassLoader, className,
  81.                                                                 methodInsnNode.owner, methodInsnNode.name, methodInsnNode.desc, listener);
  82.             }
  83.           }
  84.         }else {
  85.           // 重点代码,增强动作就是在这里完成的
  86.           MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode, groupLocationFilter);
  87.           for (InterceptorProcessor interceptor : interceptorProcessors) {
  88.             try {
  89.               List<Location> locations = interceptor.process(methodProcessor);
  90.               for (Location location : locations) {
  91.                 if (location instanceof MethodInsnNodeWare) {
  92.                   MethodInsnNodeWare methodInsnNodeWare = (MethodInsnNodeWare) location;
  93.                   MethodInsnNode methodInsnNode = methodInsnNodeWare.methodInsnNode();
  94.                   AdviceListenerManager.registerTraceAdviceListener(inClassLoader, className,
  95.                                                                     methodInsnNode.owner, methodInsnNode.name, methodInsnNode.desc, listener);
  96.                 }
  97.               }
  98.             } catch (Throwable e) {
  99.               logger.error("enhancer error, class: {}, method: {}, interceptor: {}", classNode.name, methodNode.name, interceptor.getClass().getName(), e);
  100.             }
  101.           }
  102.         }
  103.         // enter/exist 总是要插入 listener
  104.         AdviceListenerManager.registerAdviceListener(inClassLoader, className, methodNode.name, methodNode.desc,
  105.                                                      listener);
  106.         affect.addMethodAndCount(inClassLoader, className, methodNode.name, methodNode.desc);
  107.       }
  108.       // https://github.com/alibaba/arthas/issues/1223 , V1_5 的major version是49
  109.       if (AsmUtils.getMajorVersion(classNode.version) < 49) {
  110.         classNode.version = AsmUtils.setMajorVersion(classNode.version, 49);
  111.       }
  112.       byte[] enhanceClassByteArray = AsmUtils.toBytes(classNode, inClassLoader, classReader);
  113.       // 增强成功,记录类
  114.       classBytesCache.put(classBeingRedefined, new Object());
  115.       // dump the class
  116.       dumpClassIfNecessary(className, enhanceClassByteArray, affect);
  117.       // 成功计数
  118.       affect.cCnt(1);
  119.       return enhanceClassByteArray;
  120.     } catch (Throwable t) {
  121.       logger.warn("transform loader[{}]:class[{}] failed.", inClassLoader, className, t);
  122.       affect.setThrowable(t);
  123.     }
  124.     return null;
  125. }

这段代码很长,其实主要逻辑就两个

  • 解析Interceptor Class的@AtXxx,@Binding等注解,生成InterceptorProcessor对象集合

  • 遍历InterceptorProcessor集合,修改原方法的字节码

整体的流程如下图

hevc?url=https%3A%2F%2Fmmbiz.qpic.cn%2Fmmbiz_jpg%2F6mychickmupWCiaMBopOHnGSIduDesUiaMFJxuPetRJ0aVibsSEMs9jlrulxFvvtcMGfFYY0s4Ae2CicTdcyhDPGQCQ%2F640%3Fwx_fmt%3Dother%26tp%3Dwxpic%26wxfrom%3D5%26wx_lazy%3D1%26wx_co%3D1&type=jpg

整体的流程如图

那这些拦截器长什么样子呢?我们随便找一个例子来看看

  1. public static class SpyInterceptor1 {
  2.     @AtEnter(inline = true)
  3.     public static void atEnter(@Binding.This Object target, @Binding.Class Class<?> clazz,
  4.                                @Binding.MethodInfo String methodInfo, @Binding.Args Object[] args) {
  5.       SpyAPI.atEnter(clazz, methodInfo, target, args);
  6.     }
  7. }

看到这里,就很熟悉了,跟上面bytekit的例子很像,是在方法进入时插入的,当然,这里只是浅讲一下trace的原理,bytekit背后的原理,需要更底层的知识储备,我还需要继续学习

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/349171
推荐阅读
相关标签
  

闽ICP备14008679号