头图

Investigate bytecode instrumentation technology for the design and implementation of Internet distributed system monitoring!

小傅哥
中文

Author: Xiao Fu Ge
Blog: https://bugstack.cn

Precipitate, share, and grow, so that you and others can gain something! 😄

1. A call from late at night!

, is your online system streaking?

Late at night when I was sleeping on the weekend, I suddenly received a call from the boss ☎ urging. "Hurry up and check WeChat and WeChat. We don't know that there is a problem with the system, and we need to know from user feedback!!!" I got up late at night, turned on the computer to connect to the VPN, yawned, and opened my dim eyes. Check the system log, it turns out that the system is hung up, restart and recover!

Although the reboot restored the system, it also reset the boss's distorted expression. But how did the system hang? Because there is no monitoring system, and I don’t know whether it is caused by too much traffic or program problems. Through the pieces of logs, I can only roughly estimate some seem to be 160f77faed6d22 to the boss. report. But the boss is not stupid, , chat about, let all the system operation status be monitored.

I drag my sleepy head with my hands, and I can't think of a good way for a while, is hard-coded on each method to perform the time-consuming calculation . and displayed on a monitoring page. The monitoring page uses Apache's 160f77faed6d8d echarts , not to mention that if it is displayed like this, it is really good-looking and easy to use.

  • But such hard coding is not a gadget, it doesn't make the programmers who move bricks in our department tired! Besides, they must despise me for doing so. what architect, to monitor the system, you have to hard-code it, it’s silly, isn’t it? ! !
  • I can't sleep after thinking about it, so I have to find information and report to the boss tomorrow!

In fact, whether an online system runs stably depends on its operational health, which includes a comprehensive value of various indicators such as call volume, availability, impact duration, and server performance. And when an abnormal problem occurs in the system, the entire business method execution link can be captured and output; the input parameters, output parameters, abnormal information, etc. at that time. Of course, it also includes some performance indicators of JVM, Redis, and Mysql to quickly locate and solve problems.

So what are the solutions to this kind of thing? In fact, there are still more ways to do it, such as;

  1. The simplest and most rude is to hard-code in the method to collect execution time and access parameters and exception information. However, the cost of such coding is too large, and a lot of regression testing is required after hard coding, which may bring certain risks to the system. copy and paste it wrong!
  2. You can choose the aspect method to make a set of unified monitoring components, which is relatively better. But it also requires hard coding, such as writing annotations, and the maintenance cost is not low.
  3. In fact, there are a whole set of non-intrusive monitoring solutions for such monitoring on the market. For example, Google Dapper, Zipkin, etc. can all meet the monitoring system requirements, and they are all based on probe technology non-intrusive and adopt bytecode enhancement methods to collect System operation information is analyzed and the operation status is monitored.

Okay, so this article will take you to try several different ways to monitor the running status of the system.

2. Preparation

