Research byte code piling technology for the design and implementation of Internet distributed system monitoring!

Posted by BlackViperSJM on Tue, 18 Jan 2022 22:49:01 +0100


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!

Why, your online system is streaking?

Late at night sleeping on the weekend, I suddenly received a call from my boss ☎ At your urging. "Quickly look at wechat and wechat. We don't know what's wrong with the system. We have to know it through user feedback!!!" Get up late at night, turn on the computer, connect to VPN, yawn, open your hazy eyes, and check the system log. It turned out that the system hung up. Restart and recover quickly!

Although the restart restored the system, it also reset the boss's distorted expression. But how does the system hang up? Because there is no monitoring system, and I don't know whether it is caused by too much traffic or program problems. Through pieces of logs, I can only roughly estimate some labels and report to the boss. However, the boss is not stupid. He talks around and lets him monitor the operation of all the systems.

Dragging a sleepy head with both hands, I can't think of any good methods for a while and a half. Is it hard coding and time-consuming calculation on each method. After that, the information is collected in a unified way and displayed on a monitoring page, which uses Apache's echarts , don't say if it's displayed like this, it's really good-looking and easy to use.

  • But such hard coding is not called a thing. It doesn't make our department tired of moving bricks! Besides, they must look down on me. What architect, if you want to monitor the system, you have to hard code, isn't it stupid!!!
  • I can't sleep with such a thought. I have to find information and report to the boss tomorrow!

In fact, the stable operation of an online system depends on its operation health, which includes; It is a comprehensive value of various indicators such as adjustment amount, availability, impact duration and server performance. When an abnormal problem occurs in the system, the whole service method execution link can be grabbed and output; Input parameters, output parameters, exception information, etc. Of course, it also includes various performance indicators of JVM, Redis and Mysql to quickly locate and solve problems.

So what are the solutions for such things? In fact, there are many methods, such as;

  1. Simplest and crudest way is to hard code method to collect execution time and exception information. However, such coding cost is too large, and a large number of regression tests are required after hard coding, which may bring some risks to the system. In case someone shakes his hand and makes a mistake in copying and pasting!
  2. It is relatively better to choose the aspect method to make a set of unified monitoring components. But it also needs hard coding, such as writing annotations, and the maintenance cost is not low.
  3. In fact, the market actually has a complete set of non intrusion monitoring schemes for such monitoring, such as; Google Dapper and Zipkin can meet the requirements of the monitoring system. They are non-invasive based on probe technology and adopt bytecode enhancement to collect system operation information for analysis and monitoring operation status.

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

2, Preparatory work

This paper will implement different monitoring implementation codes based on AOP and bytecode framework (ASM, Javassist and byte Buddy). The whole 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 address: https://github.com/fuzhengwei/MonitorDesign
  • Brief introduction: aop, asm, bytebuddy and javassist are four different implementation schemes. Test is a simple test project based on SpringBoot.
  • Technology usage: SpringBoot, asm, byte buddy, javassist

cn-bugstack-middleware-test

@RestController
public class UserController {

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

    /**
     * Test: http://localhost:8081/api/queryUserInfo?userId=aaa
     */
    @RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
    public UserInfo queryUserInfo(@RequestParam String userId) {
        logger.info("Query user information, userId: {}", userId);
        return new UserInfo("stifling:" + userId, 19, "No. 14, Wanke Shangxiyuan, Dongli District, Tianjin-0000");
    }

}
  • The next implementation of various monitoring codes will focus on the method execution information of monitoring UserController#queryUserInfo to see how various technologies operate.

3, Using AOP to do a section 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

