前言

基于上文,统计代码执行耗时,会发现很多代码越来越不会写,这些代码的原理是什么。此时我们就需要补充一些简单的ASM字节码相关的知识。

ASM知识点汇总

类访问ClassVisitor

�这里是类访问的入口,我们的所有方法都是以此为切入点的,这里定义了一个类访问的标准流程,我们可以直接看代码注释

  1. /**
  2. * A visitor to visit a Java class. The methods of this class must be called in the following order:
  3. * {@code visit} [ {@code visitSource} ] [ {@code visitModule} ][ {@code visitNestHost} ][ {@code
  4. * visitOuterClass} ] ( {@code visitAnnotation} | {@code visitTypeAnnotation} | {@code
  5. * visitAttribute} )* ( {@code visitNestMember} | {@code visitInnerClass} | {@code visitField} |
  6. * {@code visitMethod} )* {@code visitEnd}.
  7. *
  8. * @author Eric Bruneton
  9. */
  10. public abstract class ClassVisitor {

我们基于此,给出一些关键方法的使用场景

  1. visit:Visits the header of the class
  2. visitSource:Visits the source of the class
  3. visitModule:Visit the module corresponding to the class.
  4. visitNestHost:Visits the nest host class of the class. A nest is a set of classes of the same package that share access to their private members. One of these classes, called the host, lists the other members of the nest, which in turn should link to the host of their nest. This method must be called only once and only if the visited class is a non-host member of a nest. A class is implicitly its own nest, so it's invalid to call this method with the visited class name as argument.
  5. visitOuterClass:Visits the enclosing class of the class. This method must be called only if the class has an enclosing class.
  6. visitAnnotation:Visits an annotation of the class.
  7. visitTypeAnnotation:Visits an annotation on a type in the class signature.
  8. visitAttribute:Visits a non standard attribute of the class.
  9. visitNestMember:Visits a member of the nest. A nest is a set of classes of the same package that share access to their private members. One of these classes, called the host, lists the other members of the nest, which in turn should link to the host of their nest. This method must be called only if the visited class is the host of a nest. A nest host is implicitly a member of its own nest, so it's invalid to call this method with the visited class name as argument.
  10. visitInnerClass:Visits information about an inner class. This inner class is not necessarily a member of the class being visited.
  11. visitField:Visits a field of the class.
  12. visitMethod:Visits a method of the class. This method must return a new MethodVisitor instance (or null) each time it is called, i.e., it should not return a previously returned visitor.
  13. visitEnd:Visits the end of the class. This method, which is the last one to be called, is used to inform the visitor that all the fields and methods of the class have been visited.

以上是一个类的字节码标准访问顺序,我们主要是在这个流程中带入自己的逻辑,简称插桩。我们再回头看看之前的代码TimeCoster的实现原理

  1. 定义一个特殊的字段ASM_TIME_COST,把这个字段插入到各个要关注的类中,然后围绕它把相关数据收集起来,最后打印出来。
  2. 怎么存储上述字段,我们使用了本地变量表,先写成java代码,然后借助ASM的插件编译为字节码指令
  3. 由于插入了新的代码,栈帧发生了变化,调整栈帧大小,确保新的字节码文件符合标准规范

基于此,我们会发现功能实现有些冗余,有没有更好的方法呢?
当然有!!!

  1. 插入静态代码,在方法进入前做一个时间点,在方法返回前做一个时间差
  2. 在1的基础上可以功能增强,比如使用注解

我们现在就实现第一个方案,第二个方案留给广大读者

  1. package com.deer.agent.sandbox;
  2. import java.util.HashMap;
  3. import java.util.Map;
  4. /**
  5. *
  6. * 计算耗时,fullMethodName = className.methodName
  7. */
  8. public class TimeUtil {
  9. public static Map<String, Long> sStartTime = new HashMap<>();
  10. public static Map<String, Long> sEndTime = new HashMap<>();
  11. public static void setStartTime(String fullMethodName, long time) {
  12. sStartTime.put(fullMethodName, time);
  13. }
  14. public static void setEndTime(String fullMethodName, long time) {
  15. sEndTime.put(fullMethodName, time);
  16. }
  17. public static String getCostTime(String fullMethodName) {
  18. long start = sStartTime.get(fullMethodName);
  19. long end = sEndTime.get(fullMethodName);
  20. return "method: " + fullMethodName + " -cost- " + Long.valueOf(end - start) + " ns";
  21. }
  22. }
  1. package com.deer.agent.sandbox;
  2. import org.objectweb.asm.ClassVisitor;
  3. import org.objectweb.asm.MethodVisitor;
  4. import org.objectweb.asm.Opcodes;
  5. import org.objectweb.asm.commons.AdviceAdapter;
  6. public class TimeCoster2 extends ClassVisitor implements Opcodes {
  7. private String clazzName;
  8. public TimeCoster2(ClassVisitor classVisitor) {
  9. super(ASM7, classVisitor);
  10. }
  11. @Override
  12. public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
  13. super.visit(version, access, name, signature, superName, interfaces);
  14. clazzName = name;
  15. }
  16. @Override
  17. public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
  18. MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
  19. mv = new AdviceAdapter(ASM7,mv,access,name,descriptor) {
  20. @Override
  21. protected void onMethodEnter() {
  22. TimeUtil.setStartTime(clazzName+"."+name,System.nanoTime());
  23. super.onMethodEnter();
  24. }
  25. @Override
  26. protected void onMethodExit(int opcode) {
  27. TimeUtil.setEndTime(clazzName+"."+name,System.nanoTime());
  28. System.out.println(TimeUtil.getCostTime(clazzName+"."+name));
  29. super.onMethodExit(opcode);
  30. }
  31. };
  32. return mv;
  33. }
  34. }