This article will implement different monitoring implementation codes AOP and bytecode framework ( ASM , Javassist , Byte-Buddy The entire project structure is as follows:

MonitorDesign
├── cn-bugstack-middleware-aop
├── cn-bugstack-middleware-asm
├── cn-bugstack-middleware-bytebuddy
├── cn-bugstack-middleware-javassist
├── cn-bugstack-middleware-test
└──    pom.xml
  • Source code address: https://github.com/fuzhengwei/MonitorDesign
  • Brief introduction: aop, asm, bytebuddy, javassist are four different implementation schemes. test is a simple test project based on SpringBoot.
  • Technical use: SpringBoot, asm, byte-buddy, javassist

cn-bugstack-middleware-test

@RestController
public class UserController {

    private Logger logger = LoggerFactory.getLogger(UserController.class);

    /**
     * 测试:http://localhost:8081/api/queryUserInfo?userId=aaa
     */
    @RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
    public UserInfo queryUserInfo(@RequestParam String userId) {
        logger.info("查询用户信息,userId:{}", userId);
        return new UserInfo("虫虫:" + userId, 19, "天津市东丽区万科赏溪苑14-0000");
    }

}
  • The following implementations of various monitoring codes will focus on monitoring UserController#queryUserInfo method, and see how various technologies operate.

Three, use AOP to do a face-to-face monitoring

1. Engineering structure

cn-bugstack-middleware-aop
└── src
    ├── main
    │   └── java
    │       ├── cn.bugstack.middleware.monitor
    │       │   ├── annotation
    │       │   │   └── DoMonitor.java
    │       │   ├── config
    │       │   │   └── MonitorAutoConfigure.java
    │       │   └── DoJoinPoint.java
    │       └── resources
    │           └── META-INF 
    │               └── spring.factories
    └── test
        └── java
            └── cn.bugstack.middleware.monitor.test
                └── ApiTest.java

Based on the monitoring system implemented by AOP, the above projects of the core logic are not complicated. The core point is the understanding and application of the aspects, and some configuration items need to be developed in accordance with the implementation in SpringBoot.

  • DoMonitor is a custom annotation. Its function is to add this annotation and configure the necessary information on the method monitoring interface that needs to be used.
  • MonitorAutoConfigure, under configuration, can use SpringBoot yml files, and can handle some Bean initialization operations.
  • DoJoinPoint is the core part of the entire middleware. It is responsible for intercepting and logical processing of all methods that add custom annotations.

2. Define monitoring notes

cn.bugstack.middleware.monitor.annotation.DoMonitor

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoMonitor {

   String key() default "";
   String desc() default "";

}
  • @Retention(RetentionPolicy.RUNTIME),Annotations are to be recorded in the class file by the compiler and retained by the VM at run time, so they may be read reflectively.
  • @Retention is the annotation of the annotation, also known as the meta annotation. There is an RetentionPolicy.RUNTIME parameter information in this annotation 060f77faed741b. There is such a description in its annotation: Annotations are to be recorded in the class file by the compiler and retained by the VM at run time, so they may be read reflectively. actually said that this annotation is added, and its information will be brought to the JVM runtime. When you call the method, you can get it through reflection To the annotation information. In addition, RetentionPolicy also has two attributes SOURCE and CLASS . In fact, these three enumerations officially correspond to the loading and running order of Java code, Java source code file -> .class file -> memory bytecode. And the latter range is larger than the former, so under normal circumstances only need to use RetentionPolicy.RUNTIME.
  • @Target is also a meta-annotation that serves as a mark, and its annotation name is its meaning, target , that is, our custom annotation DoWhiteList should be placed on a class, interface or method. ElementType in JDK1.8 provides a total of 10 target enumerations, TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, TYPE_PARAMETER, TYPE_USE, you can refer to your own custom annotation scope to set
  • The custom annotation @DoMonitor provides the key and desc description of the monitoring. This mainly records the unique value configuration of your monitoring method and the text description of the monitoring method.

3. Defining aspect interception

cn.bugstack.middleware.monitor.DoJoinPoint

@Aspect
public class DoJoinPoint {

    @Pointcut("@annotation(cn.bugstack.middleware.monitor.annotation.DoMonitor)")
    public void aopPoint() {
    }

    @Around("aopPoint() && @annotation(doMonitor)")
    public Object doRouter(ProceedingJoinPoint jp, DoMonitor doMonitor) throws Throwable {
        long start = System.currentTimeMillis();
        Method method = getMethod(jp);
        try {
            return jp.proceed();
        } finally {
            System.out.println("监控 - Begin By AOP");
            System.out.println("监控索引:" + doMonitor.key());
            System.out.println("监控描述:" + doMonitor.desc());
            System.out.println("方法名称:" + method.getName());
            System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms");
            System.out.println("监控 - End\r\n");
        }
    }

    private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
        Signature sig = jp.getSignature();
        MethodSignature methodSignature = (MethodSignature) sig;
        return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
    }

}
  • Use the annotation @Aspect to define the aspect class. This is a very commonly used aspect definition method.
  • @Pointcut("@annotation(cn.bugstack.middleware.monitor.annotation.DoMonitor)") defines the point of contact. There are many ways to find pointcuts in Pointcut, such as those with specified method names, those with range filter expressions, and those with custom annotation methods. Generally in middleware development, custom annotation methods are used more, because it can be more flexibly applied to various business systems.
  • @Around("aopPoint() && @annotation(doMonitor)") can be understood as an enhanced weaving action for the method. The effect of this annotation is that when you call a method that has been added with a custom annotation @DoMonitor, you will first enter the point-point enhancement method. Then you can do some operation actions on the method at this time, for example, we need to do some method monitoring and log printing.
  • Finally, in the doRouter method body, the method execution jp.proceed(); with try finally , and the relevant monitoring information is printed. The acquisition of these monitoring information can finally be sent to the server in the form of asynchronous messages, and then the server will process the monitoring data and display it on the monitoring page.

4. Initialize the aspect class

cn.bugstack.middleware.monitor.config.MonitorAutoConfigure

@Configuration
public class MonitorAutoConfigure {

    @Bean
    @ConditionalOnMissingBean
    public DoJoinPoint point(){
        return new DoJoinPoint();
    }

}
  • @Configuration can be regarded as a component annotation, which can be loaded to create a Bean file when SpringBoot starts. because the @Configuration annotation has an @Component annotation
  • MonitorAutoConfigure can process custom configuration information in yml, and can also be used to initialize Bean objects. For example, here we instantiate the DoJoinPoint aspect object.

5. Run the test

5.1 Introducing POM configuration

<!-- 监控方式:AOP -->
<dependency>
    <groupId>cn.bugstack.middleware</groupId>
    <artifactId>cn-bugstack-middleware-aop</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

5.2 Method configuration monitoring registration