For the monitoring system based on AOP, the above project of core logic is not complex. Its core point lies in the understanding and application of aspects, and some configuration items need to be developed according to the implementation method in SpringBoot.

  • DoMonitor is a custom annotation. Its function is to add this annotation and configure the necessary information on the method monitoring interface to be used.
  • MonitorAutoConfigure is configured to use the SpringBoot yml file and handle some Bean initialization operations.
  • DoJoinPoint is the core part of the whole middleware. It is responsible for intercepting and logical processing of all methods adding 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 annotation, also known as meta annotation. There is a parameter information RetentionPolicy in this annotation Runtime has such a description in its comments: 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 In fact, after adding this annotation, its information will be brought to the JVM runtime. When you call a method, you can get the annotation information through reflection. In addition, RetentionPolicy also has two attributes, SOURCE and CLASS. In fact, these three enumerations formally correspond to the loading and running order of Java code. Java SOURCE code file - > CLASS file - > memory bytecode. And the range of the latter is larger than that of the former, so generally, only RetentionPolicy needs to be used Runtime is enough.
  • @Target is also a meta annotation. Its annotation name is its meaning. The target, that is, our custom annotation DoWhiteList, should be placed on classes, interfaces or methods. At jdk1 ElementType in 8 provides 10 target enumerations, including type, FIELD, METHOD, parameter, CONSTRUCTOR and LOCAL_VARIABLE,ANNOTATION_TYPE,PACKAGE,TYPE_PARAMETER,TYPE_USE, you can refer to your own custom annotation scope for setting
  • The custom annotation @ DoMonitor provides the key and desc description of monitoring, which mainly records the unique value configuration of your monitoring method and the text description of the monitoring method.

3. Define section 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("monitor - Begin By AOP");
            System.out.println("Monitoring index:" + doMonitor.key());
            System.out.println("Monitoring Description:" + doMonitor.desc());
            System.out.println("Method name:" + method.getName());
            System.out.println("Method time consuming:" + (System.currentTimeMillis() - start) + "ms");
            System.out.println("monitor - 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 facet class. This is a very common way to define sections.
  • @Pointcut("@annotation(cn.bugstack.middleware.monitor.annotation.DoMonitor)") defines the tangent point. There are many ways to find pointcuts in pointcut, including specifying method names, range filtering expressions, and custom annotations. Generally, in middleware development, user-defined annotation is used more, because it can be more flexibly applied to various business systems.
  • @Around ("aoppoint() & & @ annotation (DoMonitor)") can be understood as a method enhanced weaving action. The effect of this annotation is that when you call a method with a custom annotation @ DoMonitor, you will first enter the pointcut enhanced method. Then at this time, you can do some operations on methods, such as method monitoring and log printing.
  • Finally, get the jp proceed(); Use try finally to wrap it and print relevant monitoring information. Finally, the acquisition of these monitoring information can be sent to the server through asynchronous messages, and then processed by the server. The monitoring data and processing are displayed on the monitoring page.

4. Initialize facet class

cn.bugstack.middleware.monitor.config.MonitorAutoConfigure

@Configuration
public class MonitorAutoConfigure {

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

}
  • @Configuration, which can be regarded as a component annotation, can be loaded to create Bean files when SpringBoot starts. Because the @ configuration annotation has a @ Component annotation
  • MonitorAutoConfigure can handle the customized configuration information in yml, and can also be used to initialize Bean objects. For example, here we instantiate the DoJoinPoint facet object.

5. Operation test

5.1 introduction of POM configuration

<!-- Monitoring mode: 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 = "Query user information")
@RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
public UserInfo queryUserInfo(@RequestParam String userId) {
    logger.info("Query user information, userId: {}", userId);
    return new UserInfo("stifling:" + userId, 19, "No. 14, Wanke Shangxiyuan, Dongli District, Tianjin-0000");
}
  • After introducing your own developed components through POM, you can obtain monitoring information through customized annotations and interception methods.

5.3 test results

2021-07-04 23:21:10.710  INFO 19376 --- [nio-8081-exec-1] c.b.m.test.interfaces.UserController     : Query user information, userId: aaa
 monitor - Begin By AOP
 Monitoring index: cn.bugstack.middleware.UserController.queryUserInfo
 Monitoring Description: query user information
 Method name: queryUserInfo
 Method time: 6 ms
 monitor - End
  • By starting the SpringBoot program, open the URL address in the web page: http://localhost:8081/api/queryUserInfo?userId=aaa , you can see that the monitoring information can be printed to the console.
  • This configuration method of user-defined annotations can solve some hard coding work, but if a large number of annotations are added to the method, it also needs some development work.

Next, we will introduce the non-invasive method of using bytecode plug-in for system monitoring. There are three common components of bytecode plug-in, including ASM, Javassit and byte buddy. Next, we will introduce how they are used.

4, 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 the class behavior before the class is loaded into the Java virtual machine. Java class es are stored in a strictly defined format In the class file, these class files have enough metadata to resolve all the elements in the class: class name, method, attribute and Java bytecode (instruction). After reading information from class files, ASM can change class behavior, analyze class information, and even generate new classes according to user requirements.

1. Let's have a test first

cn.bugstack.middleware.monitor.test.ApiTest

