For more, check out my blog, Codercc
1. Method Monitoring background
In daily development, a lot of logs are usually printed, such as method entry and exit parameters, and traceid series, etc. The purpose is to do a good job in link monitoring and daily troubleshooting. In addition, to meet the requirements of the company to isolate different BU service lines, certain formats are required for the output of logs. In this case, it is obvious that a unified common module can be made by means of AOP. How to achieve non-invasive monitoring of business applications and provide good performance is a very important point.
In order to solve this kind of problem, in the actual development, the method monitoring is completed by the way of [Java Agent + bytecode piling]. Agent provides the opportunity to intercept bytecode loading before or after the execution of main business application, and then completes the injection of business logic by changing the bytecode class. In fact, this is a concrete implementation of AOP ideas. Bytecode staking, at first thought to be a very sophisticated and advanced technical term, is actually only a means to change the class bytecode. There are many technical solutions, such as cglib, javaassit and so on.
2. Pile insertion scheme selection
Java Proxy, Cglib, Javaassit, and ASM are common bytecode manipulation tools, including JDK Proxy, Cglib, Javaassit, and ASM.
Content/technical tools | asm | javaassit | cglib | jdk |
---|---|---|---|---|
Tool background | The underlying bytecode framework operates at the assembly instruction level of the underlying JVM, requiring ASM users to have some understanding of the class organization structure and JVM assembly instructions | Java bytecode is an open source library for analyzing, editing, and creating Java bytecode. It was created by Shigeru Chiba of the Department of Mathematics and computer Science at Tokyo Institute of Technology. It has joined the open source JBoss application Server project to implement a dynamic AOP framework for JBoss by using Javassist for bytecode manipulation | They are widely used by many AOP frameworks, such as Spring AOP and Dynaop, who provide an interception for their methods. | JDK dynamic proxy Based on interface, the generated proxy classes are cached, and only one proxy class is generated for each interface |
convenience | Need to have an understanding of bytecode, development is difficult, not easy to get started | The section logic can be developed in Java code directly through the API provided. Low difficulty of operation, high convenience | The underlying framework uses this tool more, more reference cases, learning cost is medium, but also easier to use | Development is relatively easy, but scenarios are limited by the need for proxyed classes to implement interfaces |
performance | high | If javaassit is used directly to generate bytecode for propped classes, the performance is comparable to that of ASMMethodHandler Generating proxy classes is slow and has poor performance |
Poor performance | The performance of the worst |
By comparing common bytecode change tools, you can choose the appropriate tool for different business scenarios. For example, javaassit and cglib tools can be used without considering performance loss. If performance concerns are serious, asm is the most suitable tool. In terms of ease of use, Javaassit is a relatively suitable tool that can inject business logic directly in Java code via APIS at a relatively low cost.
In view of the actual scene of the work, because the real-time service link has certain requirements for performance, in addition, combined with other considerations, FINALLY selected ASM as a bytecode change tool. The main functions are service monitoring and parameter collection at the method level (including method entry and exit parameters, service time and exceptions), traceid insertion, and log standardization (unified output in accordance with the log format required by the group, which is convenient for subsequent use of ELK tool). The overall idea is shown in the figure below:
3. Asm is introduced
ASM is a Java bytecode manipulation framework. It can be used to dynamically generate classes or enhance the functionality of existing classes. ASM can either generate binary class files directly or dynamically change the behavior of classes before they are loaded into the Java virtual machine. Java classes are stored in rigorously formatted.class files that have enough metadata to parse all the elements of the class: class names, methods, attributes, and Java bytecodes (instructions). After reading information from class files, ASM can change class behavior, analyze class information, and even generate new classes based on user requirements.
The ASM package contains these modules:
-
Core: Provides basic apis for other packages to read, write, convert Java bytecodes and define them, and can generate Java bytecodes and implement most of the bytecodes conversion. In ASM, bytecodes are accessed through accessor pattern design. Several important classes are in the Core API: ClassReader, ClassVisitor, and ClassWriter classes;
-
Tree: provides an in-memory representation of Java bytecode;
-
Commons: Provides some common classes and adapters to simplify bytecode generation and transformation;
-
Util: contains helper classes and simple bytecode modification classes for use in development or testing.
-
XML: Provides an adapter to convert XML and SAX-Comliant into a bytecode structure that allows bytecode transformation to be defined using XSLT;
ASM uses visitor mode to scan the contents of the.class class file from beginning to end. Each time the corresponding contents of the class file are scanned, the corresponding methods inside the ClassVisitor are called. Such as:
- A callback is called when a class file is scanned
ClassVisitor
thevisit()
Methods; - A callback is called when a class annotation is scanned
ClassVisitor
thevisitAnnotation()
Methods; - A callback is called when a class member is scanned
ClassVisitor
thevisitField()
Methods; - A callback is called when a class method is scanned
ClassVisitor
thevisitMethod()
Methods; When the structure content is scanned, the corresponding method is called back, which returns a corresponding bytecode operation object (e.g.,visitMethod()
returnMethodVisitor
Instance), can be modified by modifying this objectclass
File corresponding structure section content, finally will thisClassVisitor
Bytecode content overrides original.class
The file implements code entry into the class file.
Bytecode region | Asm interface |
---|---|
Class | ClassVisitor |
Field | FieldVisitor |
Method | MethodVisitor |
Annotation | AnnotationVisitor |
4. Implementation
4.1 Implementation Result
Using agent and ASM bytecode staking to accomplish method-level business monitoring, you can see an example of the result. Here is an example of a business using the test method:
public String test(Long userId) {
long userAge = this.getUserAge();
User user = new User();
user.setAge("20");
user.setName("hello world");
test1((byte) 1, (short) 1.1.1.1.1.false.'a');
test2("test2".2);
test3(user, 10);
test4(user, "test4");
test5((byte) 1, (short) 1.1.1.1.1.false.'a');
test6(user, "test6");
test7("test7");
test8(user, "test8");
}
private int getUserAge(a) {
return 15;
}
Copy the code
After the Agent is configured using javaAgent, the following output is displayed after the method is executed:
[INFO][2021-06-24T14:46:10.969+0800][http-nio-8080-exec-1:AresLogUtil.java:41] _am||traceid=ac17ea7b60d42a326678246f00000186||spanid=2c04c200030cd61f||cspanid=||type=INPUT||uri=com.example.test.impl. AgentTestServiceImpl.test||proc_time=||params=[{"type":"Ljava/lang/Long;"."value":1000}] | | errno = I88888 | | errmsg = ARES normal log [INFO] [2021-06-24T14:46:10.970+0800][http-nio-8080-exec-1:AresLogUtil.java:41] _am||traceid=ac17ea7b60d42a326678246f00000186||spanid=2c04c200030cd61f||cspanid=||type=INPUT||uri=com.example.test.impl. AgentTestServiceImpl. GetUserAge | | proc_time = | | params = | | errno = I88888 | | errmsg = ARES normal log [INFO] [2021-06-24T14:46:10.974+0800][http-nio-8080-exec-1:AresProbeDataProcessor.java:45]
_am||traceid=ac17ea7b60d42a326678246f00000186||spanid=2c04c200030cd61f||{"classNameStr":"com.example.test.impl.AgentTestServiceImpl"."methodNameStr":"getUserAge"."parametersTypes": []."returnObjType":"I"}
[INFO][2021-06-24T14:46:10.979+0800][http-nio-8080-exec-1:AresProbeDataProcessor.java:48]
_am||traceid=ac17ea7b60d42a326678246f00000186||spanid=2c04c200030cd61f||{"className":"com.example.test.impl.AgentTestServiceImpl"."methodName":"getUserAge"."params": [{"type":"I"."value":15}, {"name":"cost"."type":"long"."value":4.222047}]."type":"OUTPUT"}
[INFO][2021-06-24T14:46:10.980+0800][http-nio-8080-exec-1:AresLogUtil.java:41]...Copy the code
Two main things are done in log output: 1. Log method input and output parameters, service time, and exceptions. 2. If the current thread is not tagged with traceid on the trace concatenation, it will be retagged. With the context of trace and service completion, the ELK tool can efficiently complete troubleshooting and service monitoring. Of course, because the increase of log volume will bring the cost of machine log storage, it can be cold standby. There is no best technical solution, only suitable. In this business scenario, the cost of space is sacrificed for the efficiency of troubleshooting and the stability benefits of service monitoring. Why do you have these standardized log outputs when a business method is running? After asm staking, the code for the original business method is changed to the following:
private int getUserAge(a) {
long var1 = System.nanoTime();
AresProbeDataProcessor.probeInput("com.example.test.impl.AgentTestServiceImpl"."getUserAge"."[]", (Object[])null."I");
Integer var3 = 15;
AresProbeDataProcessor.probeOutput("com.example.test.impl.AgentTestServiceImpl"."getUserAge"."[]"."I", var3, var1);
return 15;
}
public void test1(byte var1, short var2, int var3, long var4, float var6, double var7, boolean var9, char var10) {
long var11 = System.nanoTime();
Object[] var13 = new Object[]{var1, var2, var3, var4, var6, var7, var9, var10};
AresProbeDataProcessor.probeInput("com.example.test.impl.AgentTestServiceImpl"."test1"."[\"B\",\"S\",\"I\",\"J\",\"F\",\"D\",\"Z\",\"C\"]", var13, "V");
AresProbeDataProcessor.probeOutput("com.example.test.impl.AgentTestServiceImpl"."test1"."[\"B\",\"S\",\"I\",\"J\",\"F\",\"D\",\"Z\",\"C\"]"."V", (Object)null, var11);
}
Copy the code
As can be seen from the code after staking, in the original business method, input parameter collection and time-consuming calculation will be added, and finally log output will be completed through AresProbeDataProcessor, so as to complete the necessary information collection. How to inject business logic into the target method is a core problem to be solved.
4.2 Bytecode staking
By statically loading agents using Premain (see this article about agents), bytecode changes are made to the original business class by implementing the transform method of the ClassFileTransformer interface class.
@Override
public byte[] transform( ClassLoader loader, String clazzName, Class<? > classBeingRedefined, ProtectionDomain protectionDomain,byte[] classfileBuffer) {
try {
// Some business logic judgments (such as class path verification by parameter configuration, etc.) are omitted here
ClassReader classReader = new ClassReader(classfileBuffer);
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new AresClassVisitor(clazzName, classWriter, probeScanner);
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
return classWriter.toByteArray();
} catch (Exception e) {
throw newAresException(AresErrorCodeEnum.AGENT_ERROR.getErrorMsg(), e); }}Copy the code
The main thing here is to construct the ClassVisitor, which is the ability that the ASM framework provides to access the class bytecode.
public final class AresClassVisitor extends ClassVisitor {
private final String fullClassName;
private final ProbeScanner probeScanner;
private Boolean isInterface;
private final ClassVisitor cv;
public AresClassVisitor(String clazzName, final ClassVisitor classVisitor, ProbeScanner probeScanner) {
super(Opcodes.ASM5, classVisitor);
this.probeScanner = probeScanner;
this.cv = classVisitor;
if (Objects.isNull(clazzName) || "".equals(clazzName)) {
throw new AresException(AresErrorCodeEnum.CLASSNAME_BLANK.getErrorCode(), AresErrorCodeEnum.CLASSNAME_BLANK.getErrorMsg());
}
this.fullClassName = clazzName.replace("/".".");
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.isInterface = (access & Opcodes.ACC_INTERFACE) ! =0;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if (isExcludeProbe(access, name)) {
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
if (Objects.isNull(mv)) {
return null;
}
return new AresMethodVisitor(mv, access, name, descriptor, fullClassName);
}
private Boolean isExcludeProbe(int access, String name) {
// exclude interface
if (this.isInterface) {
return true;
}
if((access & Opcodes.ACC_ABSTRACT) ! =0|| (access & Opcodes.ACC_NATIVE) ! =0|| (access & Opcodes.ACC_BRIDGE) ! =0|| (access & Opcodes.ACC_SYNTHETIC) ! =0) {
return true;
}
return !this.probeScanner.isNeedProbeByMethodName(name); }}Copy the code
This is mainly to determine whether the current class is an interface, whether it is a native method, and whether some business rule filtering does not require method level staking. The AresMethodVisitor completes the insertion of the appropriate logic into the method entry. It is mainly divided into the following parts:
-
Entry parameter capture
protected void onMethodEnter(a) { this.probeStartTime(); this.probeInputParams(); this.acquireInputParams(); } /** ** ** / private void probeInputParams(a) { int parameterCount = this.paramsTypeList.size(); if (parameterCount <= 0) { return; } // init param array if (parameterCount >= ARRAY_THRESHOLD) { mv.visitVarInsn(Opcodes.BIPUSH, parameterCount); } else { switch (parameterCount) { case 1: mv.visitInsn(Opcodes.ICONST_1); break; case 2: mv.visitInsn(Opcodes.ICONST_2); break; case 3: mv.visitInsn(Opcodes.ICONST_3); break; default: mv.visitInsn(Opcodes.ICONST_0); } } mv.visitTypeInsn(Opcodes.ANEWARRAY, Type.getDescriptor(Object.class)); // local index int localCount = isStaticMethod ? -1 : 0; // assign value to array for (int i = 0; i < parameterCount; i++) { mv.visitInsn(Opcodes.DUP); if (i > LOCAL_INDEX) { mv.visitVarInsn(Opcodes.BIPUSH, i); } else { switch (i) { case 0: mv.visitInsn(Opcodes.ICONST_0); break; case 1: mv.visitInsn(Opcodes.ICONST_1); break; case 2: mv.visitInsn(Opcodes.ICONST_2); break; case 3: mv.visitInsn(Opcodes.ICONST_3); break; case 4: mv.visitInsn(Opcodes.ICONST_4); break; case 5: mv.visitInsn(Opcodes.ICONST_5); break; default: break; } } String type = this.paramsTypeList.get(i); if ("Z".equals(type)) { mv.visitVarInsn(Opcodes.ILOAD, ++localCount); mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Boolean.class), "valueOf"."(Z)Ljava/lang/Boolean;".false); } else if ("C".equals(type)) { mv.visitVarInsn(Opcodes.ILOAD, ++localCount); mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Character.class), "valueOf"."(C)Ljava/lang/Character;".false); } else if ("B".equals(type)) { mv.visitVarInsn(Opcodes.ILOAD, ++localCount); mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Byte.class), "valueOf"."(B)Ljava/lang/Byte;".false); } else if ("S".equals(type)) { mv.visitVarInsn(Opcodes.ILOAD, ++localCount); mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Short.class), "valueOf"."(S)Ljava/lang/Short;".false); } else if ("I".equals(type)) { mv.visitVarInsn(Opcodes.ILOAD, ++localCount); mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Integer.class), "valueOf"."(I)Ljava/lang/Integer;".false); } else if ("F".equals(type)) { mv.visitVarInsn(Opcodes.FLOAD, ++localCount); mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Float.class), "valueOf"."(F)Ljava/lang/Float;".false); } else if ("J".equals(type)) { mv.visitVarInsn(Opcodes.LLOAD, ++localCount); mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Long.class), "valueOf"."(J)Ljava/lang/Long;".false); localCount++; } else if ("D".equals(type)) { mv.visitVarInsn(Opcodes.DLOAD, ++localCount); mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Double.class), "valueOf"."(D)Ljava/lang/Double;".false); localCount++; } else { mv.visitVarInsn(Opcodes.ALOAD, ++localCount); } mv.visitInsn(Opcodes.AASTORE); } paramsLocal = newLocal(Type.LONG_TYPE); mv.visitVarInsn(Opcodes.ASTORE, paramsLocal); } Copy the code
We first construct an Object array to load the entry parameters of the method. Since the execution of a method in JVM is a stack frame structure, the entry parameters will be in the local variable table. When the method is being executed, the top of the stack is the current input parameter. Through ILOAD, FLOAD and other related instructions, the parameters on the top of the stack will be stored in a local variable, and then put into the Object array. See this article about JVM directives
-
Method entry implants the start time stamp of execution
/** * insert method start execution time */ private void probeStartTime(a) { mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System"."nanoTime"."()J".false); startTimeLocal = newLocal(Type.LONG_TYPE); mv.visitVarInsn(Opcodes.LSTORE, startTimeLocal); } Copy the code
INVOKESTATIC INVOKESTATIC invokes the System. nanotime method to obtain the current start time stamp, and puts it into the local variable startTimeLocal, which can be obtained once at the method exit, and calculates the method execution time.
-
Send the collected parameters to the data processing module
private void acquireInputParams(a) { mv.visitLdcInsn(this.className); mv.visitLdcInsn(this.methodName); mv.visitLdcInsn(this.paramTypes); if (this.paramsTypeList.isEmpty()) { mv.visitInsn(Opcodes.ACONST_NULL); } else { mv.visitVarInsn(Opcodes.ALOAD, this.paramsLocal); } mv.visitLdcInsn(this.returnType); mv.visitMethodInsn(INVOKESTATIC, "com/example/am/arch/ares/aspect/AresProbeDataProcessor"."probeInput"."(Ljava/lang/String; Ljava/lang/String; Ljava/lang/String; [Ljava/lang/Object;Ljava/lang/String;)V".false); } Copy the code
After steps 1 and 2, the entry parameters can be collected, so that the code calling AresProbeDataProcessor can be inserted into the raw bytecode and sent out for standardized processing. This is just a quick hint.)
-
Method To collect the parameter data
The general logic for capturing parameters at the method exit and handling exceptions is as follows:
protected void onMethodExit(int opcode) { super.onMethodExit(opcode); if (!this.containsTryCatchBlock) { if(Opcodes.RETURN ! = opcode) {// There is a return value to be processed, // Copy the original return value to the top of the stack to save, so as not to pollute the original return value mv.visitInsn(Opcodes.DUP); } // Handle try-catch-free modules processMethodExit(opcode); } else { // try-catch-finally does not get the return value, only the method timeprocessMethodExitWithTryCatchBlock(); }}/** * process the return value **@param opcode */ private void probeReturnBlock(int opcode) { switch (opcode) { case Opcodes.RETURN: break; // Runtime exceptions thrown explicitly by a throw can be handled // Exceptions that are implicitly excluded at runtime can only be handled by adding a try-catch // do not add a try-catch block case Opcodes.ARETURN: case Opcodes.ATHROW: this.returnObjLocal = this.nextLocal; mv.visitVarInsn(Opcodes.ASTORE, this.returnObjLocal); break; case Opcodes.IRETURN: this.returnObjLocal = this.nextLocal; this.handedIntReturnType(); break; case Opcodes.LRETURN: this.returnObjLocal = this.nextLocal; mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Float.class), "valueOf"."(F)Ljava/lang/Float;".false); visitVarInsn(Opcodes.ASTORE, this.returnObjLocal); break; case Opcodes.DRETURN: this.returnObjLocal = this.nextLocal; mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Float.class), "valueOf"."(F)Ljava/lang/Float;".false); visitVarInsn(Opcodes.ASTORE, this.returnObjLocal); break; case Opcodes.FRETURN: this.returnObjLocal = this.nextLocal; mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Float.class), "valueOf"."(F)Ljava/lang/Float;".false); visitVarInsn(Opcodes.ASTORE, this.returnObjLocal); default: break; }}private void processMethodExit(int opcode) { // Capture the return value this.probeReturnBlock(opcode); // Call the peg return method mv.visitLdcInsn(this.className); mv.visitLdcInsn(this.methodName); mv.visitLdcInsn(this.paramTypes); mv.visitLdcInsn(this.returnType); if (Opcodes.RETURN == opcode) { mv.visitInsn(Opcodes.ACONST_NULL); } else { mv.visitVarInsn(Opcodes.ALOAD, this.returnObjLocal); } mv.visitVarInsn(Opcodes.LLOAD, this.startTimeLocal); mv.visitMethodInsn(INVOKESTATIC, "com/example/am/arch/ares/aspect/AresProbeDataProcessor"."probeOutput"."(Ljava/lang/String; Ljava/lang/String; Ljava/lang/String; Ljava/lang/String; Ljava/lang/Object; J)V".false); } private void processMethodExitWithTryCatchBlock(a) { mv.visitLdcInsn(this.className); mv.visitLdcInsn(this.methodName); mv.visitLdcInsn(this.paramTypes); mv.visitLdcInsn(this.returnType); mv.visitVarInsn(Opcodes.LLOAD, this.startTimeLocal); mv.visitMethodInsn(INVOKESTATIC, "com/example/am/arch/ares/aspect/AresProbeDataProcessor"."probeCostTime"."(Ljava/lang/String; Ljava/lang/String; Ljava/lang/String; Ljava/lang/String; J)V".false); } Copy the code
ALOAD is used to assign the return value at the top of the stack to local variables so that the return parameters can be sent to the processing module. A key point to note is that direct operation on the return value of the original method will “pollute” the return value of the original method and affect the logical correctness of the original method. Therefore, before processing the return value, a copy of the return value is provided to the data processing module without any changes to the original business method return value. The return value copy instruction is mv.visitinsn (opcodes.dup); . If there are try-catch-finnaly method blocks, there are some problems in capturing the parameters, so the debugging has not been successful for the time being. If you know, please contact me for advice.
Char /byte/ Boolean /short are all represented as ints in the bytecode. In order to obtain the field value without ambiguity, we can only convert these basic types to the corresponding reference type data. The specific conversion can be substituted as:
/ * * * char/byte/Boolean/short/int in the bytecode is using int * so need to distinguish * /
private void handedIntReturnType(a) {
if ("B".equals(this.returnType)) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Byte.class), "valueOf"."(B)Ljava/lang/Byte;".false);
visitVarInsn(Opcodes.ASTORE, this.returnObjLocal);
return;
}
if ("S".equals(this.returnType)) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Short.class), "valueOf"."(S)Ljava/lang/Short;".false);
visitVarInsn(Opcodes.ASTORE, this.returnObjLocal);
return;
}
if ("C".equals(this.returnType)) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Character.class), "valueOf"."(C)Ljava/lang/Character;".false);
visitVarInsn(Opcodes.ASTORE, this.returnObjLocal);
return;
}
if ("Z".equals(this.returnType)) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Boolean.class), "valueOf"."(Z)Ljava/lang/Boolean;".false);
visitVarInsn(Opcodes.ASTORE, this.returnObjLocal);
return;
}
if ("I".equals(this.returnType)) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Integer.class), "valueOf"."(I)Ljava/lang/Integer;".false);
visitVarInsn(Opcodes.ASTORE, this.returnObjLocal); }}Copy the code
You also need to extract the return value type and the input parameter type from the descriptor, which makes up the complete method context data.
5. To summarize
Bytecode staking is essentially a specific implementation of dynamic proxy, and the modification of bytecode itself to complete the injection of business logic is non-invasive for business applications, and the bytecode based method can also meet the requirements of high-performance online link. And in the implementation process will go deep into the actual application of JVM Bytecode instructions is also a great fun. In addition, in the use of ASM, the ASM Bytecode plugin can be more efficient to get the Bytecode instructions corresponding to the business code to be injected. Jclasslib Bytecode Viewer is also used to analyze Bytecode.
The resources
- Introduction to ASM and ASM API
- Segmentfault.com/a/119000002…
- zhuanlan.zhihu.com/p/126299707
- www.infoq.cn/article/Liv…