@DoMonitor(key = "cn.bugstack.middleware.UserController.queryUserInfo", desc = "查询用户信息")
@RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
public UserInfo queryUserInfo(@RequestParam String userId) {
    logger.info("查询用户信息,userId:{}", userId);
    return new UserInfo("虫虫:" + userId, 19, "天津市东丽区万科赏溪苑14-0000");
}
  • After importing your own developed components through POM, you can obtain monitoring information through custom annotations and intercept methods.

5.3 Test results

2021-07-04 23:21:10.710  INFO 19376 --- [nio-8081-exec-1] c.b.m.test.interfaces.UserController     : 查询用户信息,userId:aaa
监控 - Begin By AOP
监控索引:cn.bugstack.middleware.UserController.queryUserInfo
监控描述:查询用户信息
方法名称:queryUserInfo
方法耗时:6ms
监控 - End
  • By starting the SpringBoot program and opening the URL address: http://localhost:8081/api/queryUserInfo?userId=aaa in the web page, you can see that the monitoring information can be printed to the console.
  • This configuration method through custom annotations can solve certain hard-coded work, but if a large number of annotations are added to the method, it also requires certain development work.

Next we start to introduce the non-invasive way of using bytecode instrumentation for system monitoring. There are three commonly used components for bytecode instrumentation, including: ASM, Javassit, Byte-Buddy, and then we will introduce them respectively. How to use it.

Four, ASM

ASM is a Java bytecode manipulation framework. It can be used to dynamically generate classes or enhance the functionality of existing classes. ASM can directly generate binary class files, or dynamically change class behavior before the class is loaded into the Java virtual machine. Java classes are stored in strictly formatted .class files. These class files have enough metadata to parse all the elements in the class: class names, methods, attributes, and Java bytecode (instructions). After ASM reads the information from the class file, it can change the class behavior, analyze the class information, and even generate new classes according to user requirements.

1. Let's have a test

cn.bugstack.middleware.monitor.test.ApiTest

private static byte[] generate() {
    ClassWriter classWriter = new ClassWriter(0);
    // 定义对象头;版本号、修饰符、全类名、签名、父类、实现的接口
    classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "cn/bugstack/demo/asm/AsmHelloWorld", null, "java/lang/Object", null);
    // 添加方法;修饰符、方法名、描述符、签名、异常
    MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
    // 执行指令;获取静态属性
    methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    // 加载常量 load constant
    methodVisitor.visitLdcInsn("Hello World ASM!");
    // 调用方法
    methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    // 返回
    methodVisitor.visitInsn(Opcodes.RETURN);
    // 设置操作数栈的深度和局部变量的大小
    methodVisitor.visitMaxs(2, 1);
    // 方法结束
    methodVisitor.visitEnd();
    // 类完成
    classWriter.visitEnd();
    // 生成字节数组
    return classWriter.toByteArray();
}
  • The above code is based on HelloWorld written by ASM. The whole process includes: defining a class to generate ClassWriter, setting the version, modifiers, full class name, signature, parent class, and implemented interface, which is actually that sentence; public class HelloWorld
  • Type descriptor:

    | Java Type | Type Descriptor |
    | :--------- | :------------------- |
    | boolean | Z |
    | char | C |
    | byte | B |
    | short | S |
    | int | I |
    | float | F |
    | long | J |
    | double | D |
    | Object | Ljava/lang/Object; |
    | int[] | [I |
    | Object[][] | [[Ljava/lang/Object; |

  • Method descriptor:

    | Method declaration in source file | Method descriptor |
    | :----------------------- | :---------------------- |
    | void m(int i, float f) | (IF)V |
    | int m(Object o) | (Ljava/lang/Object;)I |
    | int[] m(int i, String s) | (ILjava/lang/String;)[I |
    | Object m(int[] i) | ([I)Ljava/lang/Object; |

  • Execute instructions; get static properties. Mainly to get System.out
  • Load constant load constant, output our HelloWorld methodVisitor.visitLdcInsn("Hello World");
  • The last is to call the output method and set the null return, and at the end to set the depth of the operand stack and the size of the local variables.
  • Isn't it interesting to output a HelloWorld like this, although you may think it is too difficult to code, and it is also very difficult to understand. However, you can install an ASM plug-in ASM Bytecode Outline in IDEA, which is more convenient to view how a common code is handled in the way of using ASM.
  • In addition, the test result of the above code is mainly to generate a class file and output the result of Hello World ASM!

2. Monitoring design engineering structure

cn-bugstack-middleware-asm
└── src
    ├── main
    │   ├── java
    │   │   └── cn.bugstack.middleware.monitor
    │   │       ├── config
    │   │       │   ├── MethodInfo.java
    │   │       │   └── ProfilingFilter.java
    │   │       ├── probe
    │   │       │   ├── ProfilingAspect.java
    │   │       │   ├── ProfilingClassAdapter.java
    │   │       │   ├── ProfilingMethodVisitor.java
    │   │       │   └── ProfilingTransformer.java
    │   │       └── PreMain.java
    │   └── resources    
    │       └── META_INF
    │           └── MANIFEST.MF
    └── test
        └── java
            └── cn.bugstack.middleware.monitor.test
                └── ApiTest.java

The above project structure uses the ASM framework to enhance the system method, which is equivalent to completing the monitoring information before and after the hard-coded writing method through the framework. However, this process is transferred to Javaagent#premain when the Java program is started.

  • MethodInfo is the definition of a method, mainly describing the class name, method name, description, input parameter, and output parameter information.
  • ProfilingFilter is the monitoring configuration information, mainly to filter some methods that do not require bytecode enhancement operations, such as main, hashCode, javax/ etc.
  • ProfilingAspect, ProfilingClassAdapter, ProfilingMethodVisitor, ProfilingTransformer, these four classes are mainly used to complete bytecode insertion operations and output monitoring results.
  • PreMain provides the entry point of Javaagent. JVM first tries to call the premain method on the agent class.
  • MANIFEST.MF is the configuration information, mainly to find Premain-Class Premain-Class: cn.bugstack.middleware.monitor.PreMain

3. Monitoring entry

cn.bugstack.middleware.monitor.PreMain

public class PreMain {

    //JVM 首先尝试在代理类上调用以下方法
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new ProfilingTransformer());
    }

    //如果代理类没有实现上面的方法,那么 JVM 将尝试调用该方法
    public static void premain(String agentArgs) {
    }

}
  • This is the fixed entry method class of Javaagent technology, and the path of this class needs to be configured in MANIFEST.MF.

4. Bytecode method processing

cn.bugstack.middleware.monitor.probe.ProfilingTransformer

public class ProfilingTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            if (ProfilingFilter.isNotNeedInject(className)) {
                return classfileBuffer;
            }
            return getBytes(loader, className, classfileBuffer);
        } catch (Throwable e) {
            System.out.println(e.getMessage());
        }
        return classfileBuffer;
    }

    private byte[] getBytes(ClassLoader loader, String className, byte[] classfileBuffer) {
        ClassReader cr = new ClassReader(classfileBuffer);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        ClassVisitor cv = new ProfilingClassAdapter(cw, className);
        cr.accept(cv, ClassReader.EXPAND_FRAMES);
        return cw.toByteArray();
    }

}
  • Use ASM core classes ClassReader, ClassWriter, and ClassVisitor to process incoming class loaders, class names, bytecodes, etc., and be responsible for bytecode enhancement operations.
  • Here are mainly about ASM operation classes, ClassReader, ClassWriter, ClassVisitor, articles about bytecode programming: ASM, Javassist, Byte-bu series of articles