private static byte[] generate() {
    ClassWriter classWriter = new ClassWriter(0);
    // Define the object header; Version number, modifier, full class name, signature, parent class, implemented interface
    classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "cn/bugstack/demo/asm/AsmHelloWorld", null, "java/lang/Object", null);
    // Add method; Modifier, method name, descriptor, signature, exception
    MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
    // Execute instructions; Get static properties
    methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    // load constant
    methodVisitor.visitLdcInsn("Hello World ASM!");
    // Call method
    methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    // return
    methodVisitor.visitInsn(Opcodes.RETURN);
    // Sets the depth of the operand stack and the size of the local variable
    methodVisitor.visitMaxs(2, 1);
    // Method end
    methodVisitor.visitEnd();
    // Class completion
    classWriter.visitEnd();
    // Generate byte array
    return classWriter.toByteArray();
}
  • The above code is HelloWorld written based on ASM. The whole process includes: defining the generated ClassWriter, setting the version, modifier, full class name, signature, parent class and implemented interface of a class, which is actually that sentence; public class HelloWorld

  • Type descriptor:

    Java typetype descriptor
    booleanZ
    charC
    byteB
    shortS
    intI
    floatF
    longJ
    doubleD
    ObjectLjava/lang/Object;
    int[][I
    Object[][][[Ljava/lang/Object;
  • Method descriptor:

    Method declaration in source fileMethod 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 obtain system out

  • Load the constant load constant and output our HelloWorld methodvisitor visitLdcInsn("Hello World");

  • Finally, call the output method and set the null return. At the end, set the depth of the operand stack and the size of the local variable.

  • It's interesting to output a HelloWorld in this way, although you may think it's too difficult to encode and understand. However, you can install a plug-in ASM Bytecode Outline in the IDEA, which makes it easier to see how a common code should be handled in the way of using ASM.

  • In addition, the test results of the above code are mainly to generate a class file and output Hello World ASM! result.

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 engineering structure uses ASM framework to enhance the system method, that is, it is equivalent to completing the monitoring information before and after the hard coding writing method through the framework. However, this process is transferred to the Java agent #premain for processing when the Java program starts.

  • MethodInfo is the definition of a method, which mainly describes the class name, method name, description, input parameter and output parameter information.
  • The profiling filter is used to monitor the configuration information. It is mainly used to filter some methods that do not require bytecode enhancement operations, such as main, hashCode, javax /
  • ProfilingAspect, ProfilingClassAdapter, ProfilingMethodVisitor and ProfilingTransformer are mainly used to complete bytecode insertion and output monitoring results.
  • premain provides an entry to the Java agent. The JVM first attempts to call the premain method on the agent class.
  • MANIFEST.MF is configuration information, mainly to find premain class premain class: CN bugstack. middleware. monitor. PreMain

3. Monitoring entrance

cn.bugstack.middleware.monitor.PreMain

public class PreMain {

    //The JVM first attempts to call the following methods on the proxy class
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new ProfilingTransformer());
    }

    //If the proxy class does not implement the above method, the JVM will try to call the method
    public static void premain(String agentArgs) {
    }

}
  • This is the fixed entry method class of Javaagent technology. At the same time, the path of this class needs to be configured to manifest In MF.

4. Bytecode processing method

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 handle the incoming class loader, class name and bytecode, and be responsible for the enhancement of bytecode.
  • Here are mainly about ASM operation classes, such as ClassReader, ClassWriter and ClassVisitor. Articles on bytecode programming: ASM, Javassist, byte Bu series

5. Byte code method analysis

cn.bugstack.middleware.monitor.probe.ProfilingMethodVisitor

public class ProfilingMethodVisitor extends AdviceAdapter {

    private List<String> parameterTypeList = new ArrayList<>();
    private int parameterTypeCount = 0;     // Number of parameters
    private int startTimeIdentifier;        // Start time stamp
    private int parameterIdentifier;        // Input content tag
    private int methodId = -1;              // Method globally unique tag
    private int currentLocal = 0;           // Current local variable value
    private final boolean isStaticMethod;   // trueοΌ› Static method, false; Non static method
    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;
        // Judge whether it is a static method. The first value of the local variable in the non static method is this, and the static method is the first input parameter
        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)));
    }     

    //...  Some bytecode instrumentation operations 
}
  • When the program starts loading, each method of each class will be monitored. The name of the class, the name of the method, and the description of the method input and output parameters can be obtained here.
  • In order to avoid wasting performance by transmitting parameters (method information) every time in subsequent monitoring and processing, a global anti duplication id is generally produced for each method, and the corresponding method can be queried through this id.
  • In addition, it can be seen from here that the input and output parameters of the method are described as a specified code, (II)Ljava/lang/String, In order to parse the parameters later, we need to disassemble this string.