此处,我们只需要关注和TimeUtil有关的相关代码,让ASM插件帮我们转换为字节码即可

image.png
如图所示,我们只需要用类名方法名替换掉字符串abc即可得到全部代码
最后我们拿到最终的代码如下

  1. package com.deer.agent.sandbox;
  2. import org.objectweb.asm.ClassVisitor;
  3. import org.objectweb.asm.MethodVisitor;
  4. import org.objectweb.asm.Opcodes;
  5. import org.objectweb.asm.commons.AdviceAdapter;
  6. public class TimeCoster2 extends ClassVisitor implements Opcodes {
  7. private String clazzName;
  8. public TimeCoster2(ClassVisitor classVisitor) {
  9. super(ASM7, classVisitor);
  10. }
  11. @Override
  12. public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
  13. super.visit(version, access, name, signature, superName, interfaces);
  14. clazzName = name;
  15. }
  16. @Override
  17. public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
  18. MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
  19. //跳过java原生相关的类
  20. if (!clazzName.startsWith("com/deer/base")
  21. ) {
  22. return mv;
  23. }
  24. mv = new AdviceAdapter(ASM7,mv,access,name,descriptor) {
  25. @Override
  26. protected void onMethodEnter() {
  27. mv.visitLdcInsn(clazzName+"."+name);
  28. mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
  29. mv.visitMethodInsn(INVOKESTATIC, "com/deer/agent/sandbox/TimeUtil", "setStartTime", "(Ljava/lang/String;J)V", false);
  30. super.onMethodEnter();
  31. }
  32. @Override
  33. protected void onMethodExit(int opcode) {
  34. mv.visitLdcInsn(clazzName+"."+name);
  35. mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
  36. mv.visitMethodInsn(INVOKESTATIC, "com/deer/agent/sandbox/TimeUtil", "setEndTime", "(Ljava/lang/String;J)V", false);
  37. mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
  38. mv.visitLdcInsn(clazzName+"."+name);
  39. mv.visitMethodInsn(INVOKESTATIC, "com/deer/agent/sandbox/TimeUtil", "getCostTime", "(Ljava/lang/String;)Ljava/lang/String;", false);
  40. mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
  41. super.onMethodExit(opcode);
  42. }
  43. };
  44. return mv;
  45. }
  46. }

执行后得到结果
image.png

此时看代码逻辑,比之前清晰很多,具有非常明确的思路和方法。
那么,之前的代码写那么长是吓退一众观众的吗,是的没错
假如借助javaassist来修改字节码,那是不是更方便了,那么借助注解,是不是也可以实现?
这里给一个思路,借助我们的类名+方法名,重写 visitAnnotation�方法,识别出来要处理的类和方法即可。

方法访问MethodVisitor

�根据上述示例,我们知道了方法访问的一般用法,在此,我们回忆并思考一下之前的代码com.deer.base.service.impl.HelloServiceImpl,为什么没有被拦截执行。
首先,测试项目是一个普通的spring项目,HelloServiceImpl会变成一个普通的spring bean,众所周知,spring中的bean是通过代理或者字节码的形式转的,因此类名已经面目全非了,默认情况下是一个jdk的代理类,格式如下
java.lang.reflect.Proxy#56
那么,我们怎么才能正确的拦截呢

  1. package com.deer.agent;
  2. import com.deer.agent.sandbox.AgentClazzTransformer;
  3. import java.lang.instrument.Instrumentation;
  4. import java.util.Arrays;
  5. public class AgentMain {
  6. public static void premain(String agentArgs, Instrumentation inst) {
  7. System.out.println("premain");
  8. }
  9. public static void agentmain(String agentArgs, Instrumentation inst){
  10. System.out.println("load agent...");
  11. Class[] clazzList = inst.getAllLoadedClasses();
  12. Arrays.stream(clazzList).forEach(clazz->{
  13. if(clazz.getName().startsWith("com.deer.base.service.impl")){
  14. //
  15. try {
  16. inst.addTransformer(new AgentClazzTransformer(), true);
  17. inst.retransformClasses(clazz);
  18. }catch (Exception e){
  19. //
  20. }
  21. }
  22. });
  23. }
  24. }

到此,我们去测试一下执行的结果
image.png
符合预期,我们期待的spring bean方法也被监控到了。
那么,我们在最开头提出的最初级的功能——>在代码入口处加代码

  1. System.out.println("hello, i come in ...");

现在应该也能实现了,留给读者自己实现。
那么到现在为止,我们开始进行代码插桩的思路设计。

代码插桩

通过前面那些完整的例子,进一步思考:

  1. 在字节码中插入简单的代码:打日志,记录方法耗时,本质上是在利用字节码规范在字节码中填充我们自己需要的逻辑,如果这个诉求比较复杂,比如:记录一个方法调用的顺序,更进一步可以实现全链路监控。比如,现在线上接口受限,不能debug,我们需要知道具体的方法耗时,调用链路,出入参,是否可以灵活挂载一个java agent直接实现。比如,现在线上有个紧急问题,不能停机,也没有热启动,是否可以灵活挂载一个java agent直接把代码逻辑替换掉。
  2. asm这种纯字节码的属于技术研究,怎么把它提升到更高的维度,做成生产级产品,比如流量录制与回放,轻量级debug。

我们带着上述问题,设计几个目标,然后一步步实现。

  • 记录方法入参出参
  • 类似java远程debug的形式,打印出一个方法调用链路,出入参
  • 类似skywalking,做一个全链路的调用监控
  • 类似阿里巴巴的jvm-sandbox-repeater做一个流量录制与回放工具
  • 类似阿里巴巴的混沌工程,做一个故障注入与修复的系统