5. Bytecode method analysis

cn.bugstack.middleware.monitor.probe.ProfilingMethodVisitor

public class ProfilingMethodVisitor extends AdviceAdapter {

    private List<String> parameterTypeList = new ArrayList<>();
    private int parameterTypeCount = 0;     // 参数个数
    private int startTimeIdentifier;        // 启动时间标记
    private int parameterIdentifier;        // 入参内容标记
    private int methodId = -1;              // 方法全局唯一标记
    private int currentLocal = 0;           // 当前局部变量值
    private final boolean isStaticMethod;   // true;静态方法,false;非静态方法
    private final String className;

    protected ProfilingMethodVisitor(int access, String methodName, String desc, MethodVisitor mv, String className, String fullClassName, String simpleClassName) {
        super(ASM5, mv, access, methodName, desc);
        this.className = className;
        // 判断是否为静态方法,非静态方法中局部变量第一个值是this,静态方法是第一个入参参数
        isStaticMethod = 0 != (access & ACC_STATIC);
        //(String var1,Object var2,String var3,int var4,long var5,int[] var6,Object[][] var7,Req var8)=="(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;IJ[I[[Ljava/lang/Object;Lorg/itstack/test/Req;)V"
        Matcher matcher = Pattern.compile("(L.*?;|\\[{0,2}L.*?;|[ZCBSIFJD]|\\[{0,2}[ZCBSIFJD]{1})").matcher(desc.substring(0, desc.lastIndexOf(')') + 1));
        while (matcher.find()) {
            parameterTypeList.add(matcher.group(1));
        }
        parameterTypeCount = parameterTypeList.size();
        methodId = ProfilingAspect.generateMethodId(new MethodInfo(fullClassName, simpleClassName, methodName, desc, parameterTypeList, desc.substring(desc.lastIndexOf(')') + 1)));
    }     

    //... 一些字节码插桩操作 
}
  • When the program starts and loads, every method of every class will be monitored. The name of the class, the name of the method, the description of the method input and output parameters, etc., can all be obtained here.
  • In order to prevent the loss of performance by passing parameters (method information) every time in the follow-up monitoring and processing, generally a global anti-weight id is produced for each method, and the corresponding method can be queried through this id
  • (II)Ljava/lang/String; and output parameters of the method are described as a specified code, 060f77faed82bc. In order for us to analyze the parameters later, we need to disassemble this string.