6. Operation test

6.1 configuring VM parameters Javaagent

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

6.2 test results

monitor - Begin By ASM
 method: cn.bugstack.middleware.test.interfaces.UserController$$EnhancerBySpringCGLIB$$8f5a18ca.queryUserInfo
 Input: null Input parameter type:["Ljava/lang/String;"] Input number[value]: ["aaa"]
Output reference: Lcn/bugstack/middleware/test/interfaces/dto/UserInfo; Out parameter[value]: {"address":"No. 14, Wanke Shangxiyuan, Dongli District, Tianjin-0000","age":19,"code":"0000","info":"success","name":"stifling:aaa"}
Time consuming: 54(s)
monitor - End
  • From the running test results, it can be seen that after ASM monitoring, there is no need to hard code or operate in the code in the way of AOP. At the same time, more complete method execution information can be monitored, including input parameter type, input parameter value, output parameter information and output parameter value.
  • However, you may find that ASM is still troublesome to operate, especially in some complex coding logic, you may encounter various problems. Therefore, next, we will introduce some components developed based on ASM, which can also realize the same functions.

5, Javassist

Javassist is an open source class library for analyzing, editing and creating Java bytecode. It was founded by Shigeru Chiba of the Department of mathematics and computer science of Tokyo University of technology. It has joined the open source JBoss application server project to implement a dynamic "AOP" framework for JBoss by using javassist to operate on bytecode.

1. Let's have a test first

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");

        // attribute field
        CtField ctField = new CtField(CtClass.doubleType, "Ο€", ctClass);
        ctField.setModifiers(Modifier.PRIVATE + Modifier.STATIC + Modifier.FINAL);
        ctClass.addField(ctField, "3.14");

        // Method: calculate the circle area
        CtMethod calculateCircularArea = new CtMethod(CtClass.doubleType, "calculateCircularArea", new CtClass[]{CtClass.doubleType}, ctClass);
        calculateCircularArea.setModifiers(Modifier.PUBLIC);
        calculateCircularArea.setBody("{return Ο€ * $1 * $1;}");
        ctClass.addMethod(calculateCircularArea);

        // method; Sum of two numbers
        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);
        // Output the contents of the class
        ctClass.writeFile();

        // Test call
        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("Circular area:" + obj_01);

        Method method_sumOfTwoNumbers = clazz.getDeclaredMethod("sumOfTwoNumbers", double.class, double.class);
        Object obj_02 = method_sumOfTwoNumbers.invoke(obj, 1, 2);
        System.out.println("Sum of two numbers:" + obj_02);
    }

}
  • This is a process of calculating the circle area and abstract classes and methods generated by Javassist and running the results. You can see that Javassist is mainly used by ClassPool, CtClass, CtField, CtMethod and other methods.
  • The test results mainly include the generation of a class CN. Under the specified path bugstack. middleware. javassist. Mathutil, and the results 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

Circle area: 4.750506
 Sum of two numbers: 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 is very similar to the structure of ASM, but most of the operation of bytecode is handled by javassist framework, so the whole code structure looks simpler.