6. Run the test

6.1 Configure VM parameters Javaagent

-javaagent:E:\itstack\git\github.com\MonitorDesign\cn-bugstack-middleware-asm\target\cn-bugstack-middleware-asm.jar
  • IDEA is configured to VM options when it is running, and the jar package address is configured according to its own path.

6.2 Test results

监控 - Begin By ASM
方法:cn.bugstack.middleware.test.interfaces.UserController$$EnhancerBySpringCGLIB$$8f5a18ca.queryUserInfo
入参:null 入参类型:["Ljava/lang/String;"] 入数[值]:["aaa"]
出参:Lcn/bugstack/middleware/test/interfaces/dto/UserInfo; 出参[值]:{"address":"天津市东丽区万科赏溪苑14-0000","age":19,"code":"0000","info":"success","name":"虫虫:aaa"}
耗时:54(s)
监控 - End
  • It can be seen from the running test results that after using ASM monitoring, there is no need to hard-code or AOP to operate in the code. At the same time, more complete method execution information can be monitored, including input parameter type, input parameter value and output parameter information, and output parameter value.
  • However, you may find that ASM is still very troublesome to operate, especially in some very complex coding logic, you may encounter various problems, so next we will introduce some components developed based on ASM, which are also The same function can be achieved.

Five, Javassist

Javassist is an open source library for analyzing, editing and creating Java bytecode. It was created by Shigeru Chiba (千叶梓) from the Department of Mathematics and Computer Science at Tokyo Institute of Technology. It has joined the open source JBoss application server project, and implements a dynamic "AOP" framework for JBoss by using Javassist to manipulate bytecodes.

1. Let's have a test

cn.bugstack.middleware.monitor.test.ApiTest

public class ApiTest {

    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();

        CtClass ctClass = pool.makeClass("cn.bugstack.middleware.javassist.MathUtil");

        // 属性字段
        CtField ctField = new CtField(CtClass.doubleType, "π", ctClass);
        ctField.setModifiers(Modifier.PRIVATE + Modifier.STATIC + Modifier.FINAL);
        ctClass.addField(ctField, "3.14");

        // 方法:求圆面积
        CtMethod calculateCircularArea = new CtMethod(CtClass.doubleType, "calculateCircularArea", new CtClass[]{CtClass.doubleType}, ctClass);
        calculateCircularArea.setModifiers(Modifier.PUBLIC);
        calculateCircularArea.setBody("{return π * $1 * $1;}");
        ctClass.addMethod(calculateCircularArea);

        // 方法;两数之和
        CtMethod sumOfTwoNumbers = new CtMethod(pool.get(Double.class.getName()), "sumOfTwoNumbers", new CtClass[]{CtClass.doubleType, CtClass.doubleType}, ctClass);
        sumOfTwoNumbers.setModifiers(Modifier.PUBLIC);
        sumOfTwoNumbers.setBody("{return Double.valueOf($1 + $2);}");
        ctClass.addMethod(sumOfTwoNumbers);
        // 输出类的内容
        ctClass.writeFile();

        // 测试调用
        Class clazz = ctClass.toClass();
        Object obj = clazz.newInstance();

        Method method_calculateCircularArea = clazz.getDeclaredMethod("calculateCircularArea", double.class);
        Object obj_01 = method_calculateCircularArea.invoke(obj, 1.23);
        System.out.println("圆面积:" + obj_01);

        Method method_sumOfTwoNumbers = clazz.getDeclaredMethod("sumOfTwoNumbers", double.class, double.class);
        Object obj_02 = method_sumOfTwoNumbers.invoke(obj, 1, 2);
        System.out.println("两数和:" + obj_02);
    }

}
  • This is a process of using Javassist to generate the circle area and abstract classes and methods and running the results. It can be seen that Javassist mainly uses methods such as ClassPool, CtClass, CtField, and CtMethod.
  • The test results mainly include the generation of a class cn.bugstack.middleware.javassist.MathUtil under the specified path, and the result will be output on the console.

generated class

public class MathUtil {
  private static final double π = 3.14D;

  public double calculateCircularArea(double var1) {
      return 3.14D * var1 * var1;
  }

  public Double sumOfTwoNumbers(double var1, double var3) {
      return var1 + var3;
  }

  public MathUtil() {
  }
}

test result

圆面积:4.750506
两数和:3.0

Process finished with exit code 0

2. Monitoring design engineering structure

cn-bugstack-middleware-javassist
└── src
    ├── main
    │   ├── java
    │   │   └── cn.bugstack.middleware.monitor
    │   │       ├── config
    │   │       │   └── MethodDescription.java
    │   │       ├── probe
    │   │       │   ├── Monitor.java
    │   │       │   └── MyMonitorTransformer.java
    │   │       └── PreMain.java
    │   └── resources
    │       └── META_INF
    │           └── MANIFEST.MF
    └── test
        └── java
            └── cn.bugstack.middleware.monitor.test
                └── ApiTest.java
  • The whole monitoring framework implemented using javassist looks very similar to the structure of ASM, but most of the work of manipulating bytecode is handed over to the javassist framework, so the entire code structure looks simpler.

3. Monitoring method plugging

cn.bugstack.middleware.monitor.probe.MyMonitorTransformer

public class MyMonitorTransformer implements ClassFileTransformer {

    private static final Set<String> classNameSet = new HashSet<>();

    static {
        classNameSet.add("cn.bugstack.middleware.test.interfaces.UserController");
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        try {
            String currentClassName = className.replaceAll("/", ".");
            if (!classNameSet.contains(currentClassName)) { // 提升classNameSet中含有的类
                return null;
            }

            // 获取类
            CtClass ctClass = ClassPool.getDefault().get(currentClassName);
            String clazzName = ctClass.getName();

            // 获取方法
            CtMethod ctMethod = ctClass.getDeclaredMethod("queryUserInfo");
            String methodName = ctMethod.getName();

            // 方法信息:methodInfo.getDescriptor();
            MethodInfo methodInfo = ctMethod.getMethodInfo();

            // 方法:入参信息
            CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
            LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
            CtClass[] parameterTypes = ctMethod.getParameterTypes();

            boolean isStatic = (methodInfo.getAccessFlags() & AccessFlag.STATIC) != 0;  // 判断是否为静态方法
            int parameterSize = isStatic ? attr.tableLength() : attr.tableLength() - 1; // 静态类型取值
            List<String> parameterNameList = new ArrayList<>(parameterSize);            // 入参名称
            List<String> parameterTypeList = new ArrayList<>(parameterSize);            // 入参类型
            StringBuilder parameters = new StringBuilder();                             // 参数组装;$1、$2...,$$可以获取全部,但是不能放到数组初始化

            for (int i = 0; i < parameterSize; i++) {
                parameterNameList.add(attr.variableName(i + (isStatic ? 0 : 1))); // 静态类型去掉第一个this参数
                parameterTypeList.add(parameterTypes[i].getName());
                if (i + 1 == parameterSize) {
                    parameters.append("$").append(i + 1);
                } else {
                    parameters.append("$").append(i + 1).append(",");
                }
            }

            // 方法:出参信息
            CtClass returnType = ctMethod.getReturnType();
            String returnTypeName = returnType.getName();

            // 方法:生成方法唯一标识ID
            int idx = Monitor.generateMethodId(clazzName, methodName, parameterNameList, parameterTypeList, returnTypeName);

            // 定义属性
            ctMethod.addLocalVariable("startNanos", CtClass.longType);
            ctMethod.addLocalVariable("parameterValues", ClassPool.getDefault().get(Object[].class.getName()));

            // 方法前加强
            ctMethod.insertBefore("{ startNanos = System.nanoTime(); parameterValues = new Object[]{" + parameters.toString() + "}; }");

            // 方法后加强
            ctMethod.insertAfter("{ cn.bugstack.middleware.monitor.probe.Monitor.point(" + idx + ", startNanos, parameterValues, $_);}", false); // 如果返回类型非对象类型,$_ 需要进行类型转换

            // 方法;添加TryCatch
            ctMethod.addCatch("{ cn.bugstack.middleware.monitor.probe.Monitor.point(" + idx + ", $e); throw $e; }", ClassPool.getDefault().get("java.lang.Exception"));   // 添加异常捕获

            return ctClass.toBytecode();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}
  • Compared with the ASM implementation, the overall monitoring method is similar, so only the differences are shown here.
  • Through the operation of Javassist, it is mainly to implement a ClassFileTransformer interface, in which the bytecode is obtained and processed accordingly.
  • The processing process includes: obtaining the class, obtaining the method, obtaining the input parameter information, obtaining the parameter output information, generating a unique ID for the method, and then starting the before and after enhancement operations of the method. This enhancement is to add monitoring code to the method block.
  • Finally, the bytecode information return ctClass.toBytecode(); is returned. Now your newly added bytecode can be loaded and processed by the program.

4. Run the test

4.1 Configure VM parameters Javaagent

-javaagent:E:\itstack\git\github.com\MonitorDesign\cn-bugstack-middleware-javassist\target\cn-bugstack-middleware-javassist.jar
  • IDEA is configured to VM options when it is running, and the jar package address is configured according to its own path.

4.2 Test results

监控 -  Begin By Javassist
方法:cn.bugstack.middleware.test.interfaces.UserController$$EnhancerBySpringCGLIB$$8f5a18ca.queryUserInfo
入参:null 入参类型:["Ljava/lang/String;"] 入数[值]:["aaa"]
出参:Lcn/bugstack/middleware/test/interfaces/dto/UserInfo; 出参[值]:{"address":"天津市东丽区万科赏溪苑14-0000","age":19,"code":"0000","info":"success","name":"虫虫:aaa"}
耗时:46(s)
监控 - End
  • Judging from the test results, the effect of bytecode instrumentation with ASM is the same, and it can monitor system execution information. But such a framework will make the development process simpler and easier to control.

Six, Byte-Buddy

In October 2015, Byte Buddy was awarded the Duke's Choice Award by Oracle. The award commends Byte Buddy for his "great innovation in Java technology". We are very honored to receive this award and thank all users and everyone else who helped Byte Buddy succeed. We really appreciate it!

Byte Buddy is a code generation and manipulation library used to create and modify Java Java application is running, without the help of a compiler. In addition to the code generation utility included with the Java Byte Buddy also allows the creation of any class, and is not limited to implementing the interface used to create a runtime proxy. In addition, Byte Buddy provides a convenient API, you can use the Java proxy or manually change the class during the build process.

  • No need to understand bytecode instructions, you can use simple API to easily manipulate bytecode, control classes and methods.
  • Java 11 is already supported, the library is lightweight, only depends on the visitor API of the Java byte code parser library ASM, and it does not need any other dependencies.
  • Compared with JDK dynamic proxy, cglib, Javassist, Byte Buddy has certain advantages in performance.

1. Let's have a test

cn.bugstack.middleware.monitor.test.ApiTest

public class ApiTest {

    public static void main(String[] args) throws IllegalAccessException, InstantiationException {
        String helloWorld = new ByteBuddy()
                .subclass(Object.class)
                .method(named("toString"))
                .intercept(FixedValue.value("Hello World!"))
                .make()
                .load(ApiTest.class.getClassLoader())
                .getLoaded()
                .newInstance()
                .toString();

        System.out.println(helloWorld);
    }

}
  • This is a use ByteBuddy grammar generation of "Hello World!" Cases, the result is a line of his run, Hello World! , the entire code block core function is through method(named("toString")) , find toString method, and then by intercepting intercept return, set this method value. FixedValue.value("Hello World!") . So far, a basic method is to pass Byte-buddy , and finally load, initialize and call the output.

test result

Hello World!

Process finished with exit code 0

2. Monitoring design engineering structure

cn-bugstack-middleware-bytebuddy
└── src
    ├── main
    │   ├── java
    │   │   └── cn.bugstack.middleware.monitor
    │   │       ├── MonitorMethod
    │   │       └── PreMain.java
    │   └── resources
    │       └── META_INF
    │           └── MANIFEST.MF
    └── test
        └── java
            └── cn.bugstack.middleware.monitor.test
                └── ApiTest.java
  • This is my personal favorite framework because of its ease of operation and can use bytecode enhanced operations like ordinary business codes. As you can see from the current project structure, the number of code classes is decreasing.

3. Monitoring method plugging

cn.bugstack.middleware.monitor.MonitorMethod

public class MonitorMethod {

    @RuntimeType
    public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable, @AllArguments Object[] args) throws Exception {
        long start = System.currentTimeMillis();
        Object resObj = null;
        try {
            resObj = callable.call();
            return resObj;
        } finally {
            System.out.println("监控 - Begin By Byte-buddy");
            System.out.println("方法名称:" + method.getName());
            System.out.println("入参个数:" + method.getParameterCount());
            for (int i = 0; i < method.getParameterCount(); i++) {
                System.out.println("入参 Idx:" + (i + 1) + " 类型:" + method.getParameterTypes()[i].getTypeName() + " 内容:" + args[i]);
            }
            System.out.println("出参类型:" + method.getReturnType().getName());
            System.out.println("出参结果:" + resObj);
            System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms");
            System.out.println("监控 - End\r\n");
        }
    }

}
  • @Origin , used to intercept the original method, so that the relevant information in the method can be obtained.
  • The information in this part is relatively complete, especially the number and type of parameters are also obtained, so that it can be output cyclically during subsequent processing of parameters.

Commonly used annotations

In addition to the above annotations used to obtain method execution information, Byte Buddy also provides many other annotations. as follows;

annotationDescription
@ArgumentBind a single parameter
@AllArgumentsBind an array of all parameters
@ThisThe currently intercepted and dynamically generated object
@SuperThe parent object of the currently intercepted and dynamically generated object
@OriginCan be bound to the following types of parameters: Method The original method called Constructor The original constructor called Class The currently dynamically created class MethodHandle MethodType String The return value of the toString() of the dynamic class int The modifier of the dynamic method
@DefaultCallCall the default method instead of the super method
@SuperCallMethod used to call the parent version
@SuperInject a super-type object, which can be an interface, so as to call any of its methods
@RuntimeTypeCan be used on return values and parameters to prompt ByteBuddy to disable strict type checking
@EmptyThe default value of the type of the injected parameter
@StubValueInject a stub value. For methods that return references and void, inject null; for methods that return primitive types, inject 0
@FieldValueInject the value of a field of the intercepted object
@MorphSimilar to @SuperCall, but allows to specify call parameters

Commonly used core API