3. Monitoring method: pile insertion

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)) { // Promote the classes contained in the classNameSet
                return null;
            }

            // Get class
            CtClass ctClass = ClassPool.getDefault().get(currentClassName);
            String clazzName = ctClass.getName();

            // Acquisition method
            CtMethod ctMethod = ctClass.getDeclaredMethod("queryUserInfo");
            String methodName = ctMethod.getName();

            // Method information: methodinfo getDescriptor();
            MethodInfo methodInfo = ctMethod.getMethodInfo();

            // Method: input parameter information
            CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
            LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
            CtClass[] parameterTypes = ctMethod.getParameterTypes();

            boolean isStatic = (methodInfo.getAccessFlags() & AccessFlag.STATIC) != 0;  // Determine whether it is a static method
            int parameterSize = isStatic ? attr.tableLength() : attr.tableLength() - 1; // Static type value
            List<String> parameterNameList = new ArrayList<>(parameterSize);            // Input parameter name
            List<String> parameterTypeList = new ArrayList<>(parameterSize);            // Input parameter type
            StringBuilder parameters = new StringBuilder();                             // Parameter assembly$ 1,$2...,$$ You can get all of them, but you can't put them into array initialization

            for (int i = 0; i < parameterSize; i++) {
                parameterNameList.add(attr.variableName(i + (isStatic ? 0 : 1))); // Remove the first this parameter from the static type
                parameterTypeList.add(parameterTypes[i].getName());
                if (i + 1 == parameterSize) {
                    parameters.append("$").append(i + 1);
                } else {
                    parameters.append("$").append(i + 1).append(",");
                }
            }

            // Method: output parameter information
            CtClass returnType = ctMethod.getReturnType();
            String returnTypeName = returnType.getName();

            // Method: generate method unique ID
            int idx = Monitor.generateMethodId(clazzName, methodName, parameterNameList, parameterTypeList, returnTypeName);

            // Define properties
            ctMethod.addLocalVariable("startNanos", CtClass.longType);
            ctMethod.addLocalVariable("parameterValues", ClassPool.getDefault().get(Object[].class.getName()));

            // Pre method strengthening
            ctMethod.insertBefore("{ startNanos = System.nanoTime(); parameterValues = new Object[]{" + parameters.toString() + "}; }");

            // Post method strengthening
            ctMethod.insertAfter("{ cn.bugstack.middleware.monitor.probe.Monitor.point(" + idx + ", startNanos, parameterValues, $_);}", false); // If the return type is not an object type$_ Type conversion required

            // method; Add TryCatch
            ctMethod.addCatch("{ cn.bugstack.middleware.monitor.probe.Monitor.point(" + idx + ", $e); throw $e; }", ClassPool.getDefault().get("java.lang.Exception"));   // Add exception capture

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

}
  • Compared with ASM implementation, the overall monitoring methods are similar, so only the differences are shown here.
  • Through the operation of Javassist, it mainly implements a transform method of ClassFileTransformer interface, in which bytecode is obtained and processed accordingly.
  • The processing process includes: obtaining classes, obtaining methods, obtaining input parameter information, obtaining output parameter information, generating a unique ID for the method, and then starting the pre and post enhancement operation of the method, that is, adding monitoring code to the method block.
  • Finally, return the bytecode information return ctclass toBytecode(); Now your newly added bytecode can be loaded and processed by the program.

4. Operation test

4.1 configuring VM parameters Javaagent

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

4.2 test results

monitor -  Begin By Javassist
 method: cn.bugstack.middleware.test.interfaces.UserController$$EnhancerBySpringCGLIB$$8f5a18ca.queryUserInfo
 Input: null Input parameter type:["Ljava/lang/String;"] Input number[value]: ["aaa"]
Output reference: Lcn/bugstack/middleware/test/interfaces/dto/UserInfo; Out parameter[value]: {"address":"No. 14, Wanke Shangxiyuan, Dongli District, Tianjin-0000","age":19,"code":"0000","info":"success","name":"stifling:aaa"}
Time consuming: 46(s)
monitor - End
  • From the test results, the effect of bytecode insertion is the same as that of ASM, and the execution information of the monitoring system can be achieved. However, such a framework will make the development process simpler and easier to control.

6, Byte buddy

In October 2015, Byte Buddy was awarded Duke's Choice Award by Oracle. The award appreciates Byte Buddy's "great innovation in Java technology". We are 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 operation library used to create and modify Java classes when Java applications run without the help of the compiler. In addition to the code generation utility included with the Java class library, Byte Buddy allows the creation of any class and is not limited to implementing the interface used to create a runtime agent. In addition, Byte Buddy provides a convenient API for using java proxies or manually changing classes during construction.

  • Without understanding bytecode instructions, you can use a simple API to easily operate bytecode, control classes and methods.
  • Java 11 has been supported and the library is lightweight. It only depends on the visitor API of the Java bytecode parser library ASM, which itself does not need any other dependencies.
  • Compared with JDK dynamic proxy, cglib and Javassist, Byte Buddy has certain advantages in performance.

1. Let's have a test first

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 "Hello world!" generated using ByteBuddy syntax Case, his running result is one line, hello world!, The core function of the whole code block is to find the toString method through method(named("toString"), and then set the return value of this method by intercepting intercept. FixedValue.value("Hello World!"). Here, in fact, a basic method is to load, initialize and call the output through byte buddy.

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 convenience of operation, it can use bytecode enhanced operation like using ordinary business code. From the current engineering structure, you can see that the number of code classes is getting smaller and smaller.

3. Monitoring method: pile insertion

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("monitor - Begin By Byte-buddy");
            System.out.println("Method name:" + method.getName());
            System.out.println("Number of input parameters:" + method.getParameterCount());
            for (int i = 0; i < method.getParameterCount(); i++) {
                System.out.println("Input parameter Idx: " + (i + 1) + " Type:" + method.getParameterTypes()[i].getTypeName() + " Content:" + args[i]);
            }
            System.out.println("Output parameter type:" + method.getReturnType().getName());
            System.out.println("Output results:" + resObj);
            System.out.println("Method time consuming:" + (System.currentTimeMillis() - start) + "ms");
            System.out.println("monitor - End\r\n");
        }
    }

}
  • @Origin is 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 obtained, so that cyclic output can be carried out during subsequent parameter processing.

Common notes

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

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

Common core API s

  1. ByteBuddy

    • Streaming API entry class
    • The bytecode can be rewritten in Subclassing/Redefining/Rebasing mode
    • All operations depend on dynamictype Builder to create immutable objects
  2. ElementMatchers(ElementMatcher)

    • Provide a series of tool classes for element matching (named/any/nameEndsWith, etc.)
    • Elementmatcher (provides a way to match types, methods, fields and annotations, similar to predict)
    • Junction performs and/or operations on multiple elementmatchers
  3. DynamicType

    (dynamic type, the beginning of all bytecode operations, is of great concern)

    • Unloaded (the dynamically created bytecode has not been loaded into the virtual machine and needs to be loaded by the class loader)
    • Loaded (after being loaded into the jvm, the Class representation is resolved)
    • Default (the default implementation of dynamictype to complete relevant actual operations)
  4. `Implementation

    (used to provide implementation of dynamic methods)

    • Fixedvalue (method call returns a fixed value)
    • Methoddelegation (method call delegation, which supports two methods: static method call of Class and instance method call of object)
  5. Builder

    (used to create DynamicType, related interfaces and implementation, to be explained later)

    • MethodDefinition
    • FieldDefinition
    • AbstractBase

4. Configuration entry method

cn.bugstack.middleware.monitor.PreMain

public class PreMain {

    //The JVM first attempts to call the following methods on the proxy class
    public static void premain(String agentArgs, Instrumentation inst) {
        AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
            return builder
                    .method(ElementMatchers.named("queryUserInfo")) // Intercept arbitrary methods
                    .intercept(MethodDelegation.to(MonitorMethod.class)); // entrust
        };

        new AgentBuilder
                .Default()
                .type(ElementMatchers.nameStartsWith(agentArgs))  // Specify the class "cn.bugstack.demo.test" to be intercepted
                .transform(transformer)
                .installOn(inst);
    }

    //If the proxy class does not implement the above method, the JVM will try to call the method
    public static void premain(String agentArgs) {
    }

}
  • The premain method is mainly used to delegate the implemented MonitorMethod. At the same time, the interception method is also set in the method. This interception method can also go to the class path, etc.

5. Operation test

5.1 configuring VM parameters Javaagent

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

5.2 test results

monitor - Begin By Byte-buddy
 Method name: queryUserInfo
 Number of input parameters: 1
 Input parameter Idx: 1 Type: java.lang.String Content: aaa
 Output parameter type: cn.bugstack.middleware.test.interfaces.dto.UserInfo
 Output results: cn.bugstack.middleware.test.interfaces.dto.@214b199c
 Method time: 1 ms
 monitor - End
  • Byte buddy is the simplest and most convenient bytecode framework in the whole test process. It is also very easy to expand information. The whole process is as simple as using AOP initially, but it meets the non-invasive monitoring requirements.
  • Therefore, when using bytecode framework, you can consider using byte buddy, a very easy-to-use bytecode framework.

7, Summary

  • ASM is widely used in bytecode programming, but it may not be seen at ordinary times, because it is used as a supporting service in combination with other frameworks. There are many other technologies like javassit, Cglib, Jacobo, and so on.
  • Javassist is widely used in some components of full link monitoring. It can operate bytecode enhancement in the way of coding or handle it like ASM.
  • Byte buddy is a very convenient framework, which is more and more widely used, and the learning difficulty is the lowest among several frameworks. In addition to the case introduction in this chapter, you can also use the official website: https://bytebuddy.net , 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

Topics: Java asm