  1. ByteBuddy

    • Entry class for streaming API
    • Provide Subclassing/Redefining/Rebasing method to rewrite bytecode
    • All operations rely on DynamicType.Builder to create immutable objects
  2. ElementMatchers(ElementMatcher)

    • Provide a series of element matching tools (named/any/nameEndsWith, etc.)
    • ElementMatcher (provides a way to match types, methods, fields, and annotations, similar to Predicate)
    • Junction performs and/or operations on multiple ElementMatchers
  3. DynamicType

    (Dynamic type, the beginning of all bytecode operations, very worthy of attention)

    • Unloaded (The dynamically created bytecode has not been loaded into the virtual machine and needs to be loaded by the class loader)
    • Loaded (After it has been loaded into the jvm, the Class representation is parsed)
    • Default (the default implementation of DynamicType, complete related practical operations)
  4. `Implementation

    (Used to provide the implementation of dynamic methods)

    • FixedValue (method call returns a fixed value)
    • MethodDelegation (method call delegation, supports two methods: class static method call, object instance method method call)
  5. Builder

    (Used to create DynamicType, related interfaces and implementation to be explained in detail later)

    • MethodDefinition
    • FieldDefinition
    • AbstractBase

4. Configure entry method

cn.bugstack.middleware.monitor.PreMain

public class PreMain {

    //JVM 首先尝试在代理类上调用以下方法
    public static void premain(String agentArgs, Instrumentation inst) {
        AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
            return builder
                    .method(ElementMatchers.named("queryUserInfo")) // 拦截任意方法
                    .intercept(MethodDelegation.to(MonitorMethod.class)); // 委托
        };

        new AgentBuilder
                .Default()
                .type(ElementMatchers.nameStartsWith(agentArgs))  // 指定需要拦截的类 "cn.bugstack.demo.test"
                .transform(transformer)
                .installOn(inst);
    }

    //如果代理类没有实现上面的方法,那么 JVM 将尝试调用该方法
    public static void premain(String agentArgs) {
    }

}
  • The premain method mainly entrusts the use of the implemented MonitorMethod, and also sets the interception method in the method. This interception method can also go to the class path and so on.

5. Run the test

5.1 Configure VM parameters Javaagent

-javaagent:E:\itstack\git\github.com\MonitorDesign\cn-bugstack-middleware-bytebuddy\target\cn-bugstack-middleware-bytebuddy.jar
  • IDEA is configured to VM options when it is running, and the jar package address is configured according to its own path.

5.2 Test results

监控 - Begin By Byte-buddy
方法名称:queryUserInfo
入参个数:1
入参 Idx:1 类型:java.lang.String 内容:aaa
出参类型:cn.bugstack.middleware.test.interfaces.dto.UserInfo
出参结果:cn.bugstack.middleware.test.interfaces.dto.@214b199c
方法耗时:1ms
监控 - End
  • Byte-buddy is the simplest and most convenient one among several bytecode frameworks in our entire testing process, and it is also very easy to expand information. The whole process is as simple as the initial use of AOP, but it satisfies the non-intrusive monitoring needs.
  • So when using the bytecode framework, you can consider choosing to use Byte-buddy, a very useful bytecode framework.

Seven, summary

  • The application of ASM bytecode programming is very wide, but it may not be seen usually because it is used as a supporting service in combination with other frameworks. There are many technologies like this, such as javassit, Cglib, jacoco and so on.
  • Javassist is used very much in some components in full link monitoring. It can use encoding to manipulate bytecode enhancement, or it can be processed like ASM.
  • Byte-buddy is a very convenient framework, and it is currently used more and more widely, and the learning difficulty to get started is also the lowest among several frameworks. In addition to the introduction to the case use in this chapter, you can also go to the official website: https://bytebuddy.net to learn more about Byte Buddy .
  • All the source code of this chapter has been uploaded to GitHub: https://github.com/fuzhengwei/MonitorDesign

8. Series recommendation

阅读 531

CodeGuide | 程序员编码指南
公众号:bugstack虫洞栈,回复:设计模式,可以下载《重学Java设计模式》PDF,全网下载量17万+ | 这是一...

CodeGuide | 程序员编码指南 - 原创文章、案例源码、资料书籍、简历模版等下载。

3.9k 声望
23.1k 粉丝
0 条评论
你知道吗?

CodeGuide | 程序员编码指南 - 原创文章、案例源码、资料书籍、简历模版等下载。

3.9k 声望
23.1k 粉丝
宣